android.view.WindowManager$BadTokenException & Android – Displaying Dialogs From Background Threads

android.view.WindowManager$BadTokenException & Android – Displaying Dialogs From Background Threads

    使用线程子在后台处理一些繁重的和耗时的操作是非常标准的。后台线程结束后通常你可能会使用对话框提示或通知用户。
    对话框是在主线程中产生的,因此你可以在子线程对应的Handler中处理或者在AsyncTask的onPostExecute方法中处理。 这是一个教科书的方法,你也认为这样做没有任何错误。

令人惊奇的是我确实发现了一些问题。google更新了Android 市场后,开始出现一些应用崩溃报告个开发人员,收到的异常如下:
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)

我发现了这些异常从成千上万的应用中,所以这不是经常发生,或者很容易复制。

查栈上面轨迹,给了我们为什么出错的一个很好的提示,起始于Handle类,Handle通常是在后台线程结束后被调用,这个Handle实例试图去显示一个对话框,试图设置视图,结果失败了:android.view.WindowManager$BadTokenException: Unable to add window — token android.os.BinderProxy@447a6748 is not valid; is your activity running?

447a6748只是对象的内存地址,程序崩溃后就已经不存在了。注意:不要纠结于这个数字,每次执行可能都会不同。
现在我们知道了应用程序崩溃的原因,接下来就是解决什么引起了应用崩溃。

我们知道后台线程是独立于主线程执行的。这也就意味着用户可以与应用程序进行交互,在后台线程运行的时候。当后台线程运行的时候用户点击设备的返回按钮将会发生什么,线程试图去显示对话框会发生什么。
如果实际恰当应用很可能就会崩溃,并出现上面描述的错误。

换句话说,后台线程完成任务试图去显示对话框时,对应的Activity被销毁了。

这种情况应该是虚拟机来处理。应该意识到对应的activity在销毁的时候不去显示对话框。这是google开发者的疏忽,未来可能会被解决,这也意味着增加了开发人员的负担。

解决这个问题也很简单,仅仅是判断在显示对话框前是否销毁了对应的activity。
private Handler myHandler = new Handler() {
@private Handler myHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case DISPLAY_DLG:
if (!isFinishing()) {
showDialog(MY_DIALOG);
}
break;
}
}
};

对应的英文:

android.view.WindowManager$BadTokenException & Android – Displaying Dialogs From Background Threads


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:


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)



I 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?


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.


The fix to this is pretty simple. Just test if the Activity is going through its finishing phase before displaying the Dialog:


private Handler myHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case DISPLAY_DLG:
if (!isFinishing()) {
showDialog(MY_DIALOG);
}
break;
}
}
};
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章