android源码学习-Toast实现原理讲解
前言:
前些日志QQ群有朋友发了一个Toast的崩溃日志。Toast如此简单的用法怎么会崩溃呢?所以顺便就学习了一下Toast在源码中的实现,不算复杂,但内容挺多的,这里就来分享一下,方便读者。
一.基本使用方式
主要有两种实现方式:
1.最基本的使用方式:
使用方式很简单,直接沟通过静态方法构传入context,显示内容以及显示时长三个参数,构造Toast对象,然后通过show显示。
Toast toast = Toast.makeText(getBaseContext(), "显示内容", Toast.LENGTH_LONG);toast.show();
2.自定义View的实现方式
这种使用使用方式也很简单,首先构造一个View,然后通过setView方法传入这个自定义View,最终也是通过show方法显示。
View selfToastView = View.inflate(getBaseContext(), R.layout.self_toast, null);
Toast toast = Toast.makeText(getBaseContext(), "显示内容", Toast.LENGTH_LONG);
toast.setView(selfToastView);
toast.show();
3.使用方式总结
两种使用方式都很简单,区别只是第二种方式多传入了一个自定义View而已。但是为什么要分开来讲呢?因为虽然使用时仅仅只差一步,但是其实现原理是完全不一样的。一个是通过NotificationManagerService去显示的,而另外一个则是APP自身处理的。接下来,我们就依次的讲一下两种使用方式的实现原理。
二.Toast的创建显示流程原理讲解
1.Toast.makeText
这个的实现方式还是比较简单的,最终的生成方式传入4个参数,分别为
Context:绑定的上下文对象
Looper:绑定线程的Looper,可以为空。为空时则默认使用当前线程的looper。PS:每个线程都只能绑定唯一的一个Looper,想了解这一块的可以看我的另外一篇文章:android源码学习-Handler机制及其六个核心点_失落夏天的博客-CSDN博客_安卓开发handler机制
text:显示内容
duration:持续时间。有两种参数:
Toast.LENGTH_LONG:显示时间较长,为3.5S。其3500ms的值定义在NotificationManagerService.LONG_DELAY。
Toast.LENGTH_SHORT:显示时间较短,为2S。其2000ms的值定义在NotificationManagerService.SHORT_DELAY。
但是真实显示的时间,却不是3.5和2S,实际显示时间会比这两个时间更长一些,这个后面会讲。
最终生成一个Toast对象返回,这里需要注意的是,原生Toast和自定义View的Toast的唯一区别就是原生Toast对象中mNextView对象为null。
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,@NonNull CharSequence text, @Duration int duration) {//这里默认配置为true,走上面这个判断逻辑if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {Toast result = new Toast(context, looper);result.mText = text;result.mDuration = duration;return result;} else {Toast result = new Toast(context, looper);View v = ToastPresenter.getTextToastView(context, text);result.mNextView = v;result.mDuration = duration;return result;}}
然后Toast的构造方法如下,主要是构建几个后门需要使用到的对象:
public Toast(@NonNull Context context, @Nullable Looper looper) {mContext = context;mToken = new Binder();looper = getLooper(looper);mHandler = new Handler(looper);mCallbacks = new ArrayList<>();mTN = new TN(context, context.getPackageName(), mToken,mCallbacks, looper);mTN.mY = context.getResources().getDimensionPixelSize(com.android.internal.R.dimen.toast_y_offset);mTN.mGravity = context.getResources().getInteger(com.android.internal.R.integer.config_toastDefaultGravity);}
mContext:Context对象
mToken:构造binder对象,后面和NotificationManagerService通信都是通过这个binder。
looper:当前Toast绑定的线程looper,传入null时默认为当前线程。
mTN:Binder.Stub类型对象,作为binder的client端。其接受跨进程传递过来的信息时是在单独的binder线程中处理的。
mTN.mY:纵坐标偏移量,简单来说就是控制Toast在屏幕中显示位置是靠上一点还是靠下一点的。
mTN.mGravity:控制Toast的显示位置。一般是局中,靠下两种。
2.Toast.show()方法
show方法中主要是执行三段逻辑,
首先把Toast的mNextView赋值给tn.mNextView,如果Toast的mNextView为null,那么tn.mNextView自然也是null;
然后获取NotificationManager的binder引用service;
接下来走一个判断逻辑,
1.如果mNextView==null时,走service.enqueueToast逻辑,通过binder跨进程通讯,会调用到NotificationManagerService中的enqueueToast方法,这个我们会在第三章讲解。
2.如果mNextView!=null时,通过调用service.enqueueTextToast方法,通过binder跨进程通讯,会调用到NotificationManagerService中的enqueueTextToast方法,这个我们会在第四章讲解。
public void show() {...INotificationManager service = getService();String pkg = mContext.getOpPackageName();TN tn = mTN;tn.mNextView = mNextView;final int displayId = mContext.getDisplayId();...if (mNextView != null) {// It's a custom toast//自定义的方式第四章讲解service.enqueueToast(pkg, mToken, tn, mDuration, displayId);} else {// It's a text toast//默认方式第三章讲解ITransientNotificationCallback callback =new CallbackBinder(mCallbacks, mHandler);service.enqueueTextToast(pkg, mToken, mText, mDuration, displayId, callback);}} ...}
三.Toast显示的完整流程
3.1 Service中接收
上文讲到通过binder传输,此时NotificationManagerService中的mService对象中的enqueueTextToast()方法会接收到通知,具体参数解释如下:
/**** @param pkg 包名* @param token APP端binder* @param text 显示内容* @param duration 持续时间* @param displayId 标记唯一显示区域的ID,对应的实体类是DisplayContent* @param callback 跨进程的callBack对象,自定义View的Toast有值。默认的Toast方法为null*/@Overridepublic void enqueueTextToast(String pkg, IBinder token, CharSequence text, int duration,int displayId, @Nullable ITransientNotificationCallback callback) {enqueueToast(pkg, token, text, null, duration, displayId, callback);}
这个方法会传递到enqueueToast方法(这里稍微扩展一下,其实自定义View的Toast也会走到这个方法)。
3.2 enqueueToast方法中处理队列逻辑
我们都知道,Toast显示是有时序的,先调用的Toast一定会先展示,所以这就需要一个集合来维护这个先后的关系,而这个集合就是mToastQueue。
final ArrayList<ToastRecord> mToastQueue = new ArrayList<>();
上一小节的流程进入enqueueToast方法后,其实主要分为两块逻辑,核心代码如下:
private void enqueueToast(String pkg, IBinder token, @Nullable CharSequence text,@Nullable ITransientNotification callback, int duration, int displayId,@Nullable ITransientNotificationCallback textCallback) {...//上面的内容都是做参数合法性检查final int callingUid = Binder.getCallingUid();...//此方法做权限检查if (!checkCanEnqueueToast(pkg, callingUid, isAppRenderedToast, isSystemToast)) {return;}synchronized (mToastQueue) {int callingPid = Binder.getCallingPid();final long callingId = Binder.clearCallingIdentity();try {ToastRecord record;int index = indexOfToastLocked(pkg, token);// If it's already in the queue, we update it in place, we don't// move it to the end of the queue.if (index >= 0) {record = mToastQueue.get(index);record.update(duration);} else {//插入逻辑...}...if (index == 0) {showNextToastLocked(false);}} ...}}
这个方法中,首先我们看到了加锁的代码:synchronized (mToastQueue),所以说明这是一个多线程的场景。binder机制中,作为server端会有一个线程池来处理client发过来的binder请求,每个请求都会分配一个线程去处理,所以这里才会有多线程的加锁逻辑。
方法中如下逻辑分为以下两块:
1.首先做参数合法性检查以及权限检查,
2.然后进入队列逻辑。
队列逻辑中,首先根据pkg和token通过indexOfToastLocked方法判断在集合中是否存在。
int index = indexOfToastLocked(pkg, token);
如果index>=0,则说明mToastQueue中已经存在了传入APP所对应的binder对象,则直接更新所对应的record的持续时间。
indexOfToastLocked方法如下:
int indexOfToastLocked(String pkg, IBinder token) {ArrayList<ToastRecord> list = mToastQueue;int len = list.size();for (int i=0; i<len; i++) {ToastRecord r = list.get(i);if (r.pkg.equals(pkg) && r.token == token) {return i;}}return -1;}
是通过循环便利的方式来进行判断的,效率略微有些低,这里略微吐槽一下源码,也许使用TreeMap会是一个更好的选择(key=pkg+token.hashcode)。当然,google也是是考虑到Toast排队的场景较少,所才选择使用ArrayList。
由于每个Toast都对应一个binder对象,所以如果toast是复用的,则短时间内多次调用show放,也只会对应同一个Record对象,所以也只会显示一次。
如果index<0,则说明mToastQueue不存在该toast所对应的binder,则进入插入的逻辑。
3.3 插入逻辑
插入逻辑的代码如下
} else {// Limit the number of toasts that any given package can enqueue.// Prevents DOS attacks and deals with leaks.int count = 0;final int N = mToastQueue.size();for (int i = 0; i < N; i++) {final ToastRecord r = mToastQueue.get(i);if (r.pkg.equals(pkg)) {count++;if (count >= MAX_PACKAGE_TOASTS) {Slog.e(TAG, "Package has already queued " + count+ " toasts. Not showing more. Package=" + pkg);return;}}}Binder windowToken = new Binder();mWindowManagerInternal.addWindowToken(windowToken, TYPE_TOAST, displayId,null /* options */);record = getToastRecord(callingUid, callingPid, pkg, isSystemToast, token,text, callback, duration, windowToken, displayId, textCallback);mToastQueue.add(record);index = mToastQueue.size() - 1;keepProcessAliveForToastIfNeededLocked(callingPid);}// If it's at index 0, it's the current toast. It doesn't matter if it's// new or just been updated, show it.// If the callback fails, this will remove it from the list, so don't// assume that it's valid after this.if (index == 0) {showNextToastLocked(false);}
首先判断同一个包名下是否已经存在了5条(含)以上的未显示Toast,如果有则不允许继续添加。
否则,通过getToastRecord方法生成一个ToastRecord对象加入到集合最尾端,并且通过keepProcessAliveForToastIfNeededLocked方法保证弹Toast的进程不被杀死,如果当前只有一条记录的话,则直接调用showNextToastLocke方法进行显示。
ToastRecord其实是一个抽象方法,它有两个实现类,TextToastRecord和CustomToastRecord。getToastRecord方法中会根据callback是否为空来进行对应的生成,其中callback==null时生成的是TextToastRecord类型对象。
3.4 生产者消费者模型
这里又涉及到生产者消费者模式了,既然APP端通过binder方法向mToastQueue集合中插入数据,那么就一定有消费者来消费。而这个消费者就是showNextToastLocked方法。
由于上面所说的加锁逻辑,所以永远只会有一个线程在执行showNextToastLocked方法。
方法如下:
void showNextToastLocked(boolean lastToastWasTextRecord) {if (mIsCurrentToastShown) {return; // Don't show the same toast twice.}ToastRecord record = mToastQueue.get(0);while (record != null) {int userId = UserHandle.getUserId(record.uid);boolean rateLimitingEnabled =!mToastRateLimitingDisabledUids.contains(record.uid);boolean isWithinQuota =mToastRateLimiter.isWithinQuota(userId, record.pkg, TOAST_QUOTA_TAG)|| isExemptFromRateLimiting(record.pkg, userId);boolean isPackageInForeground = isPackageInForegroundForToast(record.uid);if (tryShowToast(record, rateLimitingEnabled, isWithinQuota, isPackageInForeground)) {scheduleDurationReachedLocked(record, lastToastWasTextRecord);mIsCurrentToastShown = true;if (rateLimitingEnabled && !isPackageInForeground) {mToastRateLimiter.noteEvent(userId, record.pkg, TOAST_QUOTA_TAG);}return;}int index = mToastQueue.indexOf(record);if (index >= 0) {mToastQueue.remove(index);}record = (mToastQueue.size() > 0) ? mToastQueue.get(0) : null;}}
代码虽较长,但核心逻辑只有三块:
1.按照先后顺序便利mToastQueue集合,取出record对象。
2.通过tryShowToast方法尝试显示record对象。如果成功,则执行scheduleDurationReachedLocked方法。
3.如果失败,则从集合中删除。就是说如果Toast显示时如果失败了也不会再次尝试。
tryShowToast的逻辑我们下一小节会讲,这里看一下scheduleDurationReachedLocked的实现:
private void scheduleDurationReachedLocked(ToastRecord r, boolean lastToastWasTextRecord){mHandler.removeCallbacksAndMessages(r);Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r);int delay = r.getDuration() == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;//通过无障碍辅助功能修正这个delay值,如果开始无障碍辅助的话,事件会比正常值要长一些delay = mAccessibilityManager.getRecommendedTimeoutMillis(delay,AccessibilityManager.FLAG_CONTENT_TEXT);//如果上一个Toast还在显示,则流出来上一个Toast的离场动画事件。if (lastToastWasTextRecord) {delay += 250; // delay to account for previous toast's "out" animation}//如果是TextToastRecord类型,则流出来动画进场时间。if (r instanceof TextToastRecord) {delay += 333; // delay to account for this toast's "in" animation}mHandler.sendMessageDelayed(m, delay);}
首先从Looper中的mQueue中删除带当前TaskRecord对象的Message,
然后从对象池中重新生成一个带TaskRecord对象的Message,加入到延时任务中。延时时间恰好就是duration中设置的2S或者3.5S。
另外还要流出来进场和出场的动画时间,所以最终的延时时间会比2S或者3.5S要长。这一块的逻辑其实就是删除Toast的,所以,这里的延时时间变长了,就会导致最终的实际时间要比要比设置值更长一些。
handler在时间到了之后,会执行MESSAGE_DURATION_REACHED类型的事件,调用handleDurationReached方法,该方法中又回调用cancelToastLocked方法:
void cancelToastLocked(int index) {//1.回调APP层TN的hide方法进行通知;ToastRecord record = mToastQueue.get(index);record.hide();if (index == 0) {mIsCurrentToastShown = false;}//2.对队列中删除ToastRecorde对象ToastRecord lastToast = mToastQueue.remove(index);//3.删除在WMS中的注册的WindowTokenmWindowManagerInternal.removeWindowToken(lastToast.windowToken, false /* removeWindows */,lastToast.displayId);//4.再发一个延时信号确保token删除完成scheduleKillTokenTimeout(lastToast);//5.确保Toast显示过程中进程不会被杀死keepProcessAliveForToastIfNeededLocked(record.pid);//6.集合中如果还有消息,就继续执行if (mToastQueue.size() > 0) {// Show the next one. If the callback fails, this will remove// it from the list, so don't assume that the list hasn't changed// after this point.showNextToastLocked(lastToast instanceof TextToastRecord);}}
主要执行了以下几段逻辑:
1.回调APP层TN的hide方法进行通知;(后续逻辑4.3小节会讲)
2.对队列中删除ToastRecorde对象
3.删除在WMS中的注册的WindowToken
4.再发一个延时信号确保token删除完成
5.确保Toast显示过程中进程不会被杀死
6.集合中如果还有消息,就继续执行
3.5 tryShowToast方法尝试显示
该方法也是很简单的,进行相关逻辑判断是否可以显示,如果可以直接调用record.show方法。
private boolean tryShowToast(ToastRecord record, boolean rateLimitingEnabled,boolean isWithinQuota, boolean isPackageInForeground) {if (rateLimitingEnabled && !isWithinQuota && !isPackageInForeground) {reportCompatRateLimitingToastsChange(record.uid);Slog.w(TAG, "Package " + record.pkg + " is above allowed toast quota, the "+ "following toast was blocked and discarded: " + record);return false;}if (blockToast(record.uid, record.isSystemToast, record.isAppRendered(),isPackageInForeground)) {Slog.w(TAG, "Blocking custom toast from package " + record.pkg+ " due to package not in the foreground at the time of showing the toast");return false;}return record.show();}
这时候,我们就要看ToastRecord的show方法了。之前说了,有两种类实现,分别是TextToastRecord和CustomToastRecord两种类型。CustomToastRecord中的类型就是自定义View的Toast,我们下一章专门来讲,这里我们只讲TextToastRecord的类型。
这里我们先看TextToastRecord类型的实现:
@Overridepublic boolean show() {...mStatusBar.showToast(uid, pkg, token, text, windowToken, getDuration(), mCallback);return true;}
这里的mStatusBar又是一个注册的service,其实现类在StatusBarManagerService.java中:
private final StatusBarManagerInternal mInternalService = new StatusBarManagerInternal() {}
我们直接看其showToast方法:
public void showToast(int uid, String packageName, IBinder token, CharSequence text,IBinder windowToken, int duration,@Nullable ITransientNotificationCallback callback) {if (mBar != null) {try {mBar.showToast(uid, packageName, token, text, windowToken, duration, callback);} catch (RemoteException ex) { }}}
这里的mBar其实是一个binder的引用,其server的实现在SystemUI进程中,实现类是CommandQueue。所以,最终会转交到CommandQueue的showToast方法中进行处理:
@Overridepublic void showToast(int uid, String packageName, IBinder token, CharSequence text,IBinder windowToken, int duration, @Nullable ITransientNotificationCallback callback) {synchronized (mLock) {SomeArgs args = SomeArgs.obtain();args.arg1 = packageName;args.arg2 = token;args.arg3 = text;args.arg4 = windowToken;args.arg5 = callback;args.argi1 = uid;args.argi2 = duration;mHandler.obtainMessage(MSG_SHOW_TOAST, args).sendToTarget();}}
通过handler转发到主线程,代码如下:
case MSG_SHOW_TOAST: {args = (SomeArgs) msg.obj;String packageName = (String) args.arg1;IBinder token = (IBinder) args.arg2;CharSequence text = (CharSequence) args.arg3;IBinder windowToken = (IBinder) args.arg4;ITransientNotificationCallback callback =(ITransientNotificationCallback) args.arg5;int uid = args.argi1;int duration = args.argi2;for (Callbacks callbacks : mCallbacks) {callbacks.showToast(uid, packageName, token, text, windowToken, duration,callback);}break;}
主线程中通过mCallBacks回调显示,这里的Callbacks的实现在com.android.systemui.toast.ToastUIjava类中。
3.6 ToastUI.showToast完成显示流程
showToast方法中中,委托给ToastPresenter进行逻辑的显示,经典的MVP架构。
public void showToast(int uid, String packageName, IBinder token, CharSequence text,IBinder windowToken, int duration, @Nullable ITransientNotificationCallback callback) {Runnable showToastRunnable = () -> {UserHandle userHandle = UserHandle.getUserHandleForUid(uid);Context context = mContext.createContextAsUser(userHandle, 0);mToast = mToastFactory.createToast(mContext /* sysuiContext */, text, packageName,userHandle.getIdentifier(), mOrientation);if (mToast.getInAnimation() != null) {mToast.getInAnimation().start();}mCallback = callback;mPresenter = new ToastPresenter(context, mIAccessibilityManager,mNotificationManager, packageName);// Set as trusted overlay so touches can pass through toastsmPresenter.getLayoutParams().setTrustedOverlay();mToastLogger.logOnShowToast(uid, packageName, text.toString(), token.toString());mPresenter.show(mToast.getView(), token, windowToken, duration, mToast.getGravity(),mToast.getXOffset(), mToast.getYOffset(), mToast.getHorizontalMargin(),mToast.getVerticalMargin(), mCallback, mToast.hasCustomAnimation());};if (mToastOutAnimatorListener != null) {// if we're currently animating out a toast, show new toast after prev toast is hiddenmToastOutAnimatorListener.setShowNextToastRunnable(showToastRunnable);} else if (mPresenter != null) {// if there's a toast already showing that we haven't tried hiding yet, hide it and// then show the next toast after its hidden animation is donehideCurrentToast(showToastRunnable);} else {// else, show this next toast immediatelyshowToastRunnable.run();}}
3.7 ToastPresenter.show()方法完成最终Toast的显示
所以接下来我们就要看ToastPresenter中的show方法了,也是最终在该方法中完成了普通Toast的展示。方法如下:
public void show(View view, IBinder token, IBinder windowToken, int duration, int gravity,int xOffset, int yOffset, float horizontalMargin, float verticalMargin,@Nullable ITransientNotificationCallback callback, boolean removeWindowAnimations) {checkState(mView == null, "Only one toast at a time is allowed, call hide() first.");mView = view;mToken = token;adjustLayoutParams(mParams, windowToken, duration, gravity, xOffset, yOffset,horizontalMargin, verticalMargin, removeWindowAnimations);addToastView();trySendAccessibilityEvent(mView, mPackageName);if (callback != null) {try {callback.onToastShown();} catch (RemoteException e) {Log.w(TAG, "Error calling back " + mPackageName + " to notify onToastShow()", e);}}}
该方法中,主要还是做了两件事:
1.设置mParams中的属性值。
2.添加到windowManager中完成最终的显示。
我们接下来分开来讲。
3.8 adjustLayoutParams方法配置mParams参数
首先,根据传入的参数调整mParams中的属性值,该属性值决定Toast显示的位置,以及显示时间等等,方法如下:
private void adjustLayoutParams(WindowManager.LayoutParams params, IBinder windowToken,int duration, int gravity, int xOffset, int yOffset, float horizontalMargin,float verticalMargin, boolean removeWindowAnimations) {Configuration config = mResources.getConfiguration();int absGravity = Gravity.getAbsoluteGravity(gravity, config.getLayoutDirection());params.gravity = absGravity;if ((absGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {params.horizontalWeight = 1.0f;}if ((absGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {params.verticalWeight = 1.0f;}params.x = xOffset;params.y = yOffset;params.horizontalMargin = horizontalMargin;params.verticalMargin = verticalMargin;params.packageName = mContext.getPackageName();params.hideTimeoutMilliseconds =(duration == Toast.LENGTH_LONG) ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;params.token = windowToken;if (removeWindowAnimations && params.windowAnimations == R.style.Animation_Toast) {params.windowAnimations = 0;}}
这里我们看一下hideTimeoutMilliseconds参数,就是这个来控制最终的显示时间的,当然,这个时间是显示的最长时间,实际情况下,3.4小节中有讲到,在延时时间到了最后,会发通知销毁Toast,所以,hideTimeoutMilliseconds在绝大场景下并不会生效。
SHORT_DURATION_TIMEOUT和LONG_DURATION_TIMEOUT的设置在代码中设置如下:
private static final long SHORT_DURATION_TIMEOUT = 4000;private static final long LONG_DURATION_TIMEOUT = 7000;
这里需要额外说明一点,params的配置的参数在构造方法中也有一部分:
private WindowManager.LayoutParams createLayoutParams() {WindowManager.LayoutParams params = new WindowManager.LayoutParams();params.height = WindowManager.LayoutParams.WRAP_CONTENT;params.width = WindowManager.LayoutParams.WRAP_CONTENT;params.format = PixelFormat.TRANSLUCENT;params.windowAnimations = R.style.Animation_Toast;params.type = WindowManager.LayoutParams.TYPE_TOAST;params.setFitInsetsIgnoringVisibility(true);params.setTitle(WINDOW_TITLE);params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;setShowForAllUsersIfApplicable(params, mPackageName);return params;}
我们这里重点看下面这一行
params.type=WindowManager.LayoutParams.TYPE_TOAST;
在安卓中,type代表window的优先层级,数字越大代表优先级越高,就会盖在上面显示。TYPE_TOAST=2005,而Activity所对应的Window优先级是最低的,其所对应的type=1,所以Toast会在Activity的上面显示。
具体代码参考如下:
public static final int TYPE_BASE_APPLICATION = 1;
public static final int FIRST_SYSTEM_WINDOW = 2000;
public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;所以
TYPE_TOAST = 2005
TYPE_BASE_APPLICATION = 1//Activity中设置的type参数的代码,代码在ActivityThread的handleResumeActivity方法中l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
addToastView方法添加到windowManager中
方法内容如下,这里就比较简单了,直接添加到windowManager上。
private void addToastView() {if (mView.getParent() != null) {mWindowManager.removeView(mView);}try {mWindowManager.addView(mView, mParams);} catch (WindowManager.BadTokenException e) {// 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.Log.w(TAG, "Error while attempting to show toast from " + mPackageName, e);return;}}
需要注意的是,第三种从service中的binder接收开始,代码是执行在NoticationManagerService所属的SystemServer进程,以及ToastPresenter所在的SystemUI进程,都不是APP进程,所以如果在显示了Toast后立马杀掉APP进程,Toast仍然会正常显示。
Toast超时隐藏流程
toast有显示,有windowManager.addView的流程,那么等到持续时间一到,自然有隐藏的Toast的流程。
既然讲到这里,那就不得不讲一下addView之后的流程,主要流程如下:
所以最终会调用WindowManagerService的addWindow方法中。
整个方法流程太长了,所以我们只看和Toast相关的这一部分,代码中会注册一个延时消息,而延时的时间恰恰就是之前设置到mParams中的hideTimeoutMilliseconds,也就是我们上面所说的4S或者7S。
public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,int displayId, int requestUserId, InsetsVisibilities requestedVisibilities,InputChannel outInputChannel, InsetsState outInsetsState,InsetsSourceControl[] outActiveControls) {...if (type == TYPE_TOAST) {if (!displayContent.canAddToastWindowForUid(callingUid)) {ProtoLog.w(WM_ERROR, "Adding more than one toast window for UID at a time.");return WindowManagerGlobal.ADD_DUPLICATE_ADD;}// Make sure this happens before we moved focus as one can make the// toast focusable to force it not being hidden after the timeout.// Focusable toasts are always timed out to prevent a focused app to// show a focusable toasts while it has focus which will be kept on// the screen after the activity goes away.if (addToastWindowRequiresToken|| (attrs.flags & FLAG_NOT_FOCUSABLE) == 0|| displayContent.mCurrentFocus == null|| displayContent.mCurrentFocus.mOwnerUid != callingUid) {mH.sendMessageDelayed(mH.obtainMessage(H.WINDOW_HIDE_TIMEOUT, win),win.mAttrs.hideTimeoutMilliseconds);}}...return res;}
所以接下来我们就要处理H.WINDOW_HIDE_TIMEOUT事件的代码:
case WINDOW_HIDE_TIMEOUT: {final WindowState window = (WindowState) msg.obj;synchronized (mGlobalLock) {...window.mAttrs.flags &= ~FLAG_KEEP_SCREEN_ON;window.hidePermanentlyLw();window.setDisplayLayoutNeeded();mWindowPlacerLocked.performSurfacePlacement();}break;}
然后WindowState.java的hidePermanentlyLw方法如如下,通过hide方法去实现隐藏,所以隐藏的流程是不需要客户端或者NotificationManagerService来控制的,而是WMS自己来维护的。
public void hidePermanentlyLw() {if (!mPermanentlyHidden) {mPermanentlyHidden = true;hide(true /* doAnimation */, true /* requestAnim */);}}
至于hide隐藏,或者show展示的流程,我们这里不展开讲了,这一块其实属于View的完整显示流程中的内容,会有另外的文章专门来讲。这里我们只需要知道,把Window注册到WMS中后,并不是立马显示的,而是在下一个Vsync信号来临时执行的渲染流程并最终显示到屏幕上的就好了。
四.自定义View的Toast流程讲解
4.1转发到APP层执行逻辑
上文讲到,自定义View的实现类型是CustomToastRecord,其show()方法如下:
public boolean show() {...callback.show(windowToken);...}
就是简单的完成了callback的show回调。而这个callback又是binder对象,其实现是APP侧Toast中TN对象。所以我们接着看一下TN中的show方法:
public void show(IBinder windowToken) {mHandler.obtainMessage(SHOW, windowToken).sendToTarget();}
通过mHander从binder线程转发事件到Toast所绑定的looper的线程进行处理(一般是主线程,但并不绝对是)。handler中会执行handleShow方法,代码如下:
public void handleShow(IBinder windowToken) {...//如果已经传入了取消和隐藏的信号,那就没必要继续显示了if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {return;}//mNextView是自定义的View,而mView是上一次显示的内容(如果Toast复用的话)if (mView != mNextView) {// remove the old view if necessaryhandleHide();mView = mNextView;mPresenter.show(mView, mToken, windowToken, mDuration, mGravity, mX, mY,mHorizontalMargin, mVerticalMargin,new CallbackBinder(getCallbacks(), mHandler));}}
toast对象首次显示的话,mView==null。则后续主要分为两块逻辑:
1.先调用handleHide隐藏当前的mView,其最终的实现也是通过ToastPresenter.hide来实现的。具体实现逻辑我们4.3中再讲
2.通过ToastPresenter.show方法进行显示流程。
4.2 ToastPresenter.show显示Toast
执行ToastPresenter.show之前,首先会把mNextView设置为当前将要显示的mView。
mView = mNextView;
show()方法上面的3.7章节我们已经讲过了,就不再重复讲述了。唯一的区别就是这里此时的代码是在APP进程中执行的,而3.7是在SystemServer进程中。
所以自定义的View最终也是通过WindowManager.addView的方式进行显示的。
4.3 Toast的隐藏流程
上面3.4小节的时候还讲到,等到设置的显示时间到了,会通过binder机制通知到Toast.TN中的hide方法。
hide方法中通过handler从binder线程转发到looper所在的线程。
然后Handler中交给handleHide方法进行处理,另外4.1中显示一个自定义view的Toast之前,也会调用handleHide的逻辑,handleHide的代码如下,主要是交给ToastPresenter.hide进行处理
public void handleHide() {if (mView != null) {...mPresenter.hide(new CallbackBinder(getCallbacks(), mHandler));mView = null;}}
ToastPresenter中hide方法如下:
public void hide(@Nullable ITransientNotificationCallback callback) {checkState(mView != null, "No toast to hide.");if (mView.getParent() != null) {mWindowManager.removeViewImmediate(mView);}try {mNotificationManager.finishToken(mPackageName, mToken);} catch (RemoteException e) {Log.w(TAG, "Error finishing toast window token from package " + mPackageName, e);}if (callback != null) {try {callback.onToastHidden();} catch (RemoteException e) {Log.w(TAG, "Error calling back " + mPackageName + " to notify onToastHide()",e);}}mView = null;mToken = null;}
具体代码如下,主要执行了以下的流程:
1.如果mView有parent的话,首先从WindowManager中删除,其中mView一定是最顶层的View。
2.通知NotificationManagerService
3.执行毁掉onToastHidden进行通知
4.清空mView和mToken,因为上一个流程执行完成了。
4.4 小节
所以总结一下,自定义View的toast显示和隐藏,其实就类似于APP侧把一个自定义View添加到WindowManager上,然后定时时间到了之后在从WindowManager中移除该自定义View。
五.总结
我们来总结一下,其实Toast显示主要分为两种类型,Text类型和Custom类型。
如果Text类型的话,最终交给SystemServer进程的负责显示,最终会交给ToastUI负责最终的显示工作。它再向WMS注册添加window的时候,会附带传入结束时候,由WMS在时间到了之后负责隐藏Window。
而Custom类型,最终交回给APP进程负责显示,最终也是通过向WMS添加Window的方式进行显示的。此时NotificationManagerService负责记录时间,时间到了之后通知APP进程进行隐藏工作。
Toast显示流程的主要流程图可以总结如下:
六.几个相关问题的拓展
1.Toast可以子线程使用吗?
答:这个问题和子线程中是否可以更新UI有一点类似。只不过检查点和流程略微有一些区别。
首先生成Toast对象的时候会有一个检查Toast.getLooper()方法中:
private Looper getLooper(@Nullable Looper looper) {if (looper != null) {return looper;}return checkNotNull(Looper.myLooper(),"Can't toast on a thread that has not called Looper.prepare()");}
如果子线程默认当前线程是不会绑定looper的,所以会报错。那么如果我们子线程中初始化Looper呢?那就可以了,只不过最终实现上还有一些区别。TextToastRecord最终仍然会在SystemServer进程中被add到WindowManager中,而CustomToastRecord类型的最终会在APP侧的子线程中显示。另外要注意,prepare一定要和loop方法搭配使用才可以,如下:
new Thread(new Runnable() {@Overridepublic void run() {Looper.prepare();Toast toast = Toast.makeText(getBaseContext(), "显示内容", Toast.LENGTH_LONG);toast.show();Looper.loop();}}).start();
2.为什么有时候显示Toast会提示要打开通知权限?
上面3.2小节中有讲到,显示Toast之前会进行权限检查,代码如下:
private boolean checkCanEnqueueToast(String pkg, int callingUid,boolean isAppRenderedToast, boolean isSystemToast) {//当前APP是否被挂起final boolean isPackageSuspended = isPackagePaused(pkg);//当前APP是否有通知权限android.Manifest.permission.INTERACT_ACROSS_USERSfinal boolean notificationsDisabledForPackage = !areNotificationsEnabledForPackage(pkg,callingUid);//APP是否在后台final boolean appIsForeground;final long callingIdentity = Binder.clearCallingIdentity();try {appIsForeground = mActivityManager.getUidImportance(callingUid)== IMPORTANCE_FOREGROUND;} finally {Binder.restoreCallingIdentity(callingIdentity);}//首先在非系统Toast情况下,APP进程在后台并且没有INTERACT_ACROSS_USERS权限,或者APP进程被挂起,都不会显示if (!isSystemToast && ((notificationsDisabledForPackage && !appIsForeground)|| isPackageSuspended)) {Slog.e(TAG, "Suppressing toast from package " + pkg+ (isPackageSuspended ? " due to package suspended.": " by user request."));return false;}//上一个自定义toast卡住了if (blockToast(callingUid, isSystemToast, isAppRenderedToast,isPackageInForegroundForToast(callingUid))) {Slog.w(TAG, "Blocking custom toast from package " + pkg+ " due to package not in the foreground at time the toast was posted");return false;}return true;}
总结一下:
首先在非系统Toast情况下,APP进程在后台并且没有INTERACT_ACROSS_USERS权限,或者APP进程被挂起,都不会显示Toast。
也就是说,如果有INTERACT_ACROSS_USERS权限就可以在后台显示Toast了。
3.先调用show的Toast一定会先显示吗?
其实这道题问的有点没有意义,一般来说是Toast越先调用show方法会越早显示。
但是也有一些特殊情况,比如两个进程或者两个线程中,A先执行Toast,但是卡住了没有立马执行到显示流程。这时候B线程中也只执行了一次show方法。过了100毫秒,A又执行了一次show。
大体流程如下
A线程中: toast.show()
B线程中: toast.show()
//100号秒后
A线程中: toast.show()
这种情况下说A在B前面也行,说A在B后面也行,最终仍然是A先执行。A的第二次show调用在最终显示之前只是会更新其在NMS中对应的ToastRecord对象中的参数而异。
4.为什么Toast会显示在Activity上面,而不会被Activity覆盖?
这个就涉及到Window的优先级的概念了。3.8小节中有细讲。
5.显示Toast后立马杀掉进程,Toast会立马消失吗?
不会,一样分两种场景:
如果是默认Toast,最终addWindow和removeWindow的操作都在SystemServer进程中,自然杀掉APP进程对Toast的展示没有任何影响。(实际场景验证过)
如果是自定义Toast,显示会在APP进程。3.3小节中有讲到,显示Toast时会调用keepProcessAliveForToastIfNeededLocked方法保证显示Toast的进程不被杀死,所以此时自定义Toast应该还是可以正常显示的。(从代码进行的推论,没有验证过,有热心的朋友可以帮忙验证下)
6.Toast的显示时间一定是4S或者7S嘛?
由于时间计算是在系统侧的,所以只能是4S或者7S,当然并不一定是绝对值。
3.4小节中有讲到,获取到delay延时时间后,如果开启了无障碍辅助功能,首先会通过无障碍修正这个delay时间。其次,Toast的入场和出场动画,也都是单独计算时间的。
所以最终显示时间会略大于4S或者7S。
下面在举两个Toast中经常容易遇到的错误。
7.show的时候提示not attached to window manager 错误解决
具体报错如下:
java.lang.IllegalArgumentException: View=androidx.recyclerview.widget.RecyclerView{adb693 VFED..... ........ 0,0-1080,1548 #7f0800d1 app:id/recycler} not attached to window managerat android.view.WindowManagerGlobal.findViewLocked(WindowManagerGlobal.java:534)at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:438)at android.view.WindowManagerImpl.removeView(WindowManagerImpl.java:157)at android.widget.ToastPresenter.addToastView(ToastPresenter.java:303)at android.widget.ToastPresenter.show(ToastPresenter.java:231)at android.widget.ToastPresenter.show(ToastPresenter.java:214)at android.widget.Toast$TN.handleShow(Toast.java:699)at android.widget.Toast$TN$1.handleMessage(Toast.java:631)at android.os.Handler.dispatchMessage(Handler.java:106)at android.os.Looper.loopOnce(Looper.java:201)at android.os.Looper.loop(Looper.java:288)
首先我们看addToastView方法:
if (mView.getParent() != null) {mWindowManager.removeView(mView);
}
如果mView.getParent不为空,则去WindowManager中removeView该view。
然后在看WindowManagerGlobal中findViewLocked方法:
private int findViewLocked(View view, boolean required) {final int index = mViews.indexOf(view);if (required && index < 0) {throw new IllegalArgumentException("View=" + view + " not attached to window manager");}return index;}
会去mViews中寻找该view,如果找不到,则会抛出上文中的错误。
mViews中保存着APP中所有注册的window的rootView。该View不在mViews中,但又有parentView,则说明该View已经绑定了parentView。
我们在根据下面代码分析可得出结论,setView传入的view是有问题的,是已经绑定了parentView的,这种View自然不能作为rootView。
View=androidx.recyclerview.widget.RecyclerView{adb693 VFED..... ........ 0,0-1080,1548 #7f0800d1 app:id/recycler}
8.hide的时候提示not attached to window manager 错误解决
具体报错如下:
java.lang.IllegalArgumentException: View=android.widget.LinearLayout{12b02f3 V.E...... ......ID 0,52-629,368} not attached to window managerat android.view.WindowManagerGlobal.findViewLocked(WindowManagerGlobal.java:572)at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:476)at android.view.WindowManagerImpl.removeViewImmediate(WindowManagerImpl.java:144)at android.widget.ToastPresenter.hide(ToastPresenter.java:230)at android.widget.Toast$TN.handleHide(Toast.java:826)at android.widget.Toast$TN$1.handleMessage(Toast.java:746)at android.os.Handler.dispatchMessage(Handler.java:106)at android.os.Looper.loop(Looper.java:236)at com.xxx.NeverCrash$1.run(NeverCrash.java:39)at android.os.Handler.handleCallback(Handler.java:938)at android.os.Handler.dispatchMessage(Handler.java:99)at android.os.Looper.loop(Looper.java:236)at android.app.ActivityThread.main(ActivityThread.java:8060)at java.lang.reflect.Method.invoke(Native Method)at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:656)at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:967)
我们可以看流程图中画红圈的部分:
当tryShowToast返回true时,就一定会走到ToastPresenter的hide流程。但是tryShowToast返回true的时候一定会显示成功吗?
上面的流程图中我们可以知道tryShowToast的返回值是有CustomToastRecord的show()方法返回,我们看一下这个方法:
@Overridepublic boolean show() {...try {callback.show(windowToken);return true;} catch (RemoteException e) {...mNotificationManager.keepProcessAliveForToastIfNeeded(pid);return false;}}
只有binder通讯失败的时候才会返回false,其余都返回true。而最终APP层一定能WindowManager.addView()成功吗?其答案自然是否定的。
我们在看最终显示的ToastPresenter.addToastView方法:
private void addToastView() {if (mView.getParent() != null) {mWindowManager.removeView(mView);}try {mWindowManager.addView(mView, mParams);} catch (WindowManager.BadTokenException e) {return;}}
也就是说既然添加失败了也没有任何处理。
所以也就是说,如果显示自定义Toast的时候,如果因为某种原因导致最终addView失败,那么等到时间到了,就会导致上面所说的崩溃。这个可以理解为源码中存在的问题,show的时候做了保护,但是hide的时候并未做任何的保护。
那么如何解决这种问题呢?既然是系统的问题,解决起来还是比较麻烦的。
我的想法是这这样的:既然是show的时候try catch了,那么hide的时候能否也进行try catch呢?
我们可以通过反射的时候拿到Toast.TN中的Handler对象,然后通过对其进行代理来实现我们先要的效果。
首先,通过反射拿到Toast.TN中的Handler对象mHandler,然后在创建一个和其同looper的Handler对象,通过反射替换掉原来的mHandler。
自定义的Handler实现伪代码如下:
Handler oldHandler = null;//反射拿到的原来的handlerHandler mHandler = new Handler(oldHandler.getLooper(), null) {@Overridepublic void handleMessage(Message msg) {switch (msg.what) {case 0: {oldHandler.obtainMessage(0, msg.obj).sendToTarget();break;}case 1: {try{//反射调用handleHide();方法}catch (Exception e){e.printStackTrace();}//反射把mNextView = null;break;}case 2: {//和1的流程类似}}}};
总体来说,因为过多的使用反射,效率不高,但是理论上确实能解决Toast问题问题。
android源码学习-Toast实现原理讲解相关推荐
- 【Android 源码学习】Zygote启动原理
Android 源码学习 Zygote启动原理 望舒课堂 Zygote进程启动原理学习记录整理. Zygote简介 Zygote是进程在init进程启动时创建的,进程本身是app_process,来源 ...
- 【Android 源码学习】SystemServer启动原理
Android 源码学习 SystemServer启动原理 望舒课堂 SystemServer进程启动原理学习记录整理. 参考文章: Android系统启动流程(三)解析SyetemServer进程启 ...
- Android源码学习之浅析SystemServer脉络
在之前的博文中<Android源码学习之如何创建使用JNI>和<Android源码学习之如何使用eclipse+NDK>中,浅谈了如何创建使用JNI和如何利用NDK工具开发创建 ...
- 【Android 源码学习】 init启动
目录 Android 源码学习 init启动 从main.cpp开始 init.cpp 部分逻辑 init启动zygote 属性服务 总结 Android 源码学习 init启动 Android 11 ...
- 【Android 源码学习】系统架构和启动流程
Android 源码学习 系统架构和启动流程 望舒课堂 学习记录整理.以及以下参考文章的整理汇总.便于我个人的学习记录. 感谢IngresGe,Gityuan的精彩文章.为我们这些初探android系 ...
- 【Android 源码学习】SharedPreferences 源码学习
第一章:SharedPreferences 源码学习 文章目录 第一章:SharedPreferences 源码学习 Android SharedPreferences的缺陷 MMKV.Jetpack ...
- Android源码学习之工厂方法模式应用
主要内容: 工厂方法模式定义 工厂方法模式优势 工厂方法模式在Android源码中的应用 一.工厂方法模式定义 工厂方法模式定义: Define an interface for creating a ...
- Android源码学习以及在工作中的应用01-TextView
有人说种下一棵树最好的时间是十年前,其次是现在.我已经浪费了整整十年,所以从现在起,脚踏实地,静下心来学习,一切从头开始.期望十年后的自己,无怨无悔. 我们在自动化测试的工作中,有一个这样的场景需求. ...
- Android源码学习之handler
前言 是滴!我又来了...今天来讲讲老少皆宜的大名鼎鼎的handler.是的,想必handler这个东西已经被讨论的天花乱坠了,也经常被我们用在实际开发中,但是其中很多细节知识还是值得我们去学习深究的 ...
最新文章
- 剑桥大学:机器学习模型部署都有哪些坑?
- VC 6.0 + SP6 下载 及 安装详细说明
- 46个PPT下载丨QCon 2019年全球软件开发大会PPT
- 带你探索CPU调度的奥秘
- ionic 集成websocket
- 在的微型计算机系统中 外设可和,微机原理第七章题库
- VB6 GDI+ 入门教程[7] Graphics 其他内容
- 生成一定范围内的互不相同的随机数的方法比较
- 【多线程经典实例】实现一个线程安全的单例模式
- python脚本微博自动转发抽奖_微博自动转发抽奖软件
- C++银行账户管理程序2
- 原生汇率计算器系统源代码
- 30天突破英语口语!(MP3版)
- ec----------
- 实验3-11 求一元二次方程的根
- 公司合伙人股权的进入和退出机制
- 多线程:模仿火车站售票
- 史上最全的HTML、CSS知识点总结,浅显易懂。适合入门新手
- 解决mac系统向日葵远控无法被远程控制问题(白屏)
- 对于一个网络营销新手,需要掌握哪些网络营销基础知识