Unable to add window崩溃分析

        显示Dialog时,我们通常会将Activity实例作为Context传给Dialog(Dialog和PopupWindow的显示与隐藏会依赖父窗口),然后调用Dialog的show方法显示。但有些情况,比如说先通过异步网络请求请求数据,等拿到数据后再调用Dialog的show方法显示Dialog,我们偶尔会发现发生这样的程序崩溃:

android.view.WindowManager$BadTokenException: Unable to add window — token android.os.BinderProxy@447a6748 is not valid; is your activity running?
at android.view.ViewRoot.setView(ViewRoot.java:468)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:177)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:91)
at android.view.Window$LocalWindowManager.addView(Window.java:424)
at android.app.Dialog.show(Dialog.java:239)
at android.app.Activity.showDialog(Activity.java:2488)
…

        从log中可以看出,Dialog窗口无法被加到一个无效的token窗口上,问我们“activity现在真的在运行”吗?

        现在再来看一下我们的实现,我们先发送一个异步网络请求去获取数据,这个过程可能会很长,尤其是在使用2G网或者网络状态差的时候尤为明显,等到获取到数据,我们再根据获取到的数据显示相应的Dialog。发现问题了么?如果我们等的时间太长,我们就没耐心再等下去了,而是选择关掉然后重新打开,或者直接关掉不管了,但是异步请求的线程可能刚完成工作,正要执行Dialog的show方法,但此时activity已经被关掉了,Dialog要show的父窗口已经不存在了,因此出现了上面log所描述的错误异常。

出错的测试代码:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new BackgroundTask().execute("requestParameter");
    }

    private void onBackgroundTaskComplete(String result) {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setMessage("Background Task Completed!")
                .setCancelable(false)
                .setPositiveButton("Yes", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        dialog.cancel();
                    }
                })
                .setNegativeButton("No", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        dialog.cancel();
                    }
                });
        AlertDialog alertDialog = builder.create();
        alertDialog.show();
    }

    private class BackgroundTask extends AsyncTask {

        @Override
        protected String doInBackground(String... args) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "State:OK";
        }

        @Override
        protected void onPostExecute(String result) {
            super.onPostExecute(result);
            onBackgroundTaskComplete(result);
        }
    }

        看了Dimitar Darazhanski的博客,简单翻译一下:

Having threads to do some heavy lifting and long processing in the background is pretty standard stuff. Very often you would want to notify or prompt the user after the background task has finished by displaying a Dialog.
The displaying of the Dialog has to happen on the UI thread, so you would do that either in the Handler object for the thread or in the onPostExecute method of an AsyncTask (which is a thread as well, just an easier way of implementing it). That is a textbook way of doing this and you would think that pretty much nothing wrong could go with this.
Surprisingly I found out that something CAN actually go wrong with this. After Google updated the Android Market and started giving crash reports to the developers I received the following exception:

        开个线程去做复杂或长时间任务是很常见的事,大多数情况下,你会选择在后台任务结束时去弹出个Dialog对话框,以便通知或提示用户任务已经完成了。

        显示Dialog必须在UI线程中进行,所以你不得不在Handler实例中调用Dialog的show方法来显示,或者在AsyncTask的onPostExecute方法中进行显示操作(AsyncTask也是一个线程,只不过是一种简单地实现方式),这是教科书式的做法,因此你会觉得这么做肯定不会有错。

        令人惊讶的是,我发现这么做确实会出现错误,在Google更新了Android Market并且开始把崩溃报告反馈给开发者时,我收到了以下的异常日志:

