通用Dialog避免IllegalArgumentException

NewDialog.java

避免IllegalArgumentException

關於錯誤分析在下面

package com.youpinwallet.ypw.view;

import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.view.Gravity;
import android.view.WindowManager;
import com.youpinwallet.ypw.R;

/*
 * 白色背景,垂直居中
 * @author Administrator
 *
 */
public class NewDialog extends Dialog {
    Activity mParentActivity;
    public NewDialog(Context context, int theme) {
        super(context, theme);
        mParentActivity= (Activity) context;
    }

    public NewDialog(Context context) {
        super(context);
    }

    /**
     * 彈出自定義ProgressDialog
     *
     * @param context        上下文
     * @param cancelable     是否按返回鍵取消
     * @param cancelListener 按下返回鍵監聽
     * @return
     */
    public static NewDialog create(Context context, boolean cancelable, OnCancelListener cancelListener) {
        NewDialog dialog = new NewDialog(context, R.style.Custom_Progress);
        dialog.setContentView(R.layout.progress_layout);
        // 按返回鍵是否取消
        dialog.setCancelable(cancelable);
        // 監聽返回鍵處理
        dialog.setOnCancelListener(cancelListener);
        // 設置居中
        dialog.getWindow().getAttributes().gravity = Gravity.CENTER;
        WindowManager.LayoutParams lp = dialog.getWindow().getAttributes();
        // 設置背景層透明度
        lp.dimAmount = 0.2f;
        dialog.getWindow().setAttributes(lp);
//    dialog.show();
        return dialog;
    }
    @Override
    public void dismiss()
    {
        // 避免依附界面銷燬跑出的異常java.lang.IllegalArgumentException: View=com.android.internal.policy.PhoneWindow$DecorView
        if (mParentActivity != null && !mParentActivity.isFinishing())
        {
            super.dismiss();    //調用超類對應方法
        }
    }
}

NewDialogUtils.java

package com.qunarjie.qnj.utils;

import android.content.Context;

import com.qunarjie.qnj.view.NewDialog;

//請稍等通用
public class NewDialogUtils {
    private static NewDialog dialog = null;

    public static void setNewDialog(Context context) {
        if (context != null) {
            if (dialog != null) {
                dialog.dismiss();
                dialog = null;
            }
            if (dialog == null) {
                dialog = NewDialog.create(context, true, null);
            }
            dialog.show();
        }

    }

    public static void dismissNewDialog() {
        if (dialog != null && dialog.isShowing()) {
            dialog.dismiss();
            dialog = null;
        }
    }
}

 

R.style.Custom_Progress

styles.xml

<!-- 自定義Progress -->
<style name="Custom_Progress" parent="@android:style/Theme.Dialog">
    <item name="android:windowFrame">@null</item>
    <item name="android:windowIsFloating">true</item>
    <item name="android:windowContentOverlay">@null</item>
    <item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
    <item name="android:windowSoftInputMode">stateUnspecified|adjustPan</item>
    <item name="android:windowBackground">@android:color/transparent</item>
    <item name="android:windowCloseOnTouchOutside">false</item>
    <item name="android:windowNoTitle">true</item>
</style>

 

progress_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:background="@drawable/shape_dialog_white"
    android:gravity="center_horizontal"
    android:orientation="vertical"
    android:paddingBottom="20dp"
    android:paddingLeft="30dp"
    android:paddingRight="30dp"
    android:paddingTop="20dp">

    <ProgressBar
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminateDrawable="@drawable/progressbar" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/mdp_15"
        android:text="請稍等..."
        android:textColor="@color/color_text_dark"
        android:textSize="@dimen/msp_14" />

</LinearLayout>

 

shape_dialog_white.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">

    <corners android:radius="5dp" />

    <solid android:color="@color/white" />

</shape>

 

progressbar.xml

<?xml version="1.0" encoding="utf-8"?>
<animated-rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromDegrees="0"
    android:pivotX="50%"
    android:pivotY="50%"
    android:toDegrees="3600">

    <shape
        android:innerRadiusRatio="3"
        android:shape="ring"
        android:thicknessRatio="8"
        android:useLevel="false">
        <gradient
            android:centerY="0.50"
            android:endColor="#12b7f5"
            android:startColor="#038fef"
            android:type="sweep"
            android:useLevel="false" />
    </shape>

</animated-rotate>

 今天遇到一個很奇特的問題,當用戶設置了PIN碼,在鎖屏界面正常解鎖PIN碼後,進入Launcher時顯示com.Android.phone 已停止運行。一開始猜想會不會是解鎖PIN碼的時候處理導致了Phone進程報錯,通過log分析找到了問題的大概原因:

[plain] view plain copy

 

  1. AndroidRuntime: FATAL EXCEPTION: main  

  2. AndroidRuntime: java.lang.IllegalArgumentException: View not attached to window manager  

  3. AndroidRuntime:     at android.view.WindowManagerGlobal.findViewLocked(WindowManagerGlobal.java:385)  

  4. AndroidRuntime:     at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:287)  

  5. AndroidRuntime:     at android.view.WindowManagerImpl.removeView(WindowManagerImpl.java:79)  

  6. AndroidRuntime:     at android.app.Dialog.dismissDialog(Dialog.java:323)  

  7. AndroidRuntime:     at android.app.Dialog.dismiss(Dialog.java:306)  

  8. AndroidRuntime:     at com.android.stk.StkDialogActivity$4.onClick(StkDialogActivity.java:188)  

  9. AndroidRuntime:     at com.android.internal.app.AlertController$ButtonHandler.handleMessage(AlertController.java:169)  

  10. AndroidRuntime:     at android.os.Handler.dispatchMessage(Handler.java:99)  

  11. AndroidRuntime:     at android.os.Looper.loop(Looper.java:153)  

  12. AndroidRuntime:     at android.app.ActivityThread.main(ActivityThread.java:5299)  

  13. AndroidRuntime:     at java.lang.reflect.Method.invokeNative(Native Method)  

  14. AndroidRuntime:     at java.lang.reflect.Method.invoke(Method.java:511)  

  15. AndroidRuntime:     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:833)  

  16. AndroidRuntime:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:600)  

  17. AndroidRuntime:     at dalvik.system.NativeStart.main(Native Method)  

       通過分析PIN碼的解鎖流程知道,並不是PIN碼解釋時的dialog導致的問題。PIN碼解鎖流程另用文檔說明。

仔細查看log後發現有:

[plain] view plain copy

 

  1. AndroidRuntime:     at com.android.stk.StkDialogActivity$4.onClick(StkDialogActivity.java:188)  

       這是開機識別到SIM卡之後彈出的STK對話框。那爲什麼會報com.android.phone已停止運行呢?

       經過以上分析,我們可以大致猜測是因爲STK引起的問題,既然是STK的問題那爲什麼會報phone的錯誤呢?如果是phone的錯誤那麼從log中應該可以檢索到phone相關的問題,爲什麼沒有呢?那麼我們接下來一一解答這些問題。

 

(1). 爲什麼報錯com.android.phone已停止運行?

       通過查看STK的源碼(單卡MTK的代碼在/mediatec/packages/app/stk1),在其AndroidManifest.xml可以發現:

android:sharedUserId="android.uid.phone"

也就是STK和Phon在一個進程中,因此在報錯的表現上來講就會是com.andorid.phone。

(2). 爲什麼會報View not attached to window manager錯誤?

       這個錯誤的意思是說我們所操作的View沒有被納入window manager的管理。

       我們知道所有的窗口創建和管理都是依附於window manager的,因此Dialog的創建也不例外。Dialog的創建流程通過查看源碼可以知道,在Dialog的構造函數中,創建了一個Window對象,但我們知道Window對象並不是用於顯示的,真正用於顯示的是View對象。因此通過Dialog的show方法構造了一個mDecor的View對象,並最終通過WindowManager的addView()方法顯示Dialog。

       通過查看log信息我們可以看到com.android.stk.StkDialogActivity$4.onClick(StkDialogActivity.java:188)

查看對應的StkDialogActivity代碼後發現,在188行處代碼爲dialog.dismiss();

       在網絡上搜索後發現,多數情況下出現這種錯誤,都是在dismiss Dialog時,發現創建該Dialog的Activity存在而導致的。

比如在界面上顯示一個Dialog,當任務處理結束後再Dismiss Dialog。如果在Dialog顯示期間,該Activity因爲某種原因被殺掉且又重新啓動了,那麼當任務結束時,Dismiss Dialog的時候WindowManager檢查,就會發現該Dialog所屬的Activity已經不存在了(重新啓動了一次,是一個新的Activity),所以會報IllegalArgumentException: View not attached to window manager.

通過以上分析我們可以知道在STK Dialog在執行dismiss方法時,發現啓動它的Activity已經不見了,被殺掉了(現在這個是重新啓動的),所以才報錯出現異常。

(3). 爲什麼STKDialogActivity會被"殺掉"?

       通過跟蹤查看異常log我們可以看到StkDialogActivity的生命週期打印log如下:

[plain] view plain copy

  1. onCreate  

  2. onResume - mbSendResp[false], sim id: 0  

  3. ... ...省略部分  

  4. onSaveInstanceState  

  5. onPause, sim id: 0  

  6. ... ...省略部分  

  7. onDestroy-  

  8. onCreate  

  9. ... ...省略部分  

  10. onRestoreInstanceState - [com.android.internal.telephony.cat.TextMessage@41fe7d80]  

  11. onResume - mbSendResp[false], sim id: 0  

到這裏後就出現了異常錯誤……

       通過log我們可以很清楚的看到該Activity啓動了兩次。在log中我們也看到了onSaveInstanceState方法。

對於onSaveInstanceState方法,在Android SDK裏面有這樣的描述:

Android calls onSaveInstanceState() before the activity becomes vulnerable to being destroyed by the system, but does not bother calling it when the instance is actually being destroyed by a user action (such as pressing the BACK key)

也就說當某個activity變得“容易”被系統銷燬時,該activity的onSaveInstanceState就會被執行,除非該activity是被用戶主動銷燬的,例如當用戶按BACK鍵的時候。

       注意這裏的容易二字,當前Activity並沒有被銷燬,只是系統覺得它有可能會被銷燬因此會執行該方法。在該方法中我們可以保存Activity中的各種數據信息,如果該Activity真的被殺掉而又重新啓動後,可以使用onRestoreInstanceState方法在重新啓動該Activity時,還原我們之前保存的數據信息。onSaveInstanceState方法的調用遵循一個重要原則,即當系統“未經你許可”銷燬了你的Activity時,onSaveInstanceState就會被系統調用,這是系統的責任,因爲它必須要提供一個機會讓你保存你的數據(當然如果你自己不保存,那就沒法恢復了)。

       因此通過以上分析我們可以看到STKDialogActivity的確被殺掉後再次啓動了,但爲什麼會被殺掉,通過log並沒有找到答案。

(4). 在STKDialogActivity被殺掉時,Dialog存在麼?

       可能大家會有疑問,Dialog都沒有看到,就出現錯誤了,怎麼能確定該Dialog當時一定是顯示的呢?繼續在log中搜索我們可以發現:

[plain] view plain copy

  1. WindowManager: Activity com.android.stk.StkDialogActivity has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView{41fea228 V.E..... R.....ID 0,0-1026,433} that was originally added here  

  2. WindowManager: android.view.WindowLeaked: Activity com.android.stk.StkDialogActivity has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView{41fea228 V.E..... R.....ID 0,0-1026,433} that was originally added here  

  3. WindowManager:  at android.view.ViewRootImpl.<init>(ViewRootImpl.java:409)  

  4. WindowManager:  at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:218)  

  5. WindowManager:  at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)  

  6. WindowManager:  at android.app.Dialog.show(Dialog.java:281)  

  7. WindowManager:  at android.app.AlertDialog$Builder.show(AlertDialog.java:951)  

  8. WindowManager:  at com.android.stk.StkDialogActivity.onCreate(StkDialogActivity.java:192)  

  9. WindowManager:  at android.app.Activity.performCreate(Activity.java:5122)  

  10. WindowManager:  at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1081)  

  11. WindowManager:  at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2270)  

  12. WindowManager:  at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2358)  

  13. WindowManager:  at android.app.ActivityThread.access$600(ActivityThread.java:156)  

  14. WindowManager:  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1340)  

  15. WindowManager:  at android.os.Handler.dispatchMessage(Handler.java:99)  

  16. WindowManager:  at android.os.Looper.loop(Looper.java:153)  

  17. WindowManager:  at android.app.ActivityThread.main(ActivityThread.java:5299)  

  18. WindowManager:  at java.lang.reflect.Method.invokeNative(Native Method)  

  19. WindowManager:  at java.lang.reflect.Method.invoke(Method.java:511)  

  20. WindowManager:  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:833)  

  21. WindowManager:  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:600)  

  22. WindowManager:  at dalvik.system.NativeStart.main(Native Method)  

       這裏的WindowManager報錯是什麼意思呢?這段log是WindowManager拋出的error錯誤,當我們的Dialog還沒有dismiss時,如果此時該Activity被銷燬了,那麼就會出現以上錯誤,提示窗口泄漏(leaked window)。這裏自己也做了一個實驗,寫一個demo,在Activity的onCreate方法中顯示一個Dialog,然後直接調用finish方法。代碼大致如下:

[java] view plain copy

  1. protected void onCreate(Bundle savedInstanceState) {  

  2.     super.onCreate(savedInstanceState);  

  3.     setContentView(R.layout.activity_main);  

  4.     AlertDialog.Builder info = new Builder(this);  

  5.     info.setTitle("Dialog").setPositiveButton("OK"null).setMessage("This is a Dialog");;  

  6.     info.show();   

  7.     finish();  

  8. }  

       直接將此程序run到模擬器或者真機上,查看log我們就能看到leaked window的報錯信息。因此這也證明了前面我們的假設,即StkActivity在被銷燬時,其所依附的Dialog是存在的。

(5). 如何解決這個問題呢?

       通過以上分析之後我們知道了問題出現的原因,那麼如何解決呢?可以通過以下兩個方面來解決:

1. 使用Activity自帶的Dialog控制方法

       在Activity中需要使用對話框,可以使用Activity自帶的回調,比如onCreateDialog(),showDialog(),dimissDialog(),removeDialog()等等。畢竟這些都是Activity自帶的方法,所以用起來更方便,也不用顯示創建和操控Dialog對象,一切都由框架操控,相對來說比較安全。

2. 限制Dialog的生命週期

       讓創建的Dialog對象的存活週期跟Activity的生命週期一致,也就是說Dialog的生命週期被限定在Activity的onCreate()和onDestroy()方法之間。

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章