java.lang.IllegalArgumentException: View not attached to window manager 参数异常:视图未附加到窗口管理器

159
我有一个启动AsyncTask并显示进度对话框的活动。该活动被声明为不会因旋转或键盘滑动而重新创建。
    <activity android:name=".MyActivity" 
              android:label="@string/app_name"
              android:configChanges="keyboardHidden|orientation"
              >
        <intent-filter>
        </intent-filter>
    </activity>

任务完成后,我关闭对话框,但在一些手机上(框架:1.5、1.6),会抛出这样的错误:

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)

我的代码是:

final Dialog dialog = new AlertDialog.Builder(context)
    .setTitle("Processing...")
    .setCancelable(true)
    .create();

final AsyncTask<MyParams, Object, MyResult> task = new AsyncTask<MyParams, Object, MyResult>() {

    @Override
    protected MyResult doInBackground(MyParams... params) {
        // Long operation goes here
    }

    @Override
    protected void onPostExecute(MyResult result) {
        dialog.dismiss();
        onCompletion(result);
    }
};

task.execute(...);

dialog.setOnCancelListener(new OnCancelListener() {
    @Override
    public void onCancel(DialogInterface arg0) {
        task.cancel(false);
    }
});

dialog.show();
根据我所了解的(来源:http://bend-ing.blogspot.com/2008/11/properly-handle-progress-dialog-in.html)以及在Android源代码中看到的,似乎惟一可能导致该异常的情况是当activity被销毁时。但正如我所提到的,对于基本事件,我禁止重新创建activity。
因此,非常感谢任何建议。

3
这个问题已经有很多答案,如果有任何答案对您有用,请将其选为正确答案。 - Parag Kadam
15个回答

234

有时候在onPostExecute方法中取消对话框并结束活动时,我也会遇到这个错误。我想有时候在对话框成功取消之前活动就已经结束了。

对我有效的简单而有效的解决方案

@Override
protected void onPostExecute(MyResult result) {
    try {
        if ((this.mDialog != null) && this.mDialog.isShowing()) {
            this.mDialog.dismiss();
        }
    } catch (final IllegalArgumentException e) {
        // Handle or log or ignore
    } catch (final Exception e) {
        // Handle or log or ignore
    } finally {
        this.mDialog = null;
    }  
}

46
简单的解决方案?是的。有效吗?在这种情况下或许吧。我会推荐它吗?不!不要像那样吞掉所有的异常!我甚至不会捕获IllegalArgumentException,而是寻找另一个解决方案。 - Simon Forsberg
6
通常情况下,空的 try-catch 结构是不好的设计……尽管有时候这么做可能是正确的选择。 - Thomas
3
根据你的回答,你建议使用catch type Exception。然而,这是Google的一种不好的做法。你可以在这里阅读相关内容:不要捕获通用异常(Don't Catch Generic Exception) - Yaniv
18
我认为这是一种有效的解决方法。一般情况下,我们不应该这样做,但由于Android Framework没有为我们提供任何简单的检查,所以我们必须使用不寻常的方式。此外,如果对话框的isShowing()调用按照我们的预期工作,我们就不需要这种hack。 - SXC
1
快速修复,直到找到更好的解决方案。 - Rohit Tigga
显示剩余11条评论

18

这是我“防弹”解决方案,汇集了我在这个话题上找到的所有好答案(感谢@Damjan和@Kachi)。在这里,只有在所有其他检测方法都失败时,异常才会被吞噬。在我的情况下,我需要自动关闭对话框,这是保护应用程序免于崩溃的唯一方法。 我希望它能帮助你!如果您有任何意见或更好的解决方案,请投票并留言。谢谢!

public void dismissWithCheck(Dialog dialog) {
        if (dialog != null) {
            if (dialog.isShowing()) {

                //get the Context object that was used to great the dialog
                Context context = ((ContextWrapper) dialog.getContext()).getBaseContext();

                // if the Context used here was an activity AND it hasn't been finished or destroyed
                // then dismiss it
                if (context instanceof Activity) {

                    // Api >=17
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                        if (!((Activity) context).isFinishing() && !((Activity) context).isDestroyed()) {
                            dismissWithTryCatch(dialog);
                        }
                    } else {

                        // Api < 17. Unfortunately cannot check for isDestroyed()
                        if (!((Activity) context).isFinishing()) {
                            dismissWithTryCatch(dialog);
                        }
                    }
                } else
                    // if the Context used wasn't an Activity, then dismiss it too
                    dismissWithTryCatch(dialog);
            }
            dialog = null;
        }
    }

    public void dismissWithTryCatch(Dialog dialog) {
        try {
            dialog.dismiss();
        } catch (final IllegalArgumentException e) {
            // Do nothing.
        } catch (final Exception e) {
            // Do nothing.
        } finally {
            dialog = null;
        }
    }

1
非常好的解决方案!将“dialog = null”设置无效。而“StatusEventDialog”应该只读取“Dialog”。 - hgoebl
1
StatusEventDialog 应该改为 dialog。 - Sreekanth Karumanaghat
这个答案应该被接受,处理得非常好。 - blueware
我理解你想要“正确”地处理,只在 isDestroyed() 不可用时使用 try/catch,但实际上,总是使用 try/catch 是否会更加实用呢? - zundi

13
我也许有一个解决方法。
我遇到同样的问题,即通过 AsyncTask 从文件系统中加载大量项目到 ListView 中。在 onPreExecute() 中启动了一个 ProgressDialog,然后在 onPostExecute()onCancelled()(通过AsyncTask.cancel()显式取消任务时调用)中都使用 .cancel() 关闭它。
当我在 AsyncTaskonCancelled() 方法中关闭对话框时,出现了相同的 "java.lang.IllegalArgumentException: View not attached to window manager" 错误(我在优秀的 Shelves 应用程序 中看到过这种方法)。
解决方法是在 AsyncTask 中创建一个公共字段,其中包含 ProgressDialog
public ProgressDialog mDialog;

然后,在 onDestroy() 中,当我取消我的 AsyncTask 时,我也可以通过以下方式关闭相关对话框:

那么,在 onDestroy() 中,当我取消我的 AsyncTask 时,我也可以通过以下代码关闭相关的对话框:

AsyncTask.mDialog.cancel();

调用AsyncTask.cancel()确实会触发AsyncTask中的onCancelled()方法,但由于在调用该方法时视图已经被销毁,因此取消对话框失败。


我发现UserTask的实现非常出色,正如@Paul所提到的那样。源代码在这里: http://code.google.com/p/shelves/source/browse/trunk/Shelves/src/org/curiouscreature/android/shelves/util/UserTask.java - Evi Song
使用案例可以在同一项目中找到:http://code.google.com/p/shelves/source/browse/trunk/Shelves/src/org/curiouscreature/android/shelves/activity/ShelvesActivity.java#613 - Evi Song

13

这是解决该问题的正确方法:

public void hideProgress() {
    if(mProgressDialog != null) {
        if(mProgressDialog.isShowing()) { //check if dialog is showing.

            //get the Context object that was used to great the dialog
            Context context = ((ContextWrapper)mProgressDialog.getContext()).getBaseContext();

            //if the Context used here was an activity AND it hasn't been finished or destroyed
            //then dismiss it
            if(context instanceof Activity) { 
                if(!((Activity)context).isFinishing() && !((Activity)context).isDestroyed()) 
                    mProgressDialog.dismiss();
            } else //if the Context used wasnt an Activity, then dismiss it too
                mProgressDialog.dismiss();
        }
        mProgressDialog = null;
    }
}

这个解决方案不是盲目地捕获所有异常,而是解决问题的根源:当用于初始化对话框的活动已经完成时,尝试关闭对话框。在我的运行KitKat的Nexus 4上工作,但应该适用于所有版本的Android。


3
"isDestroyed" 需要 API 17+。 - Androiderson
为什么需要将mProgressDialog设置为null?这与内存泄漏有关吗?您能解释一下吗? - Pawan
1
@Pawan,这是我这边的一个实现细节。它不是必需的,只是这个类中函数工作的方式。在进度对话框隐藏后,我将其设置为null。当用户想要显示另一个进度对话框时,会实例化一个新的对象。 - Kachi
肯定需要使用!((Activity)context).isFinishing(),谢谢! :) - Daniel Krzyczkowski

