Toast 实现原理解析

作者: Calllanna | 来源:发表于2018-04-22 23:50 被阅读123次

    关于Toast我们开发中最常用,但是他的实现原理往往被忽略,大概知道是通过WindowManager直接加载显示的。
    但是,不知道读者是否思考过以下问题:
    1.为什么同一个应用不同线程,调用Toast.show()的时候,是有序显示.
    2.不同应用之间Toast调用show()的时候,为什么不冲突,不会覆盖显示,而且同样也是有序的。
    3.怎样实现非UI线程调用Toast.show().而不产生崩溃。
    4.退出应用的时候,Toast.show()还在显示,如何做到退出应用后,不显示Toast

    Toast是用来提示用户信息一个view,这个View显示在Window上,通过WindowManager直接加载,而依赖于应用中的任何View上。
    首先前两个问题,要分析Toast的实现原理。
    当我们这样显示一个Toast:
    Toast.makeText(MainActivity.this,"今天天气很好哦!" ,Toast.LENGTH_LONG).show();
    首先makeText(),实例化一个Toast。并inflate布局transient_notification,使得

    public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
            @NonNull CharSequence text, @Duration int duration) {
        Toast result = new Toast(context, looper);
    
        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;
    }
    

    首先makeText(),实例化一个Toast。并inflate布局transient_notification,

    <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>
    

    并设置要显示的文字信息。实例化的Toast,实际上实例化静态对象TN。

    public Toast(@NonNull Context context, @Nullable Looper looper) {
      mContext = context;
      mTN = new TN(context.getPackageName(), looper);
    ......
    }
    TN类继承自ITransientNotification.Stub,如下是TN的源码:
    private static class TN extends ITransientNotification.Stub {
      private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
    
      private static final int SHOW = 0;
      private static final int HIDE = 1;
      private static final int CANCEL = 2;
      final Handler mHandler;
    
      int mGravity;
      int mX, mY;
      float mHorizontalMargin;
      float mVerticalMargin;
    
    
      View mView;
      View mNextView;
      int mDuration;
    
      WindowManager mWM;
    
      String mPackageName;
    
      static final long SHORT_DURATION_TIMEOUT = 4000;
      static final long LONG_DURATION_TIMEOUT = 7000;
    
      TN(String packageName, @Nullable Looper looper) {
          // 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;
    
          mPackageName = packageName;
    
          if (looper == null) {
              // Use Looper.myLooper() if looper is not specified.
              looper = Looper.myLooper();
              if (looper == null) {
                  throw new RuntimeException(
                          "Can't toast on a thread that has not called Looper.prepare()");
              }
          }
          mHandler = new Handler(looper, null) {
              @Override
              public void handleMessage(Message msg) {
                  switch (msg.what) {
                      case SHOW: {
                          IBinder token = (IBinder) msg.obj;
                          handleShow(token);
                          break;
                      }
                      case HIDE: {
                          handleHide();
                          // Don't do this in handleHide() because it is also invoked by
                          // handleShow()
                          mNextView = null;
                          break;
                      }
                      case CANCEL: {
                          handleHide();
                          // Don't do this in handleHide() because it is also invoked by
                          // handleShow()
                          mNextView = null;
                          try {
                              getService().cancelToast(mPackageName, TN.this);
                          } catch (RemoteException e) {
                          }
                          break;
                      }
                  }
              }
          };
      }
    
      /**
       * schedule handleShow into the right thread
       */
      @Override
      public void show(IBinder windowToken) {
          if (localLOGV) Log.v(TAG, "SHOW: " + this);
          mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
      }
    
      /**
       * schedule handleHide into the right thread
       */
      @Override
      public void hide() {
          if (localLOGV) Log.v(TAG, "HIDE: " + this);
          mHandler.obtainMessage(HIDE).sendToTarget();
      }
    
      public void cancel() {
          if (localLOGV) Log.v(TAG, "CANCEL: " + this);
          mHandler.obtainMessage(CANCEL).sendToTarget();
      }
    
      public void handleShow(IBinder windowToken) {
          if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                  + " mNextView=" + mNextView);
          // If a cancel/hide is pending - no need to show - at this point
          // the window token is already invalid and no need to do any work.
          if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
              return;
          }
          if (mView != mNextView) {
              // remove the old view if necessary
              handleHide();
              mView = mNextView;
              Context context = mView.getContext().getApplicationContext();
              String packageName = mView.getContext().getOpPackageName();
              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;
              mParams.packageName = packageName;
              mParams.hideTimeoutMilliseconds = mDuration ==
                  Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
              mParams.token = windowToken;
              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);
              // Since the notification manager service cancels the token right
              // after it notifies us to cancel the toast there is an inherent
              // race and we may attempt to add a window after the token has been
              // invalidated. Let us hedge against that.
              try {
                  mWM.addView(mView, mParams);
                  trySendAccessibilityEvent();
              } catch (WindowManager.BadTokenException e) {
                  /* ignore */
              }
          }
      }
    .......
    }
    ITransientNotification是AIDL进程间通讯的接口,ITransientNotification.aidl位于frameworks/base/core/java/android/app/ITransientNotification.aidl,源码如下:
    在线源码:
    [ITransientNotification](http://androidxref.com/7.1.2_r36/xref/frameworks/base/core/java/android/app/ITransientNotification.aidl)
    [java] view plain copy
    package android.app;  
    
    /** @hide */  
    oneway interface ITransientNotification {  
      void show();  
      void hide();  
    }  
    当我们调用show()的时候,通过INotificationManager将消息加入队列中。
    /**
    * Show the view for the specified duration.
    */
    public void show() {
      if (mNextView == null) {
          throw new RuntimeException("setView must have been called");
      }
    
      INotificationManager service = getService();
      String pkg = mContext.getOpPackageName();
      TN tn = mTN;
      tn.mNextView = mNextView;
    
      try {
          service.enqueueToast(pkg, tn, mDuration);
      } catch (RemoteException e) {
          // Empty
      }
    }
    
    static private INotificationManager getService() {
      if (sService != null) {
          return sService;
      }
      sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
      return sService;
    }
    INotificationManager是 INotificationManager.aidl接口的实现。源码:[INotificationManager.aidl](http://androidxref.com/7.1.2_r36/xref/frameworks/base/core/java/android/app/INotificationManager.aidl)
    
    NotificationManagerService服务开启后,就会实例化一个Binder:
    private final IBinder mService = new INotificationManager.Stub() {
    1269        // Toasts
    1270        // ============================================================================
    1271
    1272        @Override
    1273        public void enqueueToast(String pkg, ITransientNotification callback, int duration)
    1274        {
    1275            if (DBG) {
    1276                Slog.i(TAG, "enqueueToast pkg=" + pkg + " callback=" + callback
    1277                        + " duration=" + duration);
    1278            }
    1279
    1280            if (pkg == null || callback == null) {
    1281                Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
    1282                return ;
    1283            }
    1284
    1285            final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));
    1286            final boolean isPackageSuspended =
    1287                    isPackageSuspendedForUser(pkg, Binder.getCallingUid());
    1288
    1289            if (ENABLE_BLOCKED_TOASTS && (!noteNotificationOp(pkg, Binder.getCallingUid())
    1290                    || isPackageSuspended)) {
    1291                if (!isSystemToast) {
    1292                    Slog.e(TAG, "Suppressing toast from package " + pkg
    1293                            + (isPackageSuspended
    1294                                    ? " due to package suspended by administrator."
    1295                                    : " by user request."));
    1296                    return;
    1297                }
    1298            }
    1299
    1300            synchronized (mToastQueue) {
    1301                int callingPid = Binder.getCallingPid();
    1302                long callingId = Binder.clearCallingIdentity();
    1303                try {
    1304                    ToastRecord record;
    1305                    int index = indexOfToastLocked(pkg, callback);
    1306                    // If it's already in the queue, we update it in place, we don't
    1307                    // move it to the end of the queue.
    1308                    if (index >= 0) {
    1309                        record = mToastQueue.get(index);
    1310                        record.update(duration);
    1311                    } else {
    1312                        // Limit the number of toasts that any given package except the android
    1313                        // package can enqueue.  Prevents DOS attacks and deals with leaks.
    1314                        if (!isSystemToast) {
    1315                            int count = 0;
    1316                            final int N = mToastQueue.size();
    1317                            for (int i=0; i<N; i++) {
    1318                                 final ToastRecord r = mToastQueue.get(i);
    1319                                 if (r.pkg.equals(pkg)) {
    1320                                     count++;
    1321                                     if (count >= MAX_PACKAGE_NOTIFICATIONS) {
    1322                                         Slog.e(TAG, "Package has already posted " + count
    1323                                                + " toasts. Not showing more. Package=" + pkg);
    1324                                         return;
    1325                                     }
    1326                                 }
    1327                            }
    1328                        }
    1329
    1330                        Binder token = new Binder();
    1331                        mWindowManagerInternal.addWindowToken(token,
    1332                                WindowManager.LayoutParams.TYPE_TOAST);
    1333                        record = new ToastRecord(callingPid, pkg, callback, duration, token);
    1334                        mToastQueue.add(record);
    1335                        index = mToastQueue.size() - 1;
    1336                        keepProcessAliveIfNeededLocked(callingPid);
    1337                    }
    1338                    // If it's at index 0, it's the current toast.  It doesn't matter if it's
    1339                    // new or just been updated.  Call back and tell it to show itself.
    1340                    // If the callback fails, this will remove it from the list, so don't
    1341                    // assume that it's valid after this.
    1342                    if (index == 0) {
    1343                        showNextToastLocked();
    1344                    }
    1345                } finally {
    1346                    Binder.restoreCallingIdentity(callingId);
    1347                }
    1348            }
    1349        }
    1350
    1351        @Override
    1352        public void cancelToast(String pkg, ITransientNotification callback) {
    1353           
    1360            synchronized (mToastQueue) {
    1361                long callingId = Binder.clearCallingIdentity();
    1362                try {
    1363                    int index = indexOfToastLocked(pkg, callback);
    1364                    if (index >= 0) {
    1365                        cancelToastLocked(index);
    1366                    } else {
    1367                        Slog.w(TAG, "Toast already cancelled. pkg=" + pkg
    1368                                + " callback=" + callback);
    1369                    }
    1370                } finally {
    1371                    Binder.restoreCallingIdentity(callingId);
    1372                }
    1373            }
    1374        }
                 ........
    1375}
    

    INotificationManager.Stub() 实现 enqueueToast()通过 showNextToastLocked(),cancelToast()通过 cancelToastLocked(index)方法来回调ITransientNotification的show(),hide()。

    void showNextToastLocked() {
    2995        ToastRecord record = mToastQueue.get(0);
    2996        while (record != null) {
    2997            if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
    2998            try {
    2999                record.callback.show(record.token);
    3000                scheduleTimeoutLocked(record);
    3001                return;
    3002            } catch (RemoteException e) {
    3003                Slog.w(TAG, "Object died trying to show notification " + record.callback
    3004                        + " in package " + record.pkg);
    3005                // remove it from the list and let the process die
    3006                int index = mToastQueue.indexOf(record);
    3007                if (index >= 0) {
    3008                    mToastQueue.remove(index);
    3009                }
    3010                keepProcessAliveIfNeededLocked(record.pid);
    3011                if (mToastQueue.size() > 0) {
    3012                    record = mToastQueue.get(0);
    3013                } else {
    3014                    record = null;
    3015                }
    3016            }
    3017        }
    3018    }
    3019
    3020    void cancelToastLocked(int index) {
    3021        ToastRecord record = mToastQueue.get(index);
    3022        try {
    3023            record.callback.hide();
    3024        } catch (RemoteException e) {
    3025            Slog.w(TAG, "Object died trying to hide notification " + record.callback
    3026                    + " in package " + record.pkg);
    3027            // don't worry about this, we're about to remove it from
    3028            // the list anyway
    3029        }
    3030
    3031        ToastRecord lastToast = mToastQueue.remove(index);
    3032        mWindowManagerInternal.removeWindowToken(lastToast.token, true);
    3033
    3034        keepProcessAliveIfNeededLocked(record.pid);
    3035        if (mToastQueue.size() > 0) {
    3036            // Show the next one. If the callback fails, this will remove
    3037            // it from the list, so don't assume that the list hasn't changed
    3038            // after this point.
    3039            showNextToastLocked();
    3040        }
    3041    }
    

    TN是ITransientNotification的子类,通过自身的Handler将消息处理,handshow() 中mWM.addView(mView, mParams)添加。

    总结:

    1.Toast.show(),Toast.cancel()是通过跨进程通讯(IPC通讯机制)实现的,全局一个系统服务NotificationManagerService管理Toast消息队列。所以异步线程,跨进程调用都是有序,不会覆盖的。
    2.尽管每次实例化一个TN,每个线程下的Handler持有的Looper相同线程是一样的,处理各自的消息队列里的SHOW,HIDE消息。
    3.要实现非主线程调用不要忘记Looper.prepare()实例化looper:

    new Thread(){
        @Override
        public void run() {
            super.run();
            Looper.prepare();
            Toast.makeText(MainActivity.this,"今天天气很好哦!" + (++indexToast),Toast.LENGTH_LONG).show();
            Looper.loop();
        }
    }.start();
    

    4.应用在后台工作以后,要记得Toast.cancel()取消显示。

    相关文章

      网友评论

        本文标题:Toast 实现原理解析

        本文链接:https://www.haomeiwen.com/subject/yynhlftx.html