显示Dialog时,我们通常会将Activity实例作为Context传给Dialog,然后调用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所描述的错误异常。
去stackoverflow上看了一下,发现了很多比较好的解释,[详见:Unable to add window — token android.os.BinderProxy is not valid; is your activity running?]
看了Dimitar Darazhanski的博客,简单翻译一下:
开个线程去做复杂或长时间任务是很常见的事,大多数情况下,你会选择在后台任务结束时去弹出个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)
…
我仅仅从几千个安装者中得到了几个这样的异常,所以我知道这个异常并不是定期发生或者频繁发生的。看了上面的栈跟踪信息,给了我我为什么会失败的idea。在后台线程完成工作时会调用Handler实例的处理方法,Handler实例尝试去显示一个Dialog,当然前提是这个Dialog可以显示,在它尝试去设置显示View时,因为下面的原因失败了:
447a6748 只是一个已经不存在的对象的内存地址。注意:不要纠结在这个具体数字上,每次运行它都会不同。
现在,我们知道我们的程序为什么会崩溃了,剩下的事就是弄清楚是什么原因造成的。
我们知道后台线程是独立于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()中取消这个异步任务。或者使用Activity的showDialog方法显示Dialog。
try/catch这个Dialog的显示与隐藏,但是不推荐这种处理方式。
当然,show的时候会崩溃,dismiss的时候也会崩溃,崩溃log为:
也是因为Activity被销毁的缘故,所以工作线程结束后要操作Dialog时要格外注意。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)