我是靠谱客的博主 无辜豆芽,这篇文章主要介绍Android25上Toast的BadTokenException问题,现在分享给大家,希望可以做个参考。

一、问题:

在公司的crash平台上发现了这个crash,机型分布很杂乱,但是都分布在Android25上,量也挺大的,1w的DAU有40个crash,算TopCrash了。其实之前这个crash就一直存在了,但是这次突然crash量大增,没办法就分给我解决啦。先看CrashLog:

android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@831e7b5 is not valid; is your activity running?
at android.view.ViewRootImpl.setView(ViewRootImpl.java:683)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:342)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94)
at android.widget.Toast$TN.handleShow(Toast.java:502)
at android.widget.Toast$TN$2.handleMessage(Toast.java:381)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6211)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:793)

初看log,毫无解决头绪,不知道是哪里出来的,在Google和Baidu上一番搜索,终于让我找到了不少答案。

 

二、定位:

参考了 http://www.cnblogs.com/qcloud1001/p/8421356.html中的内容,我这里再简单的捋一捋,篇幅所限,贴出来的代码有删减。

1.Toast调用show时,会将一个TN对象交给NMS(NotificationManagerService),作为之后NMS和UI交互的通信渠道。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
public void show() { INotificationManager service = getService(); String pkg = mContext.getOpPackageName(); TN tn = mTN; try { service.enqueueToast(pkg, tn, mDuration); } catch (RemoteException e) { // Empty } }

2.NMS为toast在WMS(WindowManagerService)中注册token,再通过TN对象显示toast,并且向自己postDelay一个隐藏toast的消息,需要注意的是,NMS用的是NMS所在线程的Handler,与后面所说的TN的Handler不是同一个。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void enqueueToast(String pkg, ITransientNotification callback, int duration{ Binder token = new Binder(); mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY); record = new ToastRecord(callingPid, pkg, callback, duration, token); mToastQueue.add(record); showNextToastLocked(); } void showNextToastLocked() { ToastRecord record = mToastQueue.get(0); try { // 这里的callback其实是上个方法传入的TN对象 record.callback.show(record.token); // 利用Handler.postDelay了一个隐藏Toast的消息,Delay的值就是Toast的Duration scheduleTimeoutLocked(record); return; } catch (RemoteException e) { } }

3.隐藏toast的消息到达(即超时)之后,NMS会先通过TN对象隐藏toast,,然后将toast在WMS中注册的token移除。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private void handleTimeout(ToastRecord record){ int index = indexOfToastLocked(record.pkg, record.callback); if (index >= 0) { cancelToastLocked(index); } } void cancelToastLocked(int index) { ToastRecord record = mToastQueue.get(index); try { // 隐藏Toast record.callback.hide(); } catch (RemoteException e) { } // 移除Token ToastRecord lastToast = mToastQueue.remove(index); mWindowManagerInternal.removeWindowToken(lastToast.token, true, DEFAULT_DISPLAY); }

4.我们不关心NMS调用TN这种IPC是怎么实现的。在TN中,是通过postMessage的方式实现显示或隐藏toast的效果,而且所用到的Handler与Toast创建线程的Looper关联,当消息队列任务较多或者主线程卡死时,消息就会被阻塞。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public void show(IBinder windowToken) { mHandler.obtainMessage(SHOW, windowToken).sendToTarget(); } mHandler = new Handler(looper, null) { @Override public void handleMessage(Message msg) { switch (msg.what) { case SHOW: { IBinder token = (IBinder) msg.obj; handleShow(token); break; } case HIDE: { handleHide(); break; } case CANCEL: { mNextView = null; try { getService().cancelToast(mPackageName, TN.this); } catch (RemoteException e) { } break; } } } };

再接着看handleShow:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void handleShow(IBinder windowToken) { if (mView != mNextView) { mView = mNextView; Context context = mView.getContext().getApplicationContext(); String packageName = mView.getContext().getOpPackageName(); if (context == null) { context = mView.getContext(); } // Important1 mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); // Important2 mWM.addView(mView, mParams); trySendAccessibilityEvent(); }

这里有两个关键点:Important1是我们解决问题的关键,这个mView就是Toast对应的视图,而且可以通过getView方式取到;Important2在Android26开始加了try-catch块,所以只在Android25才表现出crash。

5.若TN显示toast的消息在NMS移除了token之后才到达,那么在TN调用WindowManager.addView的时候就会引发BadTokenException的异常,具体逻辑见WindowManagerImpl。

画了一个简单的图

三、解决

通过上述分析,暂时有三个思路:

第一种是判断Toast是否显示然后再决定要不要移除token,如果改NotificationManagerService的方法不太可能,而且用来显示Toast的TN类没有判断Toast是否显示的方法;
第二种是替换TN类,因为是TN类调用的addView,但是TN类是内部类不能继承,如果要自己重新写一个比较麻烦;
第三种就是替换Context了。根据Important1处的代码,当调用Context的getSystemService方法时返回一个在addView时进行了try-catch的WindowManager,防止crash。
替换Context是成本最低的,并且在源码中追踪之后,getApplicationContext只在此处有作用,影响也很小。

解决方案:

参考https://github.com/drakeet/ToastCompat,利用反射替换Toast中View的Context

最后

以上就是无辜豆芽最近收集整理的关于Android25上Toast的BadTokenException问题的全部内容,更多相关Android25上Toast内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(48)

评论列表共有 0 条评论

立即
投稿
返回
顶部