7

我同意'Damjan'的观点。
如果你使用了很多对话框,应该在onDestroy()或onStop()中关闭所有对话框。
这样就可以减少'java.lang.IllegalArgumentException: View not attached to window manager'异常发生的频率。

@Override
protected void onDestroy() {
    Log.d(TAG, "called onDestroy");
    mDialog.dismiss();
    super.onDestroy();
}



但是稍微超出了一点点...
为了更清楚,您可以在onDestroy调用后防止显示任何对话框。
我不使用如下所示的方式。但它很清晰。

private boolean mIsDestroyed = false;

private void showDialog() {
    closeDialog();

    if (mIsDestroyed) {
        Log.d(TAG, "called onDestroy() already.");
        return;
    }

    mDialog = new AlertDialog(this)
        .setTitle("title")
        .setMessage("This is DialogTest")
        .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
            }
        })
        .create();
    mDialog.show();
}

private void closeDialog() {
    if (mDialog != null) {
        mDialog.dismiss();
    }
}

@Override
protected void onDestroy() {
    Log.d(TAG, "called onDestroy");
    mIsDestroyed = true;
    closeDialog();
    super.onDestroy();
}


good luck!


我总是更喜欢避免空的 catch 块。这样做值得一试,但由于这个错误很难产生 - 只有时间才能告诉我们它是否真正起作用。无论如何,还是谢谢。 - Dror Fichman
空的catch块是什么?我不使用try/catch。一个mIsDestroyed变量正在超出工作范围。但是,如果您编写代码,在其他线程中进行一些工作后显示对话框,则可能需要此变量。当其他线程正在工作时,如果活动已完成,则可以查看此异常。 - Hogun
我曾经遇到过同样的问题,当我添加了 @Override public void onPause(){ if(dialog != null) dialog.dismiss(); super.onPause(); }之后,我就再也没有出现这个错误了...所以我认为这和你的答案是一样的,而且真的很有用。 - Chris Sim
@ChrisSim 你好!onPause() 和 onDestroy() 有区别。当 Activity 进入 onPause 状态时,Dialog 会被关闭。而当你执行应用程序时,Dialog 不会显示出来。是否需要它呢? - Hogun
@Hogun 当然,我的意思是相同的,我在暂停时关闭对话框而不是在销毁时关闭,因为我需要它在暂停时而不是在销毁时。其次,我只在它不为空时关闭它。感谢您为其他人解释这个问题。 - Chris Sim
有一个很好的答案针对类似问题提供了更多的背景信息和相似的解决方案。https://dev59.com/FGAh5IYBdhLWcg3wBPXY#23586127 - Tad

