前言
这周去杭州参加了百阿培训,见到了传说中的牛人多隆大神。从多隆大神身上看到了做技术人的纯粹,单纯。除了见到多隆大神,这次培训并没有太多的收获,反而培训过程中遇到了好多产品上的Bug,远程办公快累到死。总结一下跟Toast相关的问题,首先从深入学习Toast的源码实现开始。
Toast源码实现
Toast入口
我们在应用中使用Toast提示的时候,一般都是一行简单的代码调用,如下所示:
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
makeText就是Toast的入口,我们从makeText的源码来深入理解Toast的实现。源码如下(frameworks/base/core/java/android/widget/Toast.java):
-
public static Toast makeText(Context context, CharSequence text, int duration) {
-
Toast result = new Toast(context);
-
-
LayoutInflater inflate = (LayoutInflater)
-
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
-
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
-
tv.setText(text);
-
-
result.mNextView = v;
-
result.mDuration = duration;
-
-
return result;
-
}
-
"1.0" encoding="utf-8" xml version=
-
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-
android:layout_width= "match_parent"
-
android:layout_height= "match_parent"
-
android:orientation= "vertical"
-
android:background= "?android:attr/toastFrameBackground">
-
-
<TextView
-
android:id= "@android:id/message"
-
android:layout_width= "wrap_content"
-
android:layout_height= "wrap_content"
-
android:layout_weight= "1"
-
android:layout_gravity= "center_horizontal"
-
android:textAppearance= "@style/TextAppearance.Toast"
-
android:textColor= "@color/bright_foreground_dark"
-
android:shadowColor= "#BB000000"
-
android:shadowRadius= "2.75"
-
/>
-
-
</LinearLayout>
-
public void show() {
-
if (mNextView == null) {
-
throw new RuntimeException( "setView must have been called");
-
}
-
-
INotificationManager service = getService();
-
String pkg = mContext.getPackageName();
-
TN tn = mTN;
-
tn.mNextView = mNextView;
-
-
try {
-
service.enqueueToast(pkg, tn, mDuration);
-
} catch (RemoteException e) {
-
// Empty
-
}
-
}
TN源码
很多问题都能通过阅读源码找到答案,关键在与你是否有与之匹配的耐心和坚持。mTN的实现在Toast的构造函数中,源码如下:
-
public Toast(Context context) {
-
mContext = context;
-
mTN = new TN();
-
mTN.mY = context.getResources().getDimensionPixelSize(
-
com.android.internal.R.dimen.toast_y_offset);
-
mTN.mGravity = context.getResources().getInteger(
-
com.android.internal.R.integer.config_toastDefaultGravity);
-
}
接下来,我们就从TN类的源码出发,探寻TN的作用。TN源码如下:
-
private static class TN extends ITransientNotification.Stub {
-
final Runnable mShow = new Runnable() {
-
-
public void run() {
-
handleShow();
-
}
-
};
-
-
final Runnable mHide = new Runnable() {
-
-
public void run() {
-
handleHide();
-
// Don't do this in handleHide() because it is also invoked by handleShow()
-
mNextView = null;
-
}
-
};
-
-
private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
-
final Handler mHandler = new Handler();
-
-
int mGravity;
-
int mX, mY;
-
float mHorizontalMargin;
-
float mVerticalMargin;
-
-
-
View mView;
-
View mNextView;
-
-
WindowManager mWM;
-
-
TN() {
-
// XXX This should be changed to use a Dialog, with a Theme.Toast
-
// defined that sets up the layout params appropriately.
-
final WindowManager.LayoutParams params = mParams;
-
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
-
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
-
params.format = PixelFormat.TRANSLUCENT;
-
params.windowAnimations = com.android.internal.R.style.Animation_Toast;
-
params.type = WindowManager.LayoutParams.TYPE_TOAST;
-
params.setTitle( "Toast");
-
params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
-
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
-
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
-
/// M: [ALPS00517576] Support multi-user
-
params.privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
-
}
-
-
/**
-
* schedule handleShow into the right thread
-
*/
-
-
public void show() {
-
if (localLOGV) Log.v(TAG, "SHOW: " + this);
-
mHandler.post(mShow);
-
}
-
-
/**
-
* schedule handleHide into the right thread
-
*/
-
-
public void hide() {
-
if (localLOGV) Log.v(TAG, "HIDE: " + this);
-
mHandler.post(mHide);
-
}
-
-
public void handleShow() {
-
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
-
+ " mNextView=" + mNextView);
-
if (mView != mNextView) {
-
// remove the old view if necessary
-
handleHide();
-
mView = mNextView;
-
Context context = mView.getContext().getApplicationContext();
-
if (context == null) {
-
context = mView.getContext();
-
}
-
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
-
// We can resolve the Gravity here by using the Locale for getting
-
// the layout direction
-
final Configuration config = mView.getContext().getResources().getConfiguration();
-
final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
-
mParams.gravity = gravity;
-
if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
-
mParams.horizontalWeight = 1.0f;
-
}
-
if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
-
mParams.verticalWeight = 1.0f;
-
}
-
mParams.x = mX;
-
mParams.y = mY;
-
mParams.verticalMargin = mVerticalMargin;
-
mParams.horizontalMargin = mHorizontalMargin;
-
if (mView.getParent() != null) {
-
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
-
mWM.removeView(mView);
-
}
-
if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
-
mWM.addView(mView, mParams);
-
trySendAccessibilityEvent();
-
}
-
}
-
-
private void trySendAccessibilityEvent() {
-
AccessibilityManager accessibilityManager =
-
AccessibilityManager.getInstance(mView.getContext());
-
if (!accessibilityManager.isEnabled()) {
-
return;
-
}
-
// treat toasts as notifications since they are used to
-
// announce a transient piece of information to the user
-
AccessibilityEvent event = AccessibilityEvent.obtain(
-
AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
-
event.setClassName(getClass().getName());
-
event.setPackageName(mView.getContext().getPackageName());
-
mView.dispatchPopulateAccessibilityEvent(event);
-
accessibilityManager.sendAccessibilityEvent(event);
-
}
-
-
public void handleHide() {
-
if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
-
if (mView != null) {
-
// note: checking parent() just to make sure the view has
-
// been added... i have seen cases where we get here when
-
// the view isn't yet added, so let's try not to crash.
-
if (mView.getParent() != null) {
-
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
-
mWM.removeView(mView);
-
}
-
-
mView = null;
-
}
-
}
-
}
TN类继承自ITransientNotification.Stub,ITransientNotification.aidl位于frameworks/base/core/java/android/app/ITransientNotification.aidl,源码如下:
-
package android.app;
-
-
/** @hide */
-
oneway interface ITransientNotification {
-
void show();
-
void hide();
-
}
-
/**
-
* schedule handleShow into the right thread
-
*/
-
-
public void show() {
-
if (localLOGV) Log.v(TAG, "SHOW: " + this);
-
mHandler.post(mShow);
-
}
-
-
/**
-
* schedule handleHide into the right thread
-
*/
-
-
public void hide() {
-
if (localLOGV) Log.v(TAG, "HIDE: " + this);
-
mHandler.post(mHide);
-
}
final Handler mHandler = new Handler();
而且,我们在TN类中没有发现任何Looper.perpare()和Looper.loop()方法。说明,mHandler调用的是当前所在线程的Looper对象。所以,当我们在主线程(也就是UI线程中)可以随意调用Toast.makeText方法,因为Android系统帮我们实现了主线程的Looper初始化。但是,如果你想在子线程中调用Toast.makeText方法,就必须先进行Looper初始化了,不然就会报出
java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare() 。Handler机制的学习可以参考我之前写过的一篇博客:http://blog.csdn.net/wzy_1988/article/details/38346637。
接下来,继续跟一下mShow和mHide的实现,它俩的类型都是Runnable。
-
final Runnable mShow = new Runnable() {
-
-
public void run() {
-
handleShow();
-
}
-
};
-
-
final Runnable mHide = new Runnable() {
-
-
public void run() {
-
handleHide();
-
// Don't do this in handleHide() because it is also invoked by handleShow()
-
mNextView = null;
-
}
-
};
-
public void handleShow() {
-
if (mView != mNextView) {
-
// remove the old view if necessary
-
handleHide();
-
mView = mNextView;
-
Context context = mView.getContext().getApplicationContext();
-
if (context == null) {
-
context = mView.getContext();
-
}
-
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
-
// We can resolve the Gravity here by using the Locale for getting
-
// the layout direction
-
final Configuration config = mView.getContext().getResources().getConfiguration();
-
final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
-
mParams.gravity = gravity;
-
if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
-
mParams.horizontalWeight = 1.0f;
-
}
-
if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
-
mParams.verticalWeight = 1.0f;
-
}
-
mParams.x = mX;
-
mParams.y = mY;
-
mParams.verticalMargin = mVerticalMargin;
-
mParams.horizontalMargin = mHorizontalMargin;
-
if (mView.getParent() != null) {
-
mWM.removeView(mView);
-
}
-
mWM.addView(mView, mParams);
-
trySendAccessibilityEvent();
-
}
-
}
总结一下,通过对TN类的源码分析,我们知道了TN类是回调对象,其他进程调用tn类的show和hide方法来控制这个Toast的显示和消失。
NotificationManagerService
回到Toast类的show方法中,我们可以看到,这里调用了getService得到INotificationManager服务,源码如下:
-
private static INotificationManager sService;
-
-
static private INotificationManager getService() {
-
if (sService != null) {
-
return sService;
-
}
-
sService = INotificationManager.Stub.asInterface(ServiceManager.getService( "notification"));
-
return sService;
-
}
这里INofiticationManager接口的具体实现类是NotificationManagerService类,位于frameworks/base/services/java/com/android/server/NotificationManagerService.java。
首先,我们来分析一下Toast入队的函数实现enqueueToast,源码如下:
-
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
-
{
-
// packageName为null或者tn类为null,直接返回,不进队列
-
if (pkg == null || callback == null) {
-
return ;
-
}
-
-
// (1) 判断是否为系统Toast
-
final boolean isSystemToast = isCallerSystem() || ( "android".equals(pkg));
-
-
// 判断当前toast所属的pkg是否为系统不允许发生Toast的pkg.NotificationManagerService有一个HashSet数据结构,存储了不允许发生Toast的包名
-
if (ENABLE_BLOCKED_TOASTS && !noteNotificationOp(pkg, Binder.getCallingUid()) && !areNotificationsEnabledForPackageInt(pkg)) {
-
if (!isSystemToast) {
-
return;
-
}
-
}
-
-
synchronized (mToastQueue) {
-
int callingPid = Binder.getCallingPid();
-
long callingId = Binder.clearCallingIdentity();
-
try {
-
ToastRecord record;
-
// (2) 查看该Toast是否已经在队列当中
-
int index = indexOfToastLocked(pkg, callback);
-
// 如果Toast已经在队列中,我们只需要更新显示时间即可
-
if (index >= 0) {
-
record = mToastQueue.get(index);
-
record.update(duration);
-
} else {
-
// 非系统Toast,每个pkg在当前mToastQueue中Toast有总数限制,不能超过MAX_PACKAGE_NOTIFICATIONS
-
if (!isSystemToast) {
-
int count = 0;
-
final int N = mToastQueue.size();
-
for ( int i= 0; i<N; i++) {
-
final ToastRecord r = mToastQueue.get(i);
-
if (r.pkg.equals(pkg)) {
-
count++;
-
if (count >= MAX_PACKAGE_NOTIFICATIONS) {
-
Slog.e(TAG, "Package has already posted " + count
-
+ " toasts. Not showing more. Package=" + pkg);
-
return;
-
}
-
}
-
}
-
}
-
-
// 将Toast封装成ToastRecord对象,放入mToastQueue中
-
record = new ToastRecord(callingPid, pkg, callback, duration);
-
mToastQueue.add(record);
-
index = mToastQueue.size() - 1;
-
// (3) 将当前Toast所在的进程设置为前台进程
-
keepProcessAliveLocked(callingPid);
-
}
-
// (4) 如果index为0,说明当前入队的Toast在队头,需要调用showNextToastLocked方法直接显示
-
if (index == 0) {
-
showNextToastLocked();
-
}
-
} finally {
-
Binder.restoreCallingIdentity(callingId);
-
}
-
}
-
}
可以看到,我对上述代码做了简要的注释。代码相对简单,但是还有4点标注代码需要我们来进一步探讨。
(1) 判断是否为系统Toast。如果当前Toast所属的进程的包名为“android”,则为系统Toast,否则还可以调用isCallerSystem()方法来判断。该方法的实现源码为:
-
boolean isUidSystem(int uid) {
-
final int appid = UserHandle.getAppId(uid);
-
return (appid == Process.SYSTEM_UID || appid == Process.PHONE_UID || uid == 0);
-
}
-
boolean isCallerSystem() {
-
return isUidSystem(Binder.getCallingUid());
-
}
是否为系统Toast,通过下面的源码阅读可知,主要有两点优势:
- 系统Toast一定可以进入到系统Toast队列中,不会被黑名单阻止。
- 系统Toast在系统Toast队列中没有数量限制,而普通pkg所发送的Toast在系统Toast队列中有数量限制。
(2) 查看将要入队的Toast是否已经在系统Toast队列中。这是通过比对pkg和callback来实现的,具体源码如下所示:
-
private int indexOfToastLocked(String pkg, ITransientNotification callback)
-
{
-
IBinder cbak = callback.asBinder();
-
ArrayList<ToastRecord> list = mToastQueue;
-
int len = list.size();
-
for ( int i= 0; i<len; i++) {
-
ToastRecord r = list.get(i);
-
if (r.pkg.equals(pkg) && r.callback.asBinder() == cbak) {
-
return i;
-
}
-
}
-
return - 1;
-
}
(3) 将当前Toast所在进程设置为前台进程。源码如下所示:
-
private void keepProcessAliveLocked(int pid)
-
{
-
int toastCount = 0; // toasts from this pid
-
ArrayList<ToastRecord> list = mToastQueue;
-
int N = list.size();
-
for ( int i= 0; i<N; i++) {
-
ToastRecord r = list.get(i);
-
if (r.pid == pid) {
-
toastCount++;
-
}
-
}
-
try {
-
mAm.setProcessForeground(mForegroundToken, pid, toastCount > 0);
-
} catch (RemoteException e) {
-
// Shouldn't happen.
-
}
-
}
(4) index为0时,对队列头的Toast进行显示。源码如下:
-
private void showNextToastLocked() {
-
// 获取队列头的ToastRecord
-
ToastRecord record = mToastQueue.get( 0);
-
while (record != null) {
-
try {
-
// 调用Toast的回调对象中的show方法对Toast进行展示
-
record.callback.show();
-
scheduleTimeoutLocked(record);
-
return;
-
} catch (RemoteException e) {
-
Slog.w(TAG, "Object died trying to show notification " + record.callback
-
+ " in package " + record.pkg);
-
// remove it from the list and let the process die
-
int index = mToastQueue.indexOf(record);
-
if (index >= 0) {
-
mToastQueue.remove(index);
-
}
-
keepProcessAliveLocked(record.pid);
-
if (mToastQueue.size() > 0) {
-
record = mToastQueue.get( 0);
-
} else {
-
record = null;
-
}
-
}
-
}
-
}
-
private static final int LONG_DELAY = 3500; // 3.5 seconds
-
private static final int SHORT_DELAY = 2000; // 2 seconds
-
private void scheduleTimeoutLocked(ToastRecord r)
-
{
-
mHandler.removeCallbacksAndMessages(r);
-
Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
-
long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
-
mHandler.sendMessageDelayed(m, delay);
-
}
接下来,我们来看一下WorkerHandler中是如何处理MESSAGE_TIMEOUT消息的。mHandler对象的类型为WorkerHandler,源码如下:
-
private final class WorkerHandler extends Handler
-
{
-
-
public void handleMessage(Message msg)
-
{
-
switch (msg.what)
-
{
-
case MESSAGE_TIMEOUT:
-
handleTimeout((ToastRecord)msg.obj);
-
break;
-
}
-
}
-
}
-
private void handleTimeout(ToastRecord record)
-
{
-
synchronized (mToastQueue) {
-
int index = indexOfToastLocked(record.pkg, record.callback);
-
if (index >= 0) {
-
cancelToastLocked(index);
-
}
-
}
-
}
-
private void cancelToastLocked(int index) {
-
ToastRecord record = mToastQueue.get(index);
-
try {
-
record.callback.hide();
-
} catch (RemoteException e) {
-
// don't worry about this, we're about to remove it from
-
// the list anyway
-
}
-
mToastQueue.remove(index);
-
keepProcessAliveLocked(record.pid);
-
if (mToastQueue.size() > 0) {
-
// Show the next one. If the callback fails, this will remove
-
// it from the list, so don't assume that the list hasn't changed
-
// after this point.
-
showNextToastLocked();
-
}
-
}
原地址:https://blog.csdn.net/wzy_1988/article/details/43341761
如有侵权,请告之,速删!!!