Toast|Toast 实现原理解析

关于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,

并设置要显示的文字信息。实例化的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 1273public void enqueueToast(String pkg, ITransientNotification callback, int duration) 1274{ 1275if (DBG) { 1276Slog.i(TAG, "enqueueToast pkg=" + pkg + " callback=" + callback 1277+ " duration=" + duration); 1278} 1279 1280if (pkg == null || callback == null) { 1281Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback); 1282return ; 1283} 1284 1285final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg)); 1286final boolean isPackageSuspended = 1287isPackageSuspendedForUser(pkg, Binder.getCallingUid()); 1288 1289if (ENABLE_BLOCKED_TOASTS && (!noteNotificationOp(pkg, Binder.getCallingUid()) 1290|| isPackageSuspended)) { 1291if (!isSystemToast) { 1292Slog.e(TAG, "Suppressing toast from package " + pkg 1293+ (isPackageSuspended 1294? " due to package suspended by administrator." 1295: " by user request.")); 1296return; 1297} 1298} 1299 1300synchronized (mToastQueue) { 1301int callingPid = Binder.getCallingPid(); 1302long callingId = Binder.clearCallingIdentity(); 1303try { 1304ToastRecord record; 1305int 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. 1308if (index >= 0) { 1309record = mToastQueue.get(index); 1310record.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. 1314if (!isSystemToast) { 1315int count = 0; 1316final int N = mToastQueue.size(); 1317for (int i=0; i= MAX_PACKAGE_NOTIFICATIONS) { 1322Slog.e(TAG, "Package has already posted " + count 1323+ " toasts. Not showing more. Package=" + pkg); 1324return; 1325} 1326} 1327} 1328} 1329 1330Binder token = new Binder(); 1331mWindowManagerInternal.addWindowToken(token, 1332WindowManager.LayoutParams.TYPE_TOAST); 1333record = new ToastRecord(callingPid, pkg, callback, duration, token); 1334mToastQueue.add(record); 1335index = mToastQueue.size() - 1; 1336keepProcessAliveIfNeededLocked(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. 1342if (index == 0) { 1343showNextToastLocked(); 1344} 1345} finally { 1346Binder.restoreCallingIdentity(callingId); 1347} 1348} 1349} 1350 1351@Override 1352public void cancelToast(String pkg, ITransientNotification callback) { 1353 1360synchronized (mToastQueue) { 1361long callingId = Binder.clearCallingIdentity(); 1362try { 1363int index = indexOfToastLocked(pkg, callback); 1364if (index >= 0) { 1365cancelToastLocked(index); 1366} else { 1367Slog.w(TAG, "Toast already cancelled. pkg=" + pkg 1368+ " callback=" + callback); 1369} 1370} finally { 1371Binder.restoreCallingIdentity(callingId); 1372} 1373} 1374} ........ 1375}

INotificationManager.Stub() 实现 enqueueToast()通过 showNextToastLocked(),cancelToast()通过 cancelToastLocked(index)方法来回调ITransientNotification的show(),hide()。
void showNextToastLocked() { 2995ToastRecord record = mToastQueue.get(0); 2996while (record != null) { 2997if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback); 2998try { 2999record.callback.show(record.token); 3000scheduleTimeoutLocked(record); 3001return; 3002} catch (RemoteException e) { 3003Slog.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 3006int index = mToastQueue.indexOf(record); 3007if (index >= 0) { 3008mToastQueue.remove(index); 3009} 3010keepProcessAliveIfNeededLocked(record.pid); 3011if (mToastQueue.size() > 0) { 3012record = mToastQueue.get(0); 3013} else { 3014record = null; 3015} 3016} 3017} 3018} 3019 3020void cancelToastLocked(int index) { 3021ToastRecord record = mToastQueue.get(index); 3022try { 3023record.callback.hide(); 3024} catch (RemoteException e) { 3025Slog.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 3031ToastRecord lastToast = mToastQueue.remove(index); 3032mWindowManagerInternal.removeWindowToken(lastToast.token, true); 3033 3034keepProcessAliveIfNeededLocked(record.pid); 3035if (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. 3039showNextToastLocked(); 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();

【Toast|Toast 实现原理解析】4.应用在后台工作以后,要记得Toast.cancel()取消显示。

    推荐阅读