4
请使用这个。
if(_dialog!=null && _dialog.isShowing())
_dialog.dismiss();

2
这个解决方案与@Damjan提出的几乎相同。 - Yury
28
这还不够,尽管进行了这个检查,但是 IllegalArgumentException 仍然会发生。 - Murphy
我尝试了同样的解决方案,但仍然不确定它是否有效。唯一的区别是,我嵌套了两个if语句,以确保第二部分的.isShowing()在为null时不会被评估。 - Nick
2
这还不够。 - trante
1
@Nick:在这种情况下,没有必要嵌套多个if。Java的&&运算符具有惰性求值(也称为短路),这意味着如果第一个操作数求值为false(这意味着&&的结果始终为false),则不会评估第二个操作数。同样,如果第一个操作数求值为true,则||不会评估其第二个操作数。 注意:&|运算符没有这种行为,因此始终会评估两个操作数。 - Matthias

3

我有同样的问题,你可以通过以下方式解决:

@Override
protected void onPostExecute(MyResult result) {
    try {
        if ((this.mDialog != null) && this.mDialog.isShowing()) {
            this.mDialog.dismiss();
        }
    } catch (final IllegalArgumentException e) {
        // Handle or log or ignore
    } catch (final Exception e) {
        // Handle or log or ignore
    } finally {
        this.mDialog = null;
    }  
}

2

在大多数情况下对我有效的方法是验证Activity是否已经结束。

if (!mActivity.isFinishing()) {
    dialog.dismiss();
}

2
我认为你的代码是正确的,与其他答案所建议的不同。onPostExecute将在UI线程上运行。这就是AsyncTask的全部意义 - 你不必担心调用runOnUiThread或处理处理程序。此外,根据文档,dismiss()可以从任何线程安全地调用(不确定他们是否将其作为例外)。也许这是一个时间问题,dialog.dismiss()在活动不再显示后被调用?如果您注释掉setOnCancelListener并在后台任务正在运行时退出活动会发生什么呢?然后,您的onPostExecute将尝试关闭一个已经关闭的对话框。如果应用程序崩溃,则可以检查对话框是否打开,然后再解除它。我遇到了完全相同的问题,所以我将在代码中尝试它。

我还查看了dimiss()代码,确实可以从任何线程安全地调用。顺便说一下,我在测试中遇到了问题,因为这个问题发生在用户手机上,而我自己却无法重现 :-( 所以只能通过分析代码来尝试弄清楚。根据时间顺序来看,我在考虑这个,但无法想象出在Dialog关闭之前Activity如何被关闭的情况。如果按下BACK键,那么会先取消Dialog。并且在清单文件中禁止了自动重建活动,但可能仍然可以通过某种方式重新创建?如果你找到了什么,请告诉我! - alex2k8

2

alex,

我可能错了,但我怀疑“在野外”的多个手机存在一个错误,导致它们在标记为静态定向的应用程序中切换方向。这在我的个人手机上经常发生,在我们组使用的许多测试手机上也是如此(包括droid、n1、g1和hero)。通常,一个标记为静态定向的应用程序(可能是垂直的)会使用水平方向布置自己一两秒钟,然后立即切换回来。最终结果是,即使您不希望应用程序切换方向,您也必须准备好它可能会这样做。我不知道在什么确切的条件下可以重现这种行为,我也不知道它是否特定于Android的某个版本。我只知道我已经看到它发生了很多次:(

我建议使用您发布的链接中提供的解决方案(在链接中提供),该方案建议覆盖Activity onCreateDialog方法,并让Android操作系统管理对话框的生命周期。看起来即使您不希望活动切换方向,但它仍然在某个地方切换方向。您可以尝试找到一种始终防止方向切换的方法,但我想告诉您的是,我个人认为目前市场上所有当前的Android手机都没有一种完美无缺的方法适用。

1
你可以防止设备切换方向,但是还有许多其他的配置更改会破坏/重新创建你的Activity - 其中一个常见的是滑动键盘。 - MaximumGoat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接