android.view.WindowManager$BadTokenException: Unable to add window — token android.os.BinderProxy@447a6748 is not valid; is your activity running?
at android.view.ViewRoot.setView(ViewRoot.java:468)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:177)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:91)
at android.view.Window$LocalWindowManager.addView(Window.java:424)
at android.app.Dialog.show(Dialog.java:239)
at android.app.Activity.showDialog(Activity.java:2488)
…
at android.os.Handler.dispatchMessage(Handler.java:99)
…
only got a couple of these exceptions from thousands of installs, so I knew that was not anything that happens regularly or that it was easy to replicate.
Looking at the stack trace above it gives us a pretty good idea why it failed. It started in the Handler object, which naturally was called by a background thread after it finished its processing. The Handler instance tried to show a Dialog and before it could show it, it tried to set the View for it and then it failed with:
android.view.WindowManager$BadTokenException: Unable to add window — token android.os.BinderProxy@447a6748 is not valid; is your activity running?
The 447a6748 number is just a memory address of an object that no longer exists.
Note- do not get hung up on the exact number. It would be different with every execution.
Now we know why the application crashed, the only thing left is to figure out what caused it?

        我仅仅从几千个安装者中得到了几个这样的异常,所以我知道这个异常并不是定期发生或者频繁发生的。看了上面的栈跟踪信息,给了我我为什么会失败的idea。在后台线程完成工作时会调用Handler实例的处理方法,Handler实例尝试去显示一个Dialog,当然前提是这个Dialog可以显示,在它尝试去设置显示View时,因为下面的原因失败了:

android.view.WindowManager$BadTokenException: Unable to add window — token android.os.BinderProxy@447a6748 is not valid; is your activity running?

        447a6748 只是一个已经不存在的对象的内存地址。注意:不要纠结在这个具体数字上,每次运行它都会不同。
        现在,我们知道我们的程序为什么会崩溃了,剩下的事就是弄清楚是什么原因造成的。

We know that background threads execute independently of the main UI thread. That means that the user could be interacting with the application during the time that the thread is doing its work under the covers. Well, what happens if the user hits the “Back” button on the device while the background thread is running and what happens to the Dialog that this thread is supposed to show? Well, if the timing is right the application will most likely crash with the above described error.
In other words what happens is that the Activity will be going through its destruction when the background thread finishes its work and tries to show a Dialog.
In this case it is almost certain that this should have been handled by the Virtual Machine. It should have recognized the fact that the Activity is in the process of finishing and not even attempted to show the Dialog. This is an oversight of the Google developers and it will probably be fixed some time in the future, but in the meantime the burden is on us to take care of this.

        我们知道后台线程是独立于UI线程执行的,这就意味着用户在后台线程工作的时候依然能与应用进行交互,好了,如果后台线程正在工作的时候用户点击了返回按钮,而Dialog又要去显示,会发生什么?是的,如果时间合适应用就会出现上面所说的崩溃。

        换句话说,就是当Activity将要销毁时,后台线程完成了工作并尝试显示Dialog对话框时,就会出现这样的崩溃。在这种情况下,几乎可以肯定这个应该由虚拟机处理,它应该意识到Activity正在销毁的时候就不应该尝试显示Dialog了,这是一个谷歌开发者的疏忽,将来可能被修复吧,但在这之间还得我们自己去处理这个问题。

        解决办法:

        就是在显示Dialog之前检测一下所在Activity是不是正处在销毁阶段

    if(!((Activity) context).isFinishing()) {
        //show dialog
    }

        isFinishing()方法用来判断当前Activity是否处于finishing阶段,常用在onPause()方法中用来判断Activity是简单的pausing还是completely finishing。返回true表明Activity正在finishing,否则返回false。

        其它方法:

        将Dialog的生命周期限制在Activity的生命周期内。如:设置一个成员变量mIsRunning,在onResume()方法中设置成true,在onPause()设置成false,当要显示Dialog时判断一下mIsRunning为true时再显示。如果用的时AsyncTask,可以在onPause()中取消这个异步任务。

        try/catch这个Dialog的显示与隐藏,但是不推荐这种处理方式。

        推荐使用DialogFragment代替传统Dialog

        当然,show的时候会崩溃,dismiss的时候也会崩溃,崩溃log如下:

java.lang.IllegalArgumentException: View not attached to window manager
at android.view.WindowManagerImpl.findViewLocked(WindowManagerImpl.java:356)
at android.view.WindowManagerImpl.removeView(WindowManagerImpl.java:201)
at android.view.Window$LocalWindowManager.removeView(Window.java:400)
at android.app.Dialog.dismissDialog(Dialog.java:268)
at android.app.Dialog.access$000(Dialog.java:69)
at android.app.Dialog$1.run(Dialog.java:103)
at android.app.Dialog.dismiss(Dialog.java:252)
at xxx.onPostExecute(xxx$1.java:xxx)

        也是因为Activity被销毁的缘故,所以工作线程结束后要操作Dialog或者PopupWindow时要格外注意

你可能感兴趣的:(Android)