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