Toast引發(fā)的BadTokenException問題

前言

最近公司項(xiàng)目最近出現(xiàn)了一個Toast引發(fā)的BadTokenException崩潰伴澄,集中在Android5.0 - Android7.1.2版本,經(jīng)過分析解決了寸齐,所以現(xiàn)在想記錄一下仇穗。

崩潰日志

android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@cf6e52d is not valid; is your activity running?
        at android.view.ViewRootImpl.setView(ViewRootImpl.java:679)
        at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:342)
        at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:93)
        at android.widget.Toast$TN.handleShow(Toast.java:459)
        at android.widget.Toast$TN$2.handleMessage(Toast.java:342)
        at android.os.Handler.dispatchMessage(Handler.java:102)
        at android.os.Looper.loop(Looper.java:154)
        at android.app.ActivityThread.main(ActivityThread.java:6119)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)

模擬復(fù)現(xiàn)

// android api25 7.1.1
tv.setOnClickListener {
    Toast.makeText(this, testEntity.nameLi,Toast.LENGTH_SHORT).show()
    Thread.sleep(3000)
}

源碼復(fù)習(xí)

在Android中,我們知道所有的UI視圖都是依附于Window济赎,因此Toast也需要一個窗口。我們一步來看下Toast源碼记某。

public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
        return makeText(context, null, text, duration);
}
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,@NonNull CharSequence text, @Duration int duration) {
        Toast result = new Toast(context, looper);
        // 找到布局文件 并在布局文件中找到要展示的TextView控件并賦值
        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;
    }

我們熟知的調(diào)用makeText().show()方法即可將Toast彈窗展示出來司训,makeText()方法中實(shí)例化了Toast對象,我們看看構(gòu)造方法做了些什么液南。

public Toast(Context context) {
        this(context, null);
}
public Toast(@NonNull Context context, @Nullable Looper looper) {
        mContext = context;
        // 創(chuàng)建TN對象
        mTN = new TN(context.getPackageName(), 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);
}

通過構(gòu)造函數(shù)我們知道初始化Toast對象創(chuàng)建了TN對象壳猜,并提供了上下文。

    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;
        final int displayId = mContext.getDisplayId();
        try {
            // 添加到Toast顯示隊(duì)列
            service.enqueueToast(pkg, tn, mDuration, displayId);
        } catch (RemoteException e) {
            // Empty
        }
    }

由方法名可見滑凉,Toast的顯示是加入到隊(duì)列中统扳,但是如何加入隊(duì)列中的呢?其實(shí)Toast并非是由自身控制畅姊,而是通過AIDL進(jìn)程間通信咒钟,將Toast信息傳遞給NMS遠(yuǎn)程通知管理器進(jìn)行統(tǒng)一管理,enqueueToast()方法就是把TN對象傳遞給NMS并回傳過來用于標(biāo)志Toast顯示狀態(tài)若未。

NotificationManagerService#enqueueToast()

// 集合隊(duì)列
final ArrayList<ToastRecord> mToastQueue = new ArrayList<ToastRecord>();
...省略部分代碼
synchronized (mToastQueue) {
    try {
          ToastRecord record;
          int index = indexOfToastLocked(pkg, callback){
                    if (index >= 0) {
                        record = mToastQueue.get(index);
                        record.update(duration);
                    } else {
                        // 是不是系統(tǒng)的Toast
                        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++;
                                     // 1
                                     if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                                         Slog.e(TAG, "Package has already posted " + count
                                                + " toasts. Not showing more. Package=" + pkg);
                                         return;
                                     }
                                 }
                            }
                        }
                        Binder token = new Binder();
                       // 2
                        mWindowManagerInternal.addWindowToken(token,
                                WindowManager.LayoutParams.TYPE_TOAST);
                        record = new ToastRecord(callingPid, pkg, callback, duration, token);
                        mToastQueue.add(record);
                        index = mToastQueue.size() - 1;
                        keepProcessAliveIfNeededLocked(callingPid);
                    }
                    if (index == 0) {
                        // 3
                        showNextToastLocked();
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }

由1處代碼我們知道Toast排隊(duì)的長度最大50條朱嘴,不過在Api29中被改為了25條。由代碼2我們可知使用WindowManager將構(gòu)造的Toast添加到了當(dāng)前的Window中陨瘩,被標(biāo)記Type類型是TypeToast腕够。代碼3處如果當(dāng)先隊(duì)列中沒有元素级乍,則說明直接顯示即可,說明showNextToastLocked()這個方法就是NMS通知顯示的Toast的方法帚湘。

NotificationManagerService#showNextToastLocked()

ToastRecord(int pid, String pkg, ITransientNotification callback, int duration,
                    Binder token) {
            this.pid = pid;
            this.pkg = pkg;
            this.callback = callback;
            this.duration = duration;
            this.token = token;
        }

void showNextToastLocked() {
        // 1
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            try {
                // 2
                record.callback.show(record.token);
                scheduleTimeoutLocked(record);
                return;
            } catch (RemoteException e) {
               // 省略
            }
        }
    }

