Android视图未附加到窗口管理器

115
我遇到以下异常:
java.lang.IllegalArgumentException: View not attached to window manager
at android.view.WindowManagerImpl.findViewLocked(WindowManagerImpl.java:355)
at android.view.WindowManagerImpl.updateViewLayout(WindowManagerImpl.java:191)
at android.view.Window$LocalWindowManager.updateViewLayout(Window.java:428)
at android.app.Dialog.onWindowAttributesChanged(Dialog.java:596)
at android.view.Window.setDefaultWindowFormat(Window.java:1013)
at com.android.internal.policy.impl.PhoneWindow.access$700(PhoneWindow.java:86)
at com.android.internal.policy.impl.PhoneWindow$DecorView.drawableChanged(PhoneWindow.java:1951)
at com.android.internal.policy.impl.PhoneWindow$DecorView.fitSystemWindows(PhoneWindow.java:1889)
at android.view.ViewRoot.performTraversals(ViewRoot.java:727)
at android.view.ViewRoot.handleMessage(ViewRoot.java:1633)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:123)
at android.app.ActivityThread.main(ActivityThread.java:4338)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:521)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:860)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:618)
at dalvik.system.NativeStart.main(Native Method)

我已经搜索过并发现它与弹出窗口和屏幕旋转有关,但没有涉及到我的代码。

问题如下:

  1. 是否有一种方法可以精确找出此问题发生的时间?
  2. 除了屏幕旋转之外,还有其他事件或操作会触发此错误吗?
  3. 我该如何避免这种情况发生?

1
看看您是否能够解释清楚清单中描述的活动以及出现错误时屏幕上显示的活动。尝试将您的问题分解成最小的测试用例。 - Josh Lee
也许你正在尝试在调用View#onAttachedToWindow()之前修改视图? - Alex Lockwood
14个回答

163

我遇到了一个问题,在屏幕方向改变时,活动在AsyncTask与进度对话框完成之前就结束了。我似乎通过在onPause()中将对话框设置为null并在AsyncTask中检查这一点来解决了这个问题。

@Override
public void onPause() {
    super.onPause();

    if ((mDialog != null) && mDialog.isShowing())
        mDialog.dismiss();
    mDialog = null;
}

在我的AsyncTask中...

protected void onPreExecute() {
    mDialog = ProgressDialog.show(mContext, "", "Saving changes...",
            true);
}

protected void onPostExecute(Object result) {
   if ((mDialog != null) && mDialog.isShowing()) { 
        mDialog.dismiss();
   }
}

13
@YekhezkelYovel,AsyncTask在一个线程中运行,该线程在活动重新启动时仍然存在,并且可能持有对已死亡Activity及其视图的引用(这实际上是一种内存泄漏,尽管可能是短期的-取决于任务完成所需时间)。以上解决方案可以正常工作,如果您愿意接受短期内存泄漏。推荐的方法是在暂停Activity时取消任何正在运行的AsyncTask。 - Stevie

9

在解决这个问题的过程中,我最终采用了以下方法:

/**
 * Dismiss {@link ProgressDialog} with check for nullability and SDK version
 *
 * @param dialog instance of {@link ProgressDialog} to dismiss
 */
public void dismissProgressDialog(ProgressDialog dialog) {
    if (dialog != null && 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 (!((Activity) context).isFinishing() {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                        if (!((Activity) context).isDestroyed()) {
                            dismissWithExceptionHandling(dialog);
                        }
                    } else { 
                        // Api < 17. Unfortunately cannot check for isDestroyed()
                        dismissWithExceptionHandling(dialog);
                    }
                }
            } else
                // if the Context used wasn't an Activity, then dismiss it too
                dismissWithExceptionHandling(dialog);
        }
        dialog = null;
    }
}

/**
 * Dismiss {@link ProgressDialog} with try catch
 *
 * @param dialog instance of {@link ProgressDialog} to dismiss
 */
public void dismissWithExceptionHandling(ProgressDialog dialog) {
    try {
        dialog.dismiss();
    } catch (final IllegalArgumentException e) {
        // Do nothing.
    } catch (final Exception e) {
        // Do nothing.
    } finally {
        dialog = null;
    }
}

有时候,如果没有更好的办法解决这个问题,良好的异常处理会起到很好的作用。

6

如果您有一个Activity对象,您可以使用isDestroyed()方法:

Activity activity;

// ...

if (!activity.isDestroyed()) {
    // ...
}

如果你有一个非匿名的AsyncTask子类,在不同的地方使用它,这是很好的。


3
我正在使用一个自定义静态类,它可以显示和隐藏对话框。这个类不仅被一个活动使用,而是被其他活动使用。
现在你描述的问题也出现在我身上了,我熬了一夜来寻找解决方法...
最后,我向您呈现解决方案!
如果您想要显示或关闭对话框,但不知道哪个活动启动了对话框以便进行操作,则可使用以下代码...
 static class CustomDialog{

     public static void initDialog(){
         ...
         //init code
         ...
     }

      public static void showDialog(){
         ...
         //init code for show dialog
         ...
     }

     /****This is your Dismiss dialog code :D*******/
     public static void dismissProgressDialog(Context context) {                
            //Can't touch other View of other Activiy..
            //https://dev59.com/Z3_aa4cB1Zd3GeqP2FpO
            if ( (progressdialog != null) && progressdialog.isShowing()) {

                //is it the same context from the caller ?
                Log.w("ProgressDIalog dismiss", "the dialog is from"+progressdialog.getContext());

                Class caller_context= context.getClass();
                Activity call_Act = (Activity)context;
                Class progress_context= progressdialog.getContext().getClass();

                Boolean is_act= ( (progressdialog.getContext()) instanceof  Activity )?true:false;
                Boolean is_ctw= ( (progressdialog.getContext()) instanceof  ContextThemeWrapper )?true:false;

                if (is_ctw) {
                    ContextThemeWrapper cthw=(ContextThemeWrapper) progressdialog.getContext();
                    Boolean is_same_acivity_with_Caller= ((Activity)(cthw).getBaseContext() ==  call_Act )?true:false;

                    if (is_same_acivity_with_Caller){
                        progressdialog.dismiss();
                        progressdialog = null;
                    }
                    else {
                        Log.e("ProgressDIalog dismiss", "the dialog is NOT from the same context! Can't touch.."+((Activity)(cthw).getBaseContext()).getClass());
                        progressdialog = null;
                    }
                }


            }
        } 

 }

2

以上解决方案对我无效。所以我做的是将ProgressDialog作为全局变量,然后将其添加到我的活动中。

@Override
    protected void onDestroy() {
        if (progressDialog != null && progressDialog.isShowing())
            progressDialog.dismiss();
        super.onDestroy();
    }

这样,如果活动被销毁,ProgressDialog也将被销毁。


0

我认为您试图在销毁/完成活动后显示/关闭对话框。这就是您可以处理此场景的方式。

if (dialog?.window?.decorView?.isShown!!) {
    dialog?.dismiss()
    finish()
}

0
我在该活动的清单中添加了以下内容。
android:configChanges="keyboardHidden|orientation|screenLayout"

19
这不是一个解决方案:该系统也可能因其他原因破坏您的活动。 - Roel
1
你能解释一下你的答案吗? - Leebeedev

0
根据windowManager的代码(链接here),当您尝试更新的视图(可能属于对话框,但不一定)不再附加到窗口的实际根目录时,就会发生这种情况。
正如其他人建议的那样,在执行有特殊操作的对话框之前,应检查活动的状态。
这是相关代码,它是问题的原因(从Android源代码中复制):
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
    if (!(params instanceof WindowManager.LayoutParams)) {
        throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
    }

    final WindowManager.LayoutParams wparams
            = (WindowManager.LayoutParams)params;

    view.setLayoutParams(wparams);

    synchronized (this) {
        int index = findViewLocked(view, true);
        ViewRootImpl root = mRoots[index];
        mParams[index] = wparams;
        root.setLayoutParams(wparams, false);
    }
}

private int findViewLocked(View view, boolean required) {
        synchronized (this) {
            final int count = mViews != null ? mViews.length : 0;
            for (int i=0; i<count; i++) {
                if (mViews[i] == view) {
                    return i;
                }
            }
            if (required) {
                throw new IllegalArgumentException(
                        "View not attached to window manager");
            }
            return -1;
        }
    }

我写的代码就是Google所做的。我写这个代码是为了展示这个问题的原因。 - android developer

0

或者,您可以简单地添加

protected void onPreExecute() {
    mDialog = ProgressDialog.show(mContext, "", "Saving changes...", true, false);
}

这将使ProgressDialog不可取消


0

针对问题1:

考虑到错误信息似乎没有指出哪行代码出了问题,您可以通过使用断点来找出问题所在。断点会在程序执行到特定的代码行时暂停程序的执行。通过在关键位置添加断点,您可以确定哪一行代码导致了程序崩溃。例如,如果您的程序在 setContentView() 行崩溃,可以在那里设置一个断点。当程序运行时,它会在运行到那一行之前暂停。如果然后恢复程序运行使程序在达到下一个断点之前崩溃,那么您就知道导致程序崩溃的代码行是在这两个断点之间。

如果您正在使用 Eclipse,则添加断点非常容易。只需右键单击代码左侧的边距,并选择“切换断点”。然后,需要在调试模式下运行应用程序,该模式下的按钮看起来像正常运行按钮旁边的绿色昆虫。当程序命中断点时,Eclipse 将切换到调试视图并显示等待的代码行。要重新开始程序运行,请查找“继续”按钮,其外观与常规的“播放”按钮类似,但左侧有一个竖杠。

你也可以使用Log.d("My application", "Some information here that tells you where the log line is")来填充你的应用程序,这样就可以在Eclipse的LogCat窗口中发布消息了。如果你找不到那个窗口,可以通过Window -> Show View -> Other... -> Android -> LogCat打开它。
希望能对你有所帮助!

7
问题发生在客户的手机上,所以我没有办法进行调试。而且这个问题是一直存在的,不是偶尔发生的,所以我不知道如何追踪它。 - Daniel Benedykt
如果你能拿到一部手机,你仍然可以获取调试信息。在菜单->设置->应用程序->开发中,有一个USB调试选项。如果启用了该选项,则可以将手机插入电脑,LogCat会捕获所有正常的调试行。除此之外...嗯...间歇性问题可能是由于程序状态而导致的。我想你得使用该程序来尝试重现这个问题。 - Steve Haley
4
就像我之前所说,问题出现在客户的手机上,而我无法访问该手机。客户可能在另一个大陆上 :) - Daniel Benedykt
市场上有一些应用程序会通过电子邮件发送它们的Logcat信息。 - Tim Green
1
只是让你知道,你可以通过按下数字键盘键9来旋转模拟器。 - Rob

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