Toast引发的BadTokenException问题

前言

最近公司项目最近出现了一个Toast引发的BadTokenException崩溃,集中在Android5.0 - Android7.1.2版本,经过分析解决了,所以现在想记录一下。

崩溃日志

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)

模拟复现

// 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;
        // 创建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);
}

通过构造函数我们知道初始化Toast对象创建了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远程通知管理器进行统一管理,enqueueToast()方法就是把TN对象传递给NMS并回传过来用于标志Toast显示状态。

NotificationManagerService#enqueueToast()

// 集合队列
final ArrayList mToastQueue = new ArrayList();
...省略部分代码
synchronized (mToastQueue) {
    try {
          ToastRecord record;
          int index = indexOfToastLocked(pkg, callback){
                    if (index >= 0) {
                        record = mToastQueue.get(index);
                        record.update(duration);
                    } else {
                        // 是不是系统的Toast
                        if (!isSystemToast) {
                            int count = 0;
                            final int N = mToastQueue.size();
                            for (int i=0; i= 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转发到主线程中处理异步信息,我们看收到消息后,怎么处理的

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)放到参数里,至于这个Token的作用我们后面说,代码3调用WindowManager去添加视图, 其实问题也就在这里产生的,当token过期失效的时候,会抛出BadToken异常问题。熟悉View的绘制流程的话,我们知道WindowManager是个接口,实现类是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处代码已经显现出问题的原因了,我们进入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对象,真正的实现类是Session,所以Toast在创建过程中也会创建一个Window,之后就是Window的创建过程,我们一起在屡一下Window的创建过程。

@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()真是一波三折啊!不过这里使用了门面模式,最终实现都交给了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中创建好的WindowManager.LayoutParams中的Token也就是IBinder对象,以及标记好的Type也就是TYPE_TOKEN。所以我们在代码3处可以知道当token==null的时候,会进行异常验证,出现BadToken问题,所以我们只要找到使之Token失效的原因就可以了。根据模拟复现的代码,我们可知调用了show()方法我们已经跨进程通讯通知NMS我们要显示一个吐司,NMS准备好后再通过跨进程通信回调通知TN, TN在使用Handler发送信息通知当前线程,开始调用handleShow方法,并携带一个windowToken。这时候我们调用了Thread.Sleep()方法,休眠了主线程,导致Handler阻塞,通知延迟,Sleep()时间一过去,这是又立即通知TN#handleShow方法,可是这回由于Toast的显示时间已经过去,NMS#scheduleDurationReachedLocked(record);这个方法还在执行 不受应用进程中的线程睡眠的影响。

    @GuardedBy("mToastQueue")
    void showNextToastLocked() {
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            try {
                // 通知TN显示,并向WMS发送消息
                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发送延迟移除视图(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创建Window,但Token已经失效所以导致BadToken异常。

你可能感兴趣的:(Toast引发的BadTokenException问题)