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;
}
}
};
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章