代碼1處從集合中拿到index=0的ToastRecord, 2處代碼調(diào)用ITransientNotification#show()方法并傳入token這個token關(guān)鍵玫荣,之后回調(diào)到TN中的show()方法之中了。

TN#show()

 @Override
        public void show(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.obtainMessage(0, windowToken).sendToTarget();
        }

這里通過Handler轉(zhuǎn)發(fā)到主線程中處理異步信息大诸,我們看收到消息后捅厂,怎么處理的

final Handler mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                IBinder token = (IBinder) msg.obj;
                handleShow(token);
            }
        };
public void handleShow(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                    + " mNextView=" + mNextView);
            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();
                }
                // 1
                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;
                // 2
                mParams.token = windowToken;
                if (mView.getParent() != null) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeView(mView);
                }
                // 3
                mWM.addView(mView, mParams);
                trySendAccessibilityEvent();
            }
        }

代碼1獲取到WindowManager,方法2 將Token(Binder)放到參數(shù)里资柔,至于這個Token的作用我們后面說焙贷,代碼3調(diào)用WindowManager去添加視圖, 其實(shí)問題也就在這里產(chǎn)生的,當(dāng)token過期失效的時候贿堰,會拋出BadToken異常問題辙芍。熟悉View的繪制流程的話,我們知道WindowManager是個接口羹与,實(shí)現(xiàn)類是WindowManagerImpl故硅,最終addView方法是調(diào)用WindowManagerGlobal的addView()方法。

public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
        // ... 省略代碼
        ViewRootImpl root;
        View panelParentView = null;
        // ...  省略代碼
        synchronized (mLock) {
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        }
        try {
            // 1
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            synchronized (mLock) {
                final int index = findViewLocked(view, false);
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
            }
            throw e;
        }
    }

1處代碼已經(jīng)顯現(xiàn)出問題的原因了纵搁,我們進(jìn)入ViewRootImpl看下setView()方法吃衅;

 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView){
      // 太長了 省略一堆代碼...
       int res; 
      // 1
       res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);
        switch (res) {
                        case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
                        case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
                            throw new WindowManager.BadTokenException(
                                    "Unable to add window -- token " + attrs.token
                                    + " is not valid; is your activity running?");
      }
 }

mWindowSession 的類型是IWindowSession,他是一個Binder對象,真正的實(shí)現(xiàn)類是Session腾誉,所以Toast在創(chuàng)建過程中也會創(chuàng)建一個Window徘层,之后就是Window的創(chuàng)建過程,我們一起在屢一下Window的創(chuàng)建過程利职。

@UnsupportedAppUsage
final IWindowSession mWindowSession;
mWindowSession = WindowManagerGlobal.getWindowSession();

看WindowSession是在WindowManagerGlobal中獲取的趣效,我們跟進(jìn)下:

