Android Toast源码分析



前言

    这周去杭州参加了百阿培训,见到了传说中的牛人多隆大神。从多隆大神身上看到了做技术人的纯粹,单纯。除了见到多隆大神,这次培训并没有太多的收获,反而培训过程中遇到了好多产品上的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):
  1. public static Toast makeText(Context context, CharSequence text, int duration) {
  2. Toast result = new Toast(context);
  3. LayoutInflater inflate = (LayoutInflater)
  4. context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  5. View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
  6. TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
  7. tv.setText(text);
  8. result.mNextView = v;
  9. result.mDuration = duration;
  10. return result;
  11. }
    从makeText的源码里,我们可以看出Toast的布局文件是transient_notification.xml,位于frameworks/base/core/res/res/layout/transient_notification.xml:
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width= "match_parent"
  4. android:layout_height= "match_parent"
  5. android:orientation= "vertical"
  6. android:background= "?android:attr/toastFrameBackground">
  7. <TextView
  8. android:id= "@android:id/message"
  9. android:layout_width= "wrap_content"
  10. android:layout_height= "wrap_content"
  11. android:layout_weight= "1"
  12. android:layout_gravity= "center_horizontal"
  13. android:textAppearance= "@style/TextAppearance.Toast"
  14. android:textColor= "@color/bright_foreground_dark"
  15. android:shadowColor= "#BB000000"
  16. android:shadowRadius= "2.75"
  17. />
  18. </LinearLayout>
    系统Toast的布局文件非常简单,就是在垂直布局的LinearLayout里放置了一个TextView。接下来,我们继续跟到show()方法,研究一下布局形成之后的展示代码实现:
  1. public void show() {
  2. if (mNextView == null) {
  3. throw new RuntimeException( "setView must have been called");
  4. }
  5. INotificationManager service = getService();
  6. String pkg = mContext.getPackageName();
  7. TN tn = mTN;
  8. tn.mNextView = mNextView;
  9. try {
  10. service.enqueueToast(pkg, tn, mDuration);
  11. } catch (RemoteException e) {
  12. // Empty
  13. }
  14. }
    show方法中有两点是需要我们注意的。(1)TN是什么东东?(2)INotificationManager服务的作用。带着这两个问题,继续我们Toast源码的探索。

