Android开发经验谈Android技术知识Android开发

Toast 实现原理解析

2018-04-22  本文已影响123人  Calllanna

关于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()取消显示。

上一篇下一篇

猜你喜欢

热点阅读