@UnsupportedAppUsage
    public static IWindowSession getWindowSession() {
        synchronized (WindowManagerGlobal.class) {
            if (sWindowSession == null) {
                try {
                 @UnsupportedAppUsage
                    InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary();
                    // 1
                    IWindowManager windowManager = getWindowManagerService();
                    // 3
                    sWindowSession = windowManager.openSession(
                            new IWindowSessionCallback.Stub() {
                                @Override
                                public void onAnimatorScaleChanged(float scale) {
                                    ValueAnimator.setDurationScale(scale);
                                }
                            });
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
            return sWindowSession;
        }
}
@UnsupportedAppUsage
    public static IWindowManager getWindowManagerService() {
        synchronized (WindowManagerGlobal.class) {
            if (sWindowManagerService == null) {
                // 2
                sWindowManagerService = IWindowManager.Stub.asInterface(
                        ServiceManager.getService("window"));
                try {
                    if (sWindowManagerService != null) {
                        ValueAnimator.setDurationScale(
                                sWindowManagerService.getCurrentAnimatorScale());
                    }
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
            return sWindowManagerService;
        }
    }

代碼1和2處我們看到通過AIDL遠(yuǎn)程調(diào)用到了WindowManagerService對象,并調(diào)用了openSession()方法眼耀。

@Override
    public IWindowSession openSession(IWindowSessionCallback callback) {
        return new Session(this, callback);
    }

由此可知ViewRootImpl#setView()最終調(diào)用了Session類的addToDisplay()

@Override
    public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, Rect outFrame, Rect outContentInsets,
            Rect outStableInsets, Rect outOutsets,
            DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
            InsetsState outInsetsState) {
        // 1
        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outFrame,
                outContentInsets, outStableInsets, outOutsets, outDisplayCutout, outInputChannel,
                outInsetsState);
    }

真是轉(zhuǎn)來轉(zhuǎn)去最終由要回到WindowManagerService#addWindow()真是一波三折坝⒅А!不過這里使用了門面模式哮伟,最終實(shí)現(xiàn)都交給了WMS。堅(jiān)持淄薄楞黄!hold on !馬上到高潮了抡驼。

public int addWindow(Session session, IWindow client, int seq,
            LayoutParams attrs, int viewVisibility, int displayId, Rect outFrame,
            Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
            DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
            InsetsState outInsetsState) {
// 省略...
            // 1
            WindowToken token = displayContent.getWindowToken(hasParent ? parentWindow.mAttrs.token : attrs.token);
            // 2
            final int rootType = hasParent ? parentWindow.mAttrs.type : type;
            if (token == null) {
                  // 3
                  if (type == TYPE_TOAST) {
                    if (doesAddToastWindowRequireToken(attrs.packageName, callingUid,
                           parentWindow)){
                        return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                    }
                }
            }
}

1處獲取到在TN中創(chuàng)建好的WindowManager.LayoutParams中的Token也就是IBinder對象鬼廓,以及標(biāo)記好的Type也就是TYPE_TOKEN。所以我們在代碼3處可以知道當(dāng)token==null的時候致盟,會進(jìn)行異常驗(yàn)證碎税,出現(xiàn)BadToken問題尤慰,所以我們只要找到使之Token失效的原因就可以了。根據(jù)模擬復(fù)現(xiàn)的代碼雷蹂,我們可知調(diào)用了show()方法我們已經(jīng)跨進(jìn)程通訊通知NMS我們要顯示一個吐司伟端,NMS準(zhǔn)備好后再通過跨進(jìn)程通信回調(diào)通知TN, TN在使用Handler發(fā)送信息通知當(dāng)前線程,開始調(diào)用handleShow方法匪煌,并攜帶一個windowToken责蝠。這時候我們調(diào)用了Thread.Sleep()方法,休眠了主線程萎庭,導(dǎo)致Handler阻塞霜医,通知延遲,Sleep()時間一過去驳规,這是又立即通知TN#handleShow方法肴敛,可是這回由于Toast的顯示時間已經(jīng)過去,NMS#scheduleDurationReachedLocked(record);這個方法還在執(zhí)行 不受應(yīng)用進(jìn)程中的線程睡眠的影響吗购。

    @GuardedBy("mToastQueue")
    void showNextToastLocked() {
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            try {
                // 通知TN顯示值朋,并向WMS發(fā)送消息
                record.callback.show(record.token);
                // 計(jì)算時間
                scheduleDurationReachedLocked(record);
                return;
            } catch (RemoteException e) {
               // 省略...
            }
        }
    }

    @GuardedBy("mToastQueue")
    private void scheduleDurationReachedLocked(ToastRecord r)
    {
      
        mHandler.removeCallbacksAndMessages(r);
        Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r);
        int delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
        delay = mAccessibilityManager.getRecommendedTimeoutMillis(delay,
                AccessibilityManager.FLAG_CONTENT_TEXT);
        // 使用Handler發(fā)送延遲移除視圖(Toast)消息
        mHandler.sendMessageDelayed(m, delay);
    }


switch (msg.what)
            {
                // 到時間了
                case MESSAGE_DURATION_REACHED:
                    handleDurationReached((ToastRecord) msg.obj);
                    break;
                    args.recycle();
                    break;
            }

時間到了以后巩搏,cancelToastLocked(index);調(diào)用取消Toast昨登,并將Token置空。這時Toast中的Handler才收到handleShow()贯底,告知WMS創(chuàng)建Window丰辣,但Token已經(jīng)失效所以導(dǎo)致BadToken異常。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末禽捆,一起剝皮案震驚了整個濱河市笙什,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌胚想,老刑警劉巖琐凭,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異浊服,居然都是意外死亡统屈,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進(jìn)店門牙躺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來愁憔,“玉大人,你說我怎么就攤上這事孽拷《终疲” “怎么了?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長膜宋。 經(jīng)常有香客問我窿侈,道長,這世上最難降的妖魔是什么秋茫? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任史简,我火速辦了婚禮,結(jié)果婚禮上学辱,老公的妹妹穿的比我還像新娘乘瓤。我一直安慰自己,他們只是感情好策泣,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布衙傀。 她就那樣靜靜地躺著,像睡著了一般萨咕。 火紅的嫁衣襯著肌膚如雪统抬。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天危队,我揣著相機(jī)與錄音聪建,去河邊找鬼。 笑死茫陆,一個胖子當(dāng)著我的面吹牛金麸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播簿盅,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼挥下,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了桨醋?” 一聲冷哼從身側(cè)響起棚瘟,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎喜最,沒想到半個月后偎蘸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡瞬内,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年迷雪,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片遂鹊。...
    茶點(diǎn)故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡振乏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出秉扑,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布舟陆,位于F島的核電站误澳,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏秦躯。R本人自食惡果不足惜忆谓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望踱承。 院中可真熱鬧倡缠,春花似錦、人聲如沸茎活。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽载荔。三九已至盾饮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間懒熙,已是汗流浹背丘损。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留工扎,地道東北人徘钥。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像肢娘,于是被迫代替她去往敵國和親呈础。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評論 2 353