TN源码

    很多问题都能通过阅读源码找到答案,关键在与你是否有与之匹配的耐心和坚持。mTN的实现在Toast的构造函数中,源码如下:
  1. public Toast(Context context) {
  2. mContext = context;
  3. mTN = new TN();
  4. mTN.mY = context.getResources().getDimensionPixelSize(
  5. com.android.internal.R.dimen.toast_y_offset);
  6. mTN.mGravity = context.getResources().getInteger(
  7. com.android.internal.R.integer.config_toastDefaultGravity);
  8. }
    接下来,我们就从TN类的源码出发,探寻TN的作用。TN源码如下:
  1. private static class TN extends ITransientNotification.Stub {
  2. final Runnable mShow = new Runnable() {
  3. @Override
  4. public void run() {
  5. handleShow();
  6. }
  7. };
  8. final Runnable mHide = new Runnable() {
  9. @Override
  10. public void run() {
  11. handleHide();
  12. // Don't do this in handleHide() because it is also invoked by handleShow()
  13. mNextView = null;
  14. }
  15. };
  16. private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
  17. final Handler mHandler = new Handler();
  18. int mGravity;
  19. int mX, mY;
  20. float mHorizontalMargin;
  21. float mVerticalMargin;
  22. View mView;
  23. View mNextView;
  24. WindowManager mWM;
  25. TN() {
  26. // XXX This should be changed to use a Dialog, with a Theme.Toast
  27. // defined that sets up the layout params appropriately.
  28. final WindowManager.LayoutParams params = mParams;
  29. params.height = WindowManager.LayoutParams.WRAP_CONTENT;
  30. params.width = WindowManager.LayoutParams.WRAP_CONTENT;
  31. params.format = PixelFormat.TRANSLUCENT;
  32. params.windowAnimations = com.android.internal.R.style.Animation_Toast;
  33. params.type = WindowManager.LayoutParams.TYPE_TOAST;
  34. params.setTitle( "Toast");
  35. params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
  36. | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
  37. | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
  38. /// M: [ALPS00517576] Support multi-user
  39. params.privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
  40. }
  41. /**
  42. * schedule handleShow into the right thread
  43. */
  44. @Override
  45. public void show() {
  46. if (localLOGV) Log.v(TAG, "SHOW: " + this);
  47. mHandler.post(mShow);
  48. }
  49. /**
  50. * schedule handleHide into the right thread
  51. */
  52. @Override
  53. public void hide() {
  54. if (localLOGV) Log.v(TAG, "HIDE: " + this);
  55. mHandler.post(mHide);
  56. }
  57. public void handleShow() {
  58. if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
  59. + " mNextView=" + mNextView);
  60. if (mView != mNextView) {
  61. // remove the old view if necessary
  62. handleHide();
  63. mView = mNextView;
  64. Context context = mView.getContext().getApplicationContext();
  65. if (context == null) {
  66. context = mView.getContext();
  67. }
  68. mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
  69. // We can resolve the Gravity here by using the Locale for getting
  70. // the layout direction
  71. final Configuration config = mView.getContext().getResources().getConfiguration();
  72. final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
  73. mParams.gravity = gravity;
  74. if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
  75. mParams.horizontalWeight = 1.0f;
  76. }
  77. if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
  78. mParams.verticalWeight = 1.0f;
  79. }
  80. mParams.x = mX;
  81. mParams.y = mY;
  82. mParams.verticalMargin = mVerticalMargin;
  83. mParams.horizontalMargin = mHorizontalMargin;
  84. if (mView.getParent() != null) {
  85. if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
  86. mWM.removeView(mView);
  87. }
  88. if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
  89. mWM.addView(mView, mParams);
  90. trySendAccessibilityEvent();
  91. }
  92. }
  93. private void trySendAccessibilityEvent() {
  94. AccessibilityManager accessibilityManager =
  95. AccessibilityManager.getInstance(mView.getContext());
  96. if (!accessibilityManager.isEnabled()) {
  97. return;
  98. }
  99. // treat toasts as notifications since they are used to
  100. // announce a transient piece of information to the user
  101. AccessibilityEvent event = AccessibilityEvent.obtain(
  102. AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
  103. event.setClassName(getClass().getName());
  104. event.setPackageName(mView.getContext().getPackageName());
  105. mView.dispatchPopulateAccessibilityEvent(event);
  106. accessibilityManager.sendAccessibilityEvent(event);
  107. }
  108. public void handleHide() {
  109. if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
  110. if (mView != null) {
  111. // note: checking parent() just to make sure the view has
  112. // been added... i have seen cases where we get here when
  113. // the view isn't yet added, so let's try not to crash.
  114. if (mView.getParent() != null) {
  115. if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
  116. mWM.removeView(mView);
  117. }
  118. mView = null;
  119. }
  120. }
  121. }
    通过源码,我们能很明显的看到继承关系,TN类继承自ITransientNotification.Stub,用于进程间通信。这里假设读者都有Android进程间通信的基础(不太熟的建议学习罗升阳关于Binder进程通信的一系列博客)。既然TN是用于进程间通信,那么我们很容易想到TN类的具体作用应该是Toast类的回调对象,其他进程通过调用TN类的具体对象来操作Toast的显示和消失。
    TN类继承自ITransientNotification.Stub,ITransientNotification.aidl位于frameworks/base/core/java/android/app/ITransientNotification.aidl,源码如下:
  1. package android.app;
  2. /** @hide */
  3. oneway interface ITransientNotification {
  4. void show();
  5. void hide();
  6. }
    ITransientNotification定义了两个方法show()和hide(),它们的具体实现就在TN类当中。TN类的实现为:
  1. /**
  2. * schedule handleShow into the right thread
  3. */
  4. @Override
  5. public void show() {
  6. if (localLOGV) Log.v(TAG, "SHOW: " + this);
  7. mHandler.post(mShow);
  8. }
  9. /**
  10. * schedule handleHide into the right thread
  11. */
  12. @Override
  13. public void hide() {
  14. if (localLOGV) Log.v(TAG, "HIDE: " + this);
  15. mHandler.post(mHide);
  16. }
    这里我们就能知道,Toast的show和hide方法实现是基于Handler机制。而TN类中的Handler实现是:
        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。
  1. final Runnable mShow = new Runnable() {
  2. @Override
  3. public void run() {
  4. handleShow();
  5. }
  6. };
  7. final Runnable mHide = new Runnable() {
  8. @Override
  9. public void run() {
  10. handleHide();
  11. // Don't do this in handleHide() because it is also invoked by handleShow()
  12. mNextView = null;
  13. }
  14. };
    可以看到,show和hide的真正实现分别是调用了handleShow()和handleHide()方法。我们先来看handleShow()的具体实现:
  1. public void handleShow() {
  2. if (mView != mNextView) {
  3. // remove the old view if necessary
  4. handleHide();
  5. mView = mNextView;
  6. Context context = mView.getContext().getApplicationContext();
  7. if (context == null) {
  8. context = mView.getContext();
  9. }
  10. mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
  11. // We can resolve the Gravity here by using the Locale for getting
  12. // the layout direction
  13. final Configuration config = mView.getContext().getResources().getConfiguration();
  14. final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
  15. mParams.gravity = gravity;
  16. if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
  17. mParams.horizontalWeight = 1.0f;
  18. }
  19. if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
  20. mParams.verticalWeight = 1.0f;
  21. }
  22. mParams.x = mX;
  23. mParams.y = mY;
  24. mParams.verticalMargin = mVerticalMargin;
  25. mParams.horizontalMargin = mHorizontalMargin;
  26. if (mView.getParent() != null) {
  27. mWM.removeView(mView);
  28. }
  29. mWM.addView(mView, mParams);
  30. trySendAccessibilityEvent();
  31. }
  32. }
    从源码中,我们知道Toast是通过WindowManager调用addView加载进来的。因此,hide方法自然是WindowManager调用removeView方法来将Toast视图移除。
    总结一下,通过对TN类的源码分析,我们知道了TN类是回调对象,其他进程调用tn类的show和hide方法来控制这个Toast的显示和消失。

