Toast or Safe Toast?

  在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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容