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
-
AndroidRuntime: FATAL EXCEPTION: main
-
AndroidRuntime: java.lang.IllegalArgumentException: View not attached to window manager
-
AndroidRuntime: at android.view.WindowManagerGlobal.findViewLocked(WindowManagerGlobal.java:385)
-
AndroidRuntime: at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:287)
-
AndroidRuntime: at android.view.WindowManagerImpl.removeView(WindowManagerImpl.java:79)
-
AndroidRuntime: at android.app.Dialog.dismissDialog(Dialog.java:323)
-
AndroidRuntime: at android.app.Dialog.dismiss(Dialog.java:306)
-
AndroidRuntime: at com.android.stk.StkDialogActivity$4.onClick(StkDialogActivity.java:188)
-
AndroidRuntime: at com.android.internal.app.AlertController$ButtonHandler.handleMessage(AlertController.java:169)
-
AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:99)
-
AndroidRuntime: at android.os.Looper.loop(Looper.java:153)
-
AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:5299)
-
AndroidRuntime: at java.lang.reflect.Method.invokeNative(Native Method)
-
AndroidRuntime: at java.lang.reflect.Method.invoke(Method.java:511)
-
AndroidRuntime: at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:833)
-
AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:600)
-
AndroidRuntime: at dalvik.system.NativeStart.main(Native Method)
通過分析PIN碼的解鎖流程知道,並不是PIN碼解釋時的dialog導致的問題。PIN碼解鎖流程另用文檔說明。
仔細查看log後發現有:
[plain] view plain copy
-
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。
這個錯誤的意思是說我們所操作的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
-
onCreate
-
onResume - mbSendResp[false], sim id: 0
-
... ...省略部分
-
onSaveInstanceState
-
onPause, sim id: 0
-
... ...省略部分
-
onDestroy-
-
onCreate
-
... ...省略部分
-
onRestoreInstanceState - [com.android.internal.telephony.cat.TextMessage@41fe7d80]
-
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並沒有找到答案。
可能大家會有疑問,Dialog都沒有看到,就出現錯誤了,怎麼能確定該Dialog當時一定是顯示的呢?繼續在log中搜索我們可以發現:
[plain] view plain copy
-
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
-
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
-
WindowManager: at android.view.ViewRootImpl.<init>(ViewRootImpl.java:409)
-
WindowManager: at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:218)
-
WindowManager: at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)
-
WindowManager: at android.app.Dialog.show(Dialog.java:281)
-
WindowManager: at android.app.AlertDialog$Builder.show(AlertDialog.java:951)
-
WindowManager: at com.android.stk.StkDialogActivity.onCreate(StkDialogActivity.java:192)
-
WindowManager: at android.app.Activity.performCreate(Activity.java:5122)
-
WindowManager: at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1081)
-
WindowManager: at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2270)
-
WindowManager: at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2358)
-
WindowManager: at android.app.ActivityThread.access$600(ActivityThread.java:156)
-
WindowManager: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1340)
-
WindowManager: at android.os.Handler.dispatchMessage(Handler.java:99)
-
WindowManager: at android.os.Looper.loop(Looper.java:153)
-
WindowManager: at android.app.ActivityThread.main(ActivityThread.java:5299)
-
WindowManager: at java.lang.reflect.Method.invokeNative(Native Method)
-
WindowManager: at java.lang.reflect.Method.invoke(Method.java:511)
-
WindowManager: at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:833)
-
WindowManager: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:600)
-
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
-
protected void onCreate(Bundle savedInstanceState) {
-
super.onCreate(savedInstanceState);
-
setContentView(R.layout.activity_main);
-
AlertDialog.Builder info = new Builder(this);
-
info.setTitle("Dialog").setPositiveButton("OK", null).setMessage("This is a Dialog");;
-
info.show();
-
finish();
-
}
直接將此程序run到模擬器或者真機上,查看log我們就能看到leaked window的報錯信息。因此這也證明了前面我們的假設,即StkActivity在被銷燬時,其所依附的Dialog是存在的。
通過以上分析之後我們知道了問題出現的原因,那麼如何解決呢?可以通過以下兩個方面來解決:
在Activity中需要使用對話框,可以使用Activity自帶的回調,比如onCreateDialog(),showDialog(),dimissDialog(),removeDialog()等等。畢竟這些都是Activity自帶的方法,所以用起來更方便,也不用顯示創建和操控Dialog對象,一切都由框架操控,相對來說比較安全。
讓創建的Dialog對象的存活週期跟Activity的生命週期一致,也就是說Dialog的生命週期被限定在Activity的onCreate()和onDestroy()方法之間。