NotificationManagerService

    回到Toast类的show方法中,我们可以看到,这里调用了getService得到INotificationManager服务,源码如下:
  1. private static INotificationManager sService;
  2. static private INotificationManager getService() {
  3. if (sService != null) {
  4. return sService;
  5. }
  6. sService = INotificationManager.Stub.asInterface(ServiceManager.getService( "notification"));
  7. return sService;
  8. }
    得到INotificationManager服务后,调用了enqueueToast方法将当前的Toast放入到系统的Toast队列中。传的参数分别是pkg、tn和mDuration。也就是说,我们通过Toast.makeText(context, msg, Toast.LENGTH_SHOW).show()去呈现一个Toast,这个Toast并不是立刻显示在当前的window上,而是先进入系统的Toast队列中,然后系统调用回调对象tn的show和hide方法进行Toast的显示和隐藏。
    这里INofiticationManager接口的具体实现类是NotificationManagerService类,位于frameworks/base/services/java/com/android/server/NotificationManagerService.java。
    首先,我们来分析一下Toast入队的函数实现enqueueToast,源码如下:
  1. public void enqueueToast(String pkg, ITransientNotification callback, int duration)
  2. {
  3. // packageName为null或者tn类为null,直接返回,不进队列
  4. if (pkg == null || callback == null) {
  5. return ;
  6. }
  7. // (1) 判断是否为系统Toast
  8. final boolean isSystemToast = isCallerSystem() || ( "android".equals(pkg));
  9. // 判断当前toast所属的pkg是否为系统不允许发生Toast的pkg.NotificationManagerService有一个HashSet数据结构,存储了不允许发生Toast的包名
  10. if (ENABLE_BLOCKED_TOASTS && !noteNotificationOp(pkg, Binder.getCallingUid()) && !areNotificationsEnabledForPackageInt(pkg)) {
  11. if (!isSystemToast) {
  12. return;
  13. }
  14. }
  15. synchronized (mToastQueue) {
  16. int callingPid = Binder.getCallingPid();
  17. long callingId = Binder.clearCallingIdentity();
  18. try {
  19. ToastRecord record;
  20. // (2) 查看该Toast是否已经在队列当中
  21. int index = indexOfToastLocked(pkg, callback);
  22. // 如果Toast已经在队列中,我们只需要更新显示时间即可
  23. if (index >= 0) {
  24. record = mToastQueue.get(index);
  25. record.update(duration);
  26. } else {
  27. // 非系统Toast,每个pkg在当前mToastQueue中Toast有总数限制,不能超过MAX_PACKAGE_NOTIFICATIONS
  28. if (!isSystemToast) {
  29. int count = 0;
  30. final int N = mToastQueue.size();
  31. for ( int i= 0; i<N; i++) {
  32. final ToastRecord r = mToastQueue.get(i);
  33. if (r.pkg.equals(pkg)) {
  34. count++;
  35. if (count >= MAX_PACKAGE_NOTIFICATIONS) {
  36. Slog.e(TAG, "Package has already posted " + count
  37. + " toasts. Not showing more. Package=" + pkg);
  38. return;
  39. }
  40. }
  41. }
  42. }
  43. // 将Toast封装成ToastRecord对象,放入mToastQueue中
  44. record = new ToastRecord(callingPid, pkg, callback, duration);
  45. mToastQueue.add(record);
  46. index = mToastQueue.size() - 1;
  47. // (3) 将当前Toast所在的进程设置为前台进程
  48. keepProcessAliveLocked(callingPid);
  49. }
  50. // (4) 如果index为0,说明当前入队的Toast在队头,需要调用showNextToastLocked方法直接显示
  51. if (index == 0) {
  52. showNextToastLocked();
  53. }
  54. } finally {
  55. Binder.restoreCallingIdentity(callingId);
  56. }
  57. }
  58. }
    可以看到,我对上述代码做了简要的注释。代码相对简单,但是还有4点标注代码需要我们来进一步探讨。
    (1) 判断是否为系统Toast。如果当前Toast所属的进程的包名为“android”,则为系统Toast,否则还可以调用isCallerSystem()方法来判断。该方法的实现源码为:
  1. boolean isUidSystem(int uid) {
  2. final int appid = UserHandle.getAppId(uid);
  3. return (appid == Process.SYSTEM_UID || appid == Process.PHONE_UID || uid == 0);
  4. }
  5. boolean isCallerSystem() {
  6. return isUidSystem(Binder.getCallingUid());
  7. }
    isCallerSystem的源码也比较简单,就是判断当前Toast所属进程的uid是否为SYSTEM_UID、0、PHONE_UID中的一个,如果是,则为系统Toast;如果不是,则不为系统Toast。
    是否为系统Toast,通过下面的源码阅读可知,主要有两点优势:
  1. 系统Toast一定可以进入到系统Toast队列中,不会被黑名单阻止。
  2. 系统Toast在系统Toast队列中没有数量限制,而普通pkg所发送的Toast在系统Toast队列中有数量限制。
    (2) 查看将要入队的Toast是否已经在系统Toast队列中。这是通过比对pkg和callback来实现的,具体源码如下所示:
  1. private int indexOfToastLocked(String pkg, ITransientNotification callback)
  2. {
  3. IBinder cbak = callback.asBinder();
  4. ArrayList<ToastRecord> list = mToastQueue;
  5. int len = list.size();
  6. for ( int i= 0; i<len; i++) {
  7. ToastRecord r = list.get(i);
  8. if (r.pkg.equals(pkg) && r.callback.asBinder() == cbak) {
  9. return i;
  10. }
  11. }
  12. return - 1;
  13. }
    通过上述代码,我们可以得出一个结论,只要Toast的pkg名称和tn对象是一致的,则系统把这些Toast认为是同一个Toast。
    (3) 将当前Toast所在进程设置为前台进程。源码如下所示:
  1. private void keepProcessAliveLocked(int pid)
  2. {
  3. int toastCount = 0; // toasts from this pid
  4. ArrayList<ToastRecord> list = mToastQueue;
  5. int N = list.size();
  6. for ( int i= 0; i<N; i++) {
  7. ToastRecord r = list.get(i);
  8. if (r.pid == pid) {
  9. toastCount++;
  10. }
  11. }
  12. try {
  13. mAm.setProcessForeground(mForegroundToken, pid, toastCount > 0);
  14. } catch (RemoteException e) {
  15. // Shouldn't happen.
  16. }
  17. }
    这里的mAm=ActivityManagerNative.getDefault(),调用了setProcessForeground方法将当前pid的进程置为前台进程,保证不会系统杀死。这也就解释了为什么当我们finish当前Activity时,Toast还可以显示,因为当前进程还在执行。
    (4) index为0时,对队列头的Toast进行显示。源码如下:
  1. private void showNextToastLocked() {
  2. // 获取队列头的ToastRecord
  3. ToastRecord record = mToastQueue.get( 0);
  4. while (record != null) {
  5. try {
  6. // 调用Toast的回调对象中的show方法对Toast进行展示
  7. record.callback.show();
  8. scheduleTimeoutLocked(record);
  9. return;
  10. } catch (RemoteException e) {
  11. Slog.w(TAG, "Object died trying to show notification " + record.callback
  12. + " in package " + record.pkg);
  13. // remove it from the list and let the process die
  14. int index = mToastQueue.indexOf(record);
  15. if (index >= 0) {
  16. mToastQueue.remove(index);
  17. }
  18. keepProcessAliveLocked(record.pid);
  19. if (mToastQueue.size() > 0) {
  20. record = mToastQueue.get( 0);
  21. } else {
  22. record = null;
  23. }
  24. }
  25. }
  26. }
    这里Toast的回调对象callback就是tn对象。接下来,我们看一下,为什么系统Toast的显示时间只能是2s或者3.5s,关键在于scheduleTimeoutLocked方法的实现。原理是,调用tn的show方法展示完Toast之后,需要调用scheduleTimeoutLocked方法来将Toast消失。( 如果大家有疑问:不是说tn对象的hide方法来将Toast消失,为什么要在这里调用scheduleTimeoutLocked方法将Toast消失呢?是因为tn类的hide方法一执行,Toast立刻就消失了,而平时我们所使用的Toast都会在当前Activity停留几秒。如何实现停留几秒呢?原理就是scheduleTimeoutLocked发送MESSAGE_TIMEOUT消息去调用tn对象的hide方法,但是这个消息会有一个delay延迟,这里也是用了Handler消息机制)。
  1. private static final int LONG_DELAY = 3500; // 3.5 seconds
  2. private static final int SHORT_DELAY = 2000; // 2 seconds
  3. private void scheduleTimeoutLocked(ToastRecord r)
  4. {
  5. mHandler.removeCallbacksAndMessages(r);
  6. Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
  7. long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
  8. mHandler.sendMessageDelayed(m, delay);
  9. }
    首先,我们看到这里并不是直接发送了MESSAGE_TIMEOUT消息,而是有个delay的延迟。 而delay的时间从代码中“long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;”看出只能为2s或者3.5s,这也就解释了为什么系统Toast的呈现时间只能是2s或者3.5s。自己在Toast.makeText方法中随意传入一个duration是无作用的。
    接下来,我们来看一下WorkerHandler中是如何处理MESSAGE_TIMEOUT消息的。mHandler对象的类型为WorkerHandler,源码如下:
  1. private final class WorkerHandler extends Handler
  2. {
  3. @Override
  4. public void handleMessage(Message msg)
  5. {
  6. switch (msg.what)
  7. {
  8. case MESSAGE_TIMEOUT:
  9. handleTimeout((ToastRecord)msg.obj);
  10. break;
  11. }
  12. }
  13. }
    可以看到,WorkerHandler对MESSAGE_TIMEOUT类型的消息处理是调用了handlerTimeout方法,那我们继续跟踪handleTimeout源码:
  1. private void handleTimeout(ToastRecord record)
  2. {
  3. synchronized (mToastQueue) {
  4. int index = indexOfToastLocked(record.pkg, record.callback);
  5. if (index >= 0) {
  6. cancelToastLocked(index);
  7. }
  8. }
  9. }
    handleTimeout代码中,首先判断当前需要消失的Toast所属ToastRecord对象是否在队列中,如果在队列中,则调用cancelToastLocked(index)方法。真相就要浮现在我们眼前了,继续跟踪源码:
  1. private void cancelToastLocked(int index) {
  2. ToastRecord record = mToastQueue.get(index);
  3. try {
  4. record.callback.hide();
  5. } catch (RemoteException e) {
  6. // don't worry about this, we're about to remove it from
  7. // the list anyway
  8. }
  9. mToastQueue.remove(index);
  10. keepProcessAliveLocked(record.pid);
  11. if (mToastQueue.size() > 0) {
  12. // Show the next one. If the callback fails, this will remove
  13. // it from the list, so don't assume that the list hasn't changed
  14. // after this point.
  15. showNextToastLocked();
  16. }
  17. }
    哈哈,看到这里,我们回调对象的hide方法也被调用了,同时也将该ToastRecord对象从mToastQueue中移除了。到这里,一个Toast的完整显示和消失就讲解结束了。

原地址:https://blog.csdn.net/wzy_1988/article/details/43341761

如有侵权,请告之,速删!!!


猜你喜欢

转载自blog.csdn.net/callmezhe/article/details/80939644