Toast 自定义布局重复添加异常分析
Toast Exception : java.lang.IllegalStateException: View has already been added to the window manager.
Crash堆栈如下:
Exception:java.lang.IllegalStateException: View com.autonavi.skin.view.SkinRelativeLayout{95730 V.E...... ......I. 0,0-0,0}
has already been added to the window manager.
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:328)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:93)
at android.widget.Toast$TN.handleShow(Toast.java:498)
at android.widget.Toast$TN$1.handleMessage(Toast.java:401)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6494)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
在项目中显示Toast用到了自定义布局mLayContent,而mLayContent是重复利用的。代码示例:
Toast toast = new Toast(this);
toast.setDuration(1000);
toast.setView(mLayContent);
纯粹从发生Crash的异常堆栈来分析,针对这种异常,常规处理是在设置Toast布局前先检测下mLayContent父布局是否为空,如果不为空则从父布局中移除掉mLayContent布局。然而,这个方法在这边并没什么用。下面对Toast显示的源码逻辑处理进行分析。
1、Toast.java类源码分析
public void handleShow(IBinder windowToken) {
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();
}
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
if (mView.getParent() != null) {
mWM.removeView(mView);
}
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
}
}
public void handleHide() {
if (mView != null) {
if (mView.getParent() != null) {
mWM.removeViewImmediate(mView);
}
mView = null;
}
}
上面是Toast显示和隐藏的简化代码,从代码里面可以看到,显示和隐藏的时候都有对当前添加的布局是否含有父布局进行检查,如果已经添加过布局,则从WindowManager中移除这个布局。
在Crash发生之前,查看Log可以看到Toast的显示和隐藏操作非常频繁,频繁操作导致时序上的问题,在上层添加是否含有父布局的判断是没用的。到framework层WindowManager显示Toast的时候一样会报异常。
2、WindowManagerGlobal.java类源码分析
Toast显示的布局是通过WindowManager接口去添加的,找到继承WindowManager接口的实现类WindowManagerImpl。可以看到WindowManagerImpl最终是通过WindowManagerGlobal单例对象来显示的。
WindowManager addView方法
接着看WindowManagerGlobal里面的addView方法,可以看到抛出
" has already been added to the window manager."
异常的地方。addView方法简化代码如下:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
if (view == null) {
throw new IllegalArgumentException(“view must not be null”);
}
if (display == null) {
throw new IllegalArgumentException(“display must not be null”);
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException(“Params must be WindowManager.LayoutParams”);
}
synchronized (mLock) {
int index = findViewLocked(view, false);
if (index >= 0) {
if (mDyingViews.contains(view)) {
// Don’t wait for MSG_DIE to make it’s way through root’s queue.
mRoots.get(index).doDie();
} else {
throw new IllegalStateException(“View ” + view + ” has already been added to the window manager .”);
}
}
}
}
3、异常解决方法
既然是时序上的问题导致布局重复添加异常,解决方法很简单,不进行布局复用,每次显示Toast之前都用新的Inflate出来的布局就行。