-
Notifications
You must be signed in to change notification settings - Fork 0
7.1.1设备Toast crash
panwj edited this page Dec 6, 2018
·
3 revisions
#1664 android.view.WindowManager$BadTokenException Unable to add window -- window android.view.ViewRootImpl$W@4a51004 has already been added android.view.ViewRootImpl.setView(ViewRootImpl.java:695) android.view.ViewRootImpl.setView(ViewRootImpl.java:691) android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:342) android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94) android.widget.Toast$TN.handleShow(Toast.java:506) android.widget.Toast$TN2ドル.handleMessage(Toast.java:389) android.os.Handler.dispatchMessage(Handler.java:102) android.os.Looper.loop(Looper.java:154) android.app.ActivityThread.main(ActivityThread.java:6292) java.lang.reflect.Method.invoke(Native Method) com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:906) com.android.internal.os.ZygoteInit.main(ZygoteInit.java:796)
首先Toast显示依赖于一个窗口,这个窗口被WMS管理(WindowManagerService),当需要show的时候这个请求会放在WMS请求队列中,并且会传递一个TN类型的Bider对象给WMS,WMS并生成一个token传递给Android进行显示与隐藏,但是如果UI线程的某个线程发生了阻塞,并且已经NotificationManager检测已经超时就不删除token记录,此时token已经过期,阻塞结束的时候再显示的时候就发生了异常。
该问题仅发生在android 7.1.1 版本, 在该版本toast的显示实现是
mWM.addView(mView, mParams);
而在8.0的实现是
try { mWM.addView(mView, mParams); trySendAccessibilityEvent(); } catch (WindowManager.BadTokenException e) { }
我们直接在 Toast.show 函数外增加 try-catch 是没有意义的。因为 Toast.show 实际上只是发了一条命令给 NotificationManager 服务。真正的显示需要等 NotificationManager 通知我们的 TN 对象 show 的时候才能触发。NotificationManager 通知给 TN 对象的消息,都会被 TN.mHandler 这个内部对象进行处理
//code Toast.java private static class TN { final Runnable mHide = new Runnable() {// 通过 mHandler.post(mHide) 执行 @Override public void run() { handleHide(); mNextView = null; } }; final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { IBinder token = (IBinder) msg.obj; handleShow(token);// 处理 show 消息 } }; }
在NotificationManager 通知给 TN 对象显示的时候,TN 对象将给 mHandler 对象发送一条消息,并在 mHandler 的 handleMessage 函数中执行。 当NotificationManager 通知 TN 对象隐藏的时候,将通过 mHandler.post(mHide) 方法,发送隐藏指令。不论采用哪种方式发送的指令,都将执行 Handler 的 dispatchMessage(Message msg) 函数:
//code Handler.java public void dispatchMessage(Message msg) { if (msg.callback != null) { handleCallback(msg);// 执行 post(Runnable)形式的消息 } else { ... handleMessage(msg);// 执行 sendMessage形式的消息 } } 因此,我们只需要在 dispatchMessage 方法体内加入 try-catch 就可以避免 Toast 崩溃对应用程序的影响: public void dispatchMessage(Message msg) { try { super.dispatchMessage(msg); } catch(Exception e) {} }
因此,我们可以定义一个安全的 Handler 装饰器:
private static class SafelyHandlerWarpper extends Handler { private Handler impl; public SafelyHandlerWarpper(Handler impl) { this.impl = impl; } @Override public void dispatchMessage(Message msg) { try { super.dispatchMessage(msg); } catch (Exception e) {} } @Override public void handleMessage(Message msg) { impl.handleMessage(msg);//需要委托给原Handler执行 } }
由于 TN.mHandler 对象复写了 handleMessage 方法,因此,在 Handler 装饰器里,需要将 handleMessage 方法委托给 TN.mHandler 执行。定义完装饰器之后,我们就可以通过反射往我们的 Toast 对象中注入了:
public class ToastUtils { private static Field sField_TN ; private static Field sField_TN_Handler ; static { try { sField_TN = Toast.class.getDeclaredField("mTN"); sField_TN.setAccessible(true); sField_TN_Handler = sField_TN.getType().getDeclaredField("mHandler"); sField_TN_Handler.setAccessible(true); } catch (Exception e) {} } private static void hook(Toast toast) { try { Object tn = sField_TN.get(toast); Handler preHandler = (Handler)sField_TN_Handler.get(tn); sField_TN_Handler.set(tn,new SafelyHandlerWarpper(preHandler)); } catch (Exception e) {} } public static void showToast(Context context,CharSequence cs, int length) { Toast toast = Toast.makeText(context,cs,length); hook(toast); toast.show(); } }