公司某个产品有反馈,说是在进入某个界面时容易引发崩溃,要到了崩溃日志后发现,确实爆出了一个异常,非必现,偶发bug,一看就大概明白是啥原因了,自己就写了个demo,复现了这个问题,下面是报错日志
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.wisely, PID: 12063
android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@bf4921f is not valid; is your activity running?
at android.view.ViewRootImpl.setView(ViewRootImpl.java:696)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:347)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94)
at android.app.Dialog.show(Dialog.java:319)
at com.wisely.DialogUtil.showProgressDialog(DialogUtil.java:54)
at com.wisely.DialogUtil.showProgressDialog(DialogUtil.java:16)
at com.wisely.activity.bug.WindowDestroyBugActivity$1.handleMessage(WindowDestroyBugActivity.java:29)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6114)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:874)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:764)
xml比较简单,未列出,当前Activity继承了一个自己写的BaseActivity,其实什么都没做,而且我本身也不建议这种抽取,不过因为这是我的demo集,场景特殊,所以才抽取了这个BaseActivity。
注意,下面的代码是有问题的代码,切勿复制粘贴。
public class WindowDestroyBugActivity extends BaseActivity {
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 1:
DialogUtil.showProgressDialog(WindowDestroyBugActivity.this, "弹窗喽");
break;
}
}
};
/**
* 点击事件的回调,在xml中写的
*/
public void onClick(View v) {
switch (v.getId()) {
case R.id.tv_unable_to_add_window_finish:
finish();
mHandler.sendEmptyMessageDelayed(1, 3000);
break;
}
}
@Override
public int getLayoutResource() {
return R.layout.activity_unable_to_add_window;
}
@Override
public String getTitleContent() {
return "Unable to add window——token android.os.BinderProxy@2ed5fc06 is not valid;is your activity running?";
}
}
/**
* @author wisely
*/
public class DialogUtil {
public static void showProgressDialog(Activity activity, String message){
showProgressDialog(activity,message,false);
}
private static WeakReference mWeakReference;
private static ProgressDialog mProgressDialog;
/**
* @param activity 需要弹窗的activity
* @param message 弹窗展示的内容
* @param flag 触摸弹窗外区域,是否取消窗口
* */
public static void showProgressDialog(Activity activity,String message,boolean flag){
if(!isLiving(activity)){
return;
}
if(mWeakReference == null){
mWeakReference = new WeakReference(activity);
}
activity = mWeakReference.get();
if (mProgressDialog == null) {
if (activity.getParent() != null) {
mProgressDialog = new ProgressDialog(activity.getParent());
} else {
mProgressDialog = new ProgressDialog(activity);
}
}
if (!mProgressDialog.isShowing()) {
mProgressDialog.dismiss();
mProgressDialog.setMessage(message);
mProgressDialog.setIndeterminate(false);
mProgressDialog.setCancelable(flag);
mProgressDialog.setIcon(R.mipmap.ic_launcher);
mProgressDialog.show();
} else {
mProgressDialog.setMessage(message);
}
}
/**
* 判断activity是否存活
* */
private static boolean isLiving(Activity activity){
if(activity == null){
Log.d("wisely","activity == null");
return false;
}
return true;
}
}
造成上面崩溃的原因很简单,就是因为在要show dialog的时候,忽然发现,dialog依附的activity已经挂掉了,于是程序崩溃。
知道了原因,解决就容易了。
我们只需要将上面DialogUtil中的isLiving方法修改一下,如下
private static boolean isLiving(Activity activity) {
if (activity == null) {
Log.d("wisely", "activity == null");
return false;
}
if (activity.isFinishing()) {
Log.d("wisely", "activity is finishing");
return false;
}
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {//android 4.2
//
// if (activity.isDestroyed()) {
// Log.d("wisely", "activity is destroy");
// return false;
// }
// }
return true;
}
上面的代码很容易理解,注意我注释掉的代码,里面有一个方法,isDestroyed(),它是从4.2版本开始新增的方法。在很多博文里,都有关于它的使用,但我为什么会注释掉它呢?
再看它前面的代码,里面有一个isFinishing()方法,这个方法和isDestroyed()方法到底有啥不同呢?
根据我的打印发现,在onPause(),onStop(),和onDestroy()方法中,isFinishing()方法的返回值一直为true(在onCreate(),onStart(),onResume()方法中返回false),而isDestroy()的返回值,只有在onDestroy()方法中才为true,其它生命周期方法中,为false。
这么算起来,只要前面写了isFinishing(),那么写不写isDestroyed()方法都是一样的。
神马?还有新问题,对的。
在DialogUtil中,我们只添加了show进度框的方法,怎么能没加close的方法呢,好的,我们加上,如下
/**
* 关闭进度框
*/
public static void closeProgressDialog() {
if (isShowing(mProgressDialog)) {
mProgressDialog.dismiss();
mProgressDialog = null;
mWeakReference.clear();
mWeakReference = null;
}
}
/**
* 判断进度框是否正在显示
*/
private static boolean isShowing(ProgressDialog dialog) {
boolean isShowing = dialog != null
&& dialog.isShowing();
Log.d("wisely",">------isShow:"+isShowing);
return isShowing;
}
代码很明白,但我们调用的时候,还是会存在偶发性bug,下面,我们就自己制造出一个bug。
只列出更改的代码
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 1:
DialogUtil.closeProgressDialog();
break;
case 2:
DialogUtil.showProgressDialog(WindowDestroyBugActivity.this, "弹窗喽");
break;
}
}
};
/**
* 点击事件的回调,在xml中写的
*/
public void onClick(View v) {
switch (v.getId()) {
case R.id.tv_unable_to_add_window_finish:
DialogUtil.showProgressDialog(WindowDestroyBugActivity.this, "弹窗喽");
finish();
mHandler.sendEmptyMessageDelayed(1, 3000);
break;
}
}
逻辑很清晰,先显示一个弹窗,然后finish掉整个activity,过了3s后,再关闭dialog,然后,就爆出了下面的bug。
05-14 23:46:33.867 13456-13456/com.wisely E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.wisely, PID: 13456
java.lang.IllegalArgumentException: View=DecorView@9d7cbf6[] not attached to window manager
at android.view.WindowManagerGlobal.findViewLocked(WindowManagerGlobal.java:479)
at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:388)
at android.view.WindowManagerImpl.removeViewImmediate(WindowManagerImpl.java:126)
at android.app.Dialog.dismissDialog(Dialog.java:360)
at android.app.Dialog.dismiss(Dialog.java:343)
at com.wisely.DialogUtil.closeProgressDialog(DialogUtil.java:67)
at com.wisely.activity.bug.WindowDestroyBugActivity$1.handleMessage(WindowDestroyBugActivity.java:28)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6114)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:874)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:764)
最终,报错的其实是DialogUtil中的dismiss方法
mProgressDialog.dismiss();
not attached to window manager,通俗点讲就是,dialog要关闭,但activity已经挂掉了。这个原因,与在show dialog时非常类似。
原因明白,解决也很简单
只需要在close的时候,多加一个判断
if (isShowing(mProgressDialog) && isExist_Living(mWeakReference)) {
....
}
private static boolean isExist_Living(WeakReference weakReference) {
if(weakReference != null){
Activity activity = weakReference.get();
if (activity == null) {
return false;
}
if (activity.isFinishing()) {
return false;
}
return true;
}
return false;
}
好了,问题解决了。
纳尼?还有问题,不是说好都解决了吗?!
好吧,其实还真有一个,在上面的代码运行时,会发现一个问题,虽然不会导致程序挂掉,但看着也让人不爽快,如下
E/WindowManager: android.view.WindowLeaked: Activity com.wisely.activity.bug.WindowDestroyBugActivity has leaked window DecorView@f9bf091[] that was originally added here
at android.view.ViewRootImpl.(ViewRootImpl.java:423)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:337)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94)
at android.app.Dialog.show(Dialog.java:319)
at com.wisely.DialogUtil.showProgressDialog(DialogUtil.java:55)
at com.wisely.DialogUtil.showProgressDialog(DialogUtil.java:17)
at com.wisely.activity.bug.WindowDestroyBugActivity.onClick(WindowDestroyBugActivity.java:54)
at java.lang.reflect.Method.invoke(Native Method)
at android.view.View$DeclaredOnClickListener.onClick(View.java:4694)
at android.view.View.performClick(View.java:5611)
at android.view.View$PerformClick.run(View.java:22276)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6114)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:874)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:764)
上面的错误翻译一下就是,你的dialog窗口泄露了,因为没有在activity finish的时候,关闭dialog。
思路明细,解决方案自然有,只需要在某个生命周期方法中,调用dialog关闭的方法即可。
那么,问题来了,在哪个生命周期方法中close呢?有3个可选,onPause(),onStop(),onDestroy()。
当然是onPause()方法了,很好,我们在onPause()方法中调用DialogUtil中的closeProgressDialog()方法,但是发现,貌似没有作用,原因为嘛,我们来看closeProgressDialog()中的一个判断
private static boolean isExist_Living(WeakReference weakReference) {
if(weakReference != null){
Activity activity = weakReference.get();
if (activity == null) {
return false;
}
if (activity.isFinishing()) {
return false;
}
return true;
}
return false;
}
由于我们是在onPause方法中调用的closeProgressDialog()方法,所以activity.isFinishing()的返回值为true,导致整个isExist_Living()方法的返回值为false,最终在closeProgressDialog()方法中,并不会运行mProgressDialog.dismiss()这句代码。
那怎么办,没办法,只能单独为它来设置一个方法了,如下:
public static void closeProgressDialog(boolean flag){
if (isShowing(mProgressDialog) && flag) {
mProgressDialog.dismiss();
mProgressDialog = null;
mWeakReference.clear();
mWeakReference = null;
}
}
这个方法与之前的方法类似,唯一的不同之处就在于有一个传入参数,在onPause方法中调用时,直接传入true即可。
@Override
protected void onPause() {
super.onPause();
DialogUtil.closeProgressDialog(true);
}
至于方法的抽取,就不在本篇博文的讨论范围之内了。
偶发的bug,时间久了,也就变成必现了。