美文网首页
Toast or Safe Toast?

Toast or Safe Toast?

作者: ZDCrazy | 来源:发表于2018-04-28 15:52 被阅读198次

      在Toast与Snackbar的那点事儿这篇文章中,受益良多,翻看了自己项目,发现项目也有重构一份SafeToastManager来统一管理应用Toast的弹出,并且解决方案和美团的这篇文章竟然出奇的一致!由于项目的最高TargetSdk版本还是23,所以暂时还没用到后续提到的SnackBar。

      但是在翻看该篇文章的时候,发现作者有一处错误的地方,在作者解释Toast导致BadTokenException的时候,有说明到该奔溃主要集中在Android 5.0 -- Android 7.1.2的机型上,自己有翻看了源码并实践,发现这种说法是有问题的。

      经过试验发现,在Sdk25(Android 7.1)版本以下的机型上,是不会产生BadTokenException问题的,只有在Sdk版本为 25 的机型上,才有可能产生异常崩溃,因为在Android O(26)中,Google修复了此问题。

    原因分析

      Toast导致BadTokenException的原因详见Toast与Snackbar的那点事儿,其根本原因就是主线程Looper阻塞导致的异常,NMS会在固定时间之后移除生成的Token标识,而此时主线程由于阻塞没能及时的执行WindowManager.addView(),导致出现BadTokenException异常。在Android 7.1.1上,WMS执行addWindow()判断代码如下:

    public int addWindow(Session session, IWindow client, int seq,
                WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
                Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
                InputChannel outInputChannel) {
            ..............
            if (token == null) { 
                   ..............
                   if (type == TYPE_TOAST) {
                        // 注 : 此处判断的为 Target Sdk 版本是否大于 25
                        // Apps targeting SDK above N MR1 cannot arbitrary add toast windows.
                        if (doesAddToastWindowRequireToken(attrs.packageName, callingUid,
                                attachedWindow)) {
                            Slog.w(TAG_WM, "Attempted to add a toast window with unknown token "
                                    + attrs.token + ".  Aborting.");
                            return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                        }
                    }
            }
            ...............
    }
    

      从上述可看出,在TargetSdk为25以上,当需要弹出一个window type为toast类型的弹窗时,首先需要强制判断Token是否存在。那么当一个toast弹出的时候,这个token是什么时候产生的呢。
      通过对Toast.show()的代码跟踪如下,最终会调用到NMS去管理Toast弹出,NMS维护一个toastRecord队列,依次弹出toast,代码如下:

     public void enqueueToast(String pkg, ITransientNotification callback, int duration)
            {
                if (DBG) {
                    Slog.i(TAG, "enqueueToast pkg=" + pkg + " callback=" + callback
                            + " duration=" + duration);
                }
    
                if (pkg == null || callback == null) {
                    Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
                    return ;
                }
    
                final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));
                final boolean isPackageSuspended =
                        isPackageSuspendedForUser(pkg, Binder.getCallingUid());
    
                if (ENABLE_BLOCKED_TOASTS && (!noteNotificationOp(pkg, Binder.getCallingUid())
                        || isPackageSuspended)) {
                    if (!isSystemToast) {
                        Slog.e(TAG, "Suppressing toast from package " + pkg
                                + (isPackageSuspended
                                        ? " due to package suspended by administrator."
                                        : " by user request."));
                        return;
                    }
                }
    
                synchronized (mToastQueue) {
                    int callingPid = Binder.getCallingPid();
                    long callingId = Binder.clearCallingIdentity();
                    try {
                        ToastRecord record;
                        int index = indexOfToastLocked(pkg, callback);
                        // 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 {
                            // Limit the number of toasts that any given package except the android
                            // package can enqueue.  Prevents DOS attacks and deals with leaks.
                            if (!isSystemToast) {
                                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_NOTIFICATIONS) {
                                             Slog.e(TAG, "Package has already posted " + count
                                                    + " toasts. Not showing more. Package=" + pkg);
                                             return;
                                         }
                                     }
                                }
                            }
                            // 产生一个新的Token 对象  source from android 7.1
                            // 注 : 从 Android 7.1 开始,才有的代码
                            Binder token = new Binder();
                            // 通知 WMS 去添加 Token 
                            mWindowManagerInternal.addWindowToken(token,
                                    WindowManager.LayoutParams.TYPE_TOAST);
                            record = new ToastRecord(callingPid, pkg, callback, duration, token);
                            // 将新生成的 ToastRecord 添加至 mToastQueue
                            mToastQueue.add(record);
                            index = mToastQueue.size() - 1;
                            keepProcessAliveIfNeededLocked(callingPid);
                        }
                        // If it's at index 0, it's the current toast.  It doesn't matter if it's
                        // new or just been updated.  Call back and tell it to show itself.
                        // If the callback fails, this will remove it from the list, so don't
                        // assume that it's valid after this.
                        if (index == 0) {
                            // 通知客户端去弹toast
                            showNextToastLocked();
                        }
                    } finally {
                        Binder.restoreCallingIdentity(callingId);
                    }
                }
            }
    

      从上述代码分析可知,在SDK 25版本及以上,产生ToastRecord时会强制生成一个Token,并且在 SDK 为25 的 WMS addWindow()的代码中,会强制判断当前Token是否有效(注:强制判断的条件为当前应用的 TargetSdk 版本是否大于 25),所以,即使在 Android 7.1 的手机上,如果应用的TargetSdk 版本在 26以下,也是不会有问题的。

      综上所述,唯一可能因为Token 不生效,而导致BadTokenException 的情况,只有可能是在Android 7.1 的机型上,但是安装应用的Target Sdk版本为26!

      在Android O(SDK 26)中,Google为此做了修复处理,具体代码参加Toast源码如下:

    public void handleShow(IBinder windowToken) {
                ...................
                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.
                    // 注: Android O 在 windowmanager.addView()的时候,做了 try catch
                    // 处理,手动捕获了异常
                    try {
                        mWM.addView(mView, mParams);
                        trySendAccessibilityEvent();
                    } catch (WindowManager.BadTokenException e) {
                        /* ignore */
                    }
                }
            }
    
    

      Android O在 Toast 最终要展示的时候(即 mWM.addView())的时候,做了 try catch处理,手动捕获了 badTokenException 异常。

      那么如何打造一个Safe Toast 来规避Android 7.1可能遇到的问题呢,在Toast与Snackbar的那点事儿这篇文章中也已经写得很明白,将 NMS 管理Toast的代码移出来,形成一个自己的ToastManager,然后思路和Android O一致,在真正 addView() 的地方,加一个try catch去保护。至于后续文章中提到的为了适配 Android 7.1 而采用的SnackBar策略。自己觉得如果对应用自身TargetSdk版本没有特别高要求的话,可以暂时不升到26。这样的话,即使不try catch 的话,Android 7.1 也不会产生 BadTokenException问题。

    参考文章:http://zhuanlan.51cto.com/art/201804/569585.htm

    相关文章

      网友评论

          本文标题:Toast or Safe Toast?

          本文链接:https://www.haomeiwen.com/subject/xhpglftx.html