Toast引發(fā)的BadTokenException問題

前言

最近公司項目最近出現(xiàn)了一個Toast引發(fā)的BadTokenException崩潰,集中在Android5.0 - Android7.1.2版本,經過分析解決了,所以現(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)

模擬復現(xiàn)

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

源碼復習

在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;
    }

我們熟知的調用makeText().show()方法即可將Toast彈窗展示出來,makeText()方法中實例化了Toast對象,我們看看構造方法做了些什么。

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);
}

通過構造函數(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顯示隊列
            service.enqueueToast(pkg, tn, mDuration, displayId);
        } catch (RemoteException e) {
            // Empty
        }
    }

由方法名可見,Toast的顯示是加入到隊列中,但是如何加入隊列中的呢?其實Toast并非是由自身控制,而是通過AIDL進程間通信,將Toast信息傳遞給NMS遠程通知管理器進行統(tǒng)一管理,enqueueToast()方法就是把TN對象傳遞給NMS并回傳過來用于標志Toast顯示狀態(tài)。

NotificationManagerService#enqueueToast()

// 集合隊列
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排隊的長度最大50條,不過在Api29中被改為了25條。由代碼2我們可知使用WindowManager將構造的Toast添加到了當前的Window中,被標記Type類型是TypeToast。代碼3處如果當先隊列中沒有元素,則說明直接顯示即可,說明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處代碼調用ITransientNotification#show()方法并傳入token這個token關鍵,之后回調到TN中的show()方法之中了。

TN#show()

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

這里通過Handler轉發(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調用WindowManager去添加視圖, 其實問題也就在這里產生的,當token過期失效的時候,會拋出BadToken異常問題。熟悉View的繪制流程的話,我們知道WindowManager是個接口,實現(xiàn)類是WindowManagerImpl,最終addView方法是調用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處代碼已經顯現(xià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對象,真正的實現(xiàn)類是Session,所以Toast在創(chuàng)建過程中也會創(chuàng)建一個Window,之后就是Window的創(chuàng)建過程,我們一起在屢一下Window的創(chuàng)建過程。

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

看WindowSession是在WindowManagerGlobal中獲取的,我們跟進下:

@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遠程調用到了WindowManagerService對象,并調用了openSession()方法。

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

由此可知ViewRootImpl#setView()最終調用了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);
    }

真是轉來轉去最終由要回到WindowManagerService#addWindow()真是一波三折啊!不過這里使用了門面模式,最終實現(xiàn)都交給了WMS。堅持住!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對象,以及標記好的Type也就是TYPE_TOKEN。所以我們在代碼3處可以知道當token==null的時候,會進行異常驗證,出現(xiàn)BadToken問題,所以我們只要找到使之Token失效的原因就可以了。根據(jù)模擬復現(xiàn)的代碼,我們可知調用了show()方法我們已經跨進程通訊通知NMS我們要顯示一個吐司,NMS準備好后再通過跨進程通信回調通知TN, TN在使用Handler發(fā)送信息通知當前線程,開始調用handleShow方法,并攜帶一個windowToken。這時候我們調用了Thread.Sleep()方法,休眠了主線程,導致Handler阻塞,通知延遲,Sleep()時間一過去,這是又立即通知TN#handleShow方法,可是這回由于Toast的顯示時間已經過去,NMS#scheduleDurationReachedLocked(record);這個方法還在執(zhí)行 不受應用進程中的線程睡眠的影響。

    @GuardedBy("mToastQueue")
    void showNextToastLocked() {
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            try {
                // 通知TN顯示,并向WMS發(fā)送消息
                record.callback.show(record.token);
                // 計算時間
                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);調用取消Toast,并將Token置空。這時Toast中的Handler才收到handleShow(),告知WMS創(chuàng)建Window,但Token已經失效所以導致BadToken異常。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。