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()取消显示。
推荐阅读
- 做一件事情的基本原理是什么()
- 关于QueryWrapper|关于QueryWrapper,实现MybatisPlus多表关联查询方式
- MybatisPlus使用queryWrapper如何实现复杂查询
- python学习之|python学习之 实现QQ自动发送消息
- 孩子不是实现父母欲望的工具——林哈夫
- opencv|opencv C++模板匹配的简单实现
- Node.js中readline模块实现终端输入
- java中如何实现重建二叉树
- 【读书笔记】贝叶斯原理
- 人脸识别|【人脸识别系列】| 实现自动化妆