之前在學習Fragment和總結Android異步操作的時候會在很多blog中看到對Configuration Change的討論,以前做的項目都是固定豎屏的,所以對橫豎屏切換以及橫豎屏切換對程序有什么影響都沒什么了解。見到的次數多了,總是掠過去心理總覺得不踏實,最終還是好好看了些介紹Congifuration Change的blog,在此做個梳理也不枉花了那么多時間。有疏漏和描述不准確的地方懇請指正。
前言
在研究Configuration Change之前我主要的疑問:
- 橫豎屏切換對布局有影響嗎,要做什么處理嗎?
- 屏幕旋轉的話為什么要保存數據?
- 啟動一個線程(worker thread或者AsyncTask)來跑一個耗時任務,此時旋轉屏幕會對線程有什么影響嗎?
- 異步操作過程會顯示進度對話框,旋轉屏幕造成程序終止的原因及解決方法?
- 在AndroidManifest.xml中通過配置android:configuration的方法來防止Activity被銷毀並重建為什么不被推薦,這種方法有哪些缺點?
- 推薦使用
Fragment的setRetainInstance(true)來處理配置變化時保存對象,具體怎么實現?
屏幕方向是設備配置的一個屬性,屏幕旋轉是影響配置變化的因素之一,在項目中最常見。在對Configuration Change有個全面認識后,這些問題都會迎刃而解。
由一道網上總結的Android測試題引發的測試
對Configuration Change的第一印象還是看網上總結的Andorid面試題里有問到:
問題:橫豎屏切換時Activity的生命周期?
答案:
1、不設置Activity的android:configChanges時,切屏會重新調用各個生命周期,切橫屏時會執行一次,切豎屏時會執行兩次
2、設置Activity的android:configChanges=”orientation”時,切屏還是會重新調用各個生命周期,切橫、豎屏時只會執行一次
3、設置Activity的android:configChanges=”orientation|keyboardHidden”時,切屏不會重新調用各個生命周期,只會執行onConfigurationChanged方法
但是經過測試后結果表明並不都和‘答案’一致:
測試環境:在HTC t329d 4.1和模擬器2.2.3上的測試結果:
1、和答案中的第1點不一致。不設置Activity的android:configChanges時,不管切橫屏還是切豎屏,都只會重新調用生命周期一次。
2、和‘答案’中第2點一致
3、 和答案中的第3點不一致。 設置Activity的 android:configChanges=”orientation|keyboardHidden”時,在Android 3.2(API Level 13)之前,切屏還是會重新調用各個生命周期,不會執行onConfigurationChanged()方法。在Android 3.2之后必須在configChanges中添加screenSize才不會在切屏時重新調用各個生命周期。並執行onConfigurationChanged()方法。
從測試結果和‘答案’的不一致告訴me,對於所謂的'答案'最好親測比較靠譜,而且對於給答案的人最好指明下測試環境,否則測試結果不同也無處對照。全面透徹盡可能多地去覆蓋有關Configuration Change的知識。其實對於第一點,切橫屏還是豎屏導致Activity重建的次數並不重要,重要的是它被重建了以及重建會引發什么問題。
Configuration Change概述
Configuration 這個類描述了設備的所有配置信息,這些配置信息會影響到應用程序檢索的資源。包括了用戶指定的選項(locale和scaling)也包括設備本身配置(例如input modes,screen size and screen orientation).可以在該類里查看所有影響Configuration Change 的屬性。
橫豎屏切換是我們最常見的影響配置變化的因素,還有很多其他影響配置的因素有語言的更改(例如中英文切換)、鍵盤的可用性(這個沒理解)等
常見的引發Configuration Change的屬性:
橫豎屏切換:android:configChanges="orientation"
鍵盤可用性:android:configChanges="keyboardHidden"
屏幕大小變化:android:configChanges="screenSize"
語言的更改:android:configChanges="locale"
在程序運行時,如果發生Configuration Change會導致當前的Activity被銷毀並重新創建 ,即先調用onDestroy緊接着調用onCreate()方法。 重建的目的是為了讓應用程序通過自動加載可替代資源來適應新的配置。
Configuration Change引發的問題
當程序運行時, 設備配置的改變會導致當前Activity被銷毀並重新創建 。
在Activity被銷毀之前我們需要保存當前的數據以防Activity重建后數據丟失。例如界面中用戶選擇了checkbox和radiobutton選項或者通過網絡請求顯示在界面上的數據在屏幕旋轉后Activity被destroy-recreate,這些控件上被選擇的狀態和界面上的數據都會消失。
再比如當進入某個Activity時加載頁面進行網絡請求,此時旋轉屏幕會重新創建網絡連接請求,這樣的用戶體驗非常不好。而且常見的一個問題是如果伴隨異步操作顯示一個progressDialog的話,異步任務未完成去旋轉屏幕,程序會因為 Activity has leaked window 而 終止。而當old Activity被銷毀后,線程執行完畢后還是會把結果返回給old Activity而非新的Activity,而且新的Activity如果又觸發了后台任務(在onCreate()中會啟動線程),就又會去啟動一個子線程,消耗可用的資源。
下面通過一個例子來看看橫豎屏切換引發的以上問題:
- 異步操作結束后旋轉屏幕,界面數據丟失
- 顯示進度對話框的異步操作,未結束時旋轉屏幕,程序終止
該示例,通過點擊屏幕按鈕啟動一個異步操作(模擬執行耗時任務),同時顯示一個進度對話框。當異步操作執行完畢后更新界 面,並取消進度對話框。在本節最后可查看代碼。
1. 異步操作結束后旋轉屏幕,界面數據丟失
2. 異步操作未結束旋轉屏幕,程序終止
log打印出的錯誤信息:
04-14 21:34:10.192 26254-26254/com.aliao.myandroiddemo E/WindowManager﹕ Activity com.aliao.myandroiddemo.view.handler.TestHandlerActivity has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView@4208a548 that was originally added here android.view.WindowLeaked: Activity com.aliao.myandroiddemo.view.handler.TestHandlerActivity has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView@4208a548 that was originally added here at android.view.ViewRootImpl.<init>(ViewRootImpl.java:415) at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:322) at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:234) at android.view.WindowManagerImpl$CompatModeWrapper.addView(WindowManagerImpl.java:153) at android.view.Window$LocalWindowManager.addView(Window.java:557) at android.app.Dialog.show(Dialog.java:277) at android.app.ProgressDialog.show(ProgressDialog.java:116) at android.app.ProgressDialog.show(ProgressDialog.java:104) at com.aliao.myandroiddemo.view.handler.TestHandlerActivity.excuteLongTimeOperation(TestHandlerActivity.java:60) at com.aliao.myandroiddemo.view.handler.TestHandlerActivity.onClick(TestHandlerActivity.java:51) at android.view.View.performClick(View.java:4191) at android.view.View$PerformClick.run(View.java:17229) at android.os.Handler.handleCallback(Handler.java:615) at android.os.Handler.dispatchMessage(Handler.java:92) at android.os.Looper.loop(Looper.java:137) at android.app.ActivityThread.main(ActivityThread.java:4963) at java.lang.reflect.Method.invokeNative(Native Method) at java.lang.reflect.Method.invoke(Method.java:511) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1038) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:805) at dalvik.system.NativeStart.main(Native Method) 04-14 21:34:11.692 483-635/? E/Watchdog﹕ !@Sync 3825 04-14 21:34:12.192 142-142/? E/SMD﹕ DCD ON 04-14 21:34:12.502 26254-26254/com.aliao.myandroiddemo E/AndroidRuntime﹕ FATAL EXCEPTION: main java.lang.IllegalArgumentException: View not attached to window manager at android.view.WindowManagerImpl.findViewLocked(WindowManagerImpl.java:696) at android.view.WindowManagerImpl.removeView(WindowManagerImpl.java:379) at android.view.WindowManagerImpl$CompatModeWrapper.removeView(WindowManagerImpl.java:164) at android.app.Dialog.dismissDialog(Dialog.java:319) at android.app.Dialog.dismiss(Dialog.java:302) at com.aliao.myandroiddemo.view.handler.TestHandlerActivity$1.handleMessage(TestHandlerActivity.java:87) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:137) at android.app.ActivityThread.main(ActivityThread.java:4963) at java.lang.reflect.Method.invokeNative(Native Method) at java.lang.reflect.Method.invoke(Method.java:511) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1038) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:805) at dalvik.system.NativeStart.main(Native Method)
該示例的代碼:
res/layout/activity_handler.xml——TestHandlerActivity的布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="通過點擊按鈕來啟動一個線程模擬運行一個網絡耗時操作,獲取新聞詳情並顯示在按鈕下面" android:textSize="16sp"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="獲取MH370航班最新新聞動態" android:textSize="16sp" android:id="@+id/btn_createthread" android:layout_gravity="center_horizontal" /> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:textAppearance="?android:attr/textAppearanceLarge" android:textColor="@android:color/holo_green_dark" android:textSize="16sp" android:id="@+id/tv_showsth" android:layout_marginTop="10dp"/> </LinearLayout>
TestHandlerActivity——進行異步操作,獲取數據並更新界面
package com.aliao.myandroiddemo.view.handler;
import android.app.Activity; import android.app.ProgressDialog; import android.content.res.Configuration; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.TextView; import com.aliao.myandroiddemo.R; import com.aliao.myandroiddemo.utils.ThreadUtil; /** * Created by liaolishuang on 14-4-9. */ public class TestHandlerActivity extends Activity implements View.OnClickListener{ private final String TAG = "testhandler"; private TextView showNewsInfoTxt; private ProgressDialog progressDialog; private String newsInfo; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_handler); //打印當前線程的部分信息 ThreadUtil.logThreadSignature(); Button anrBtn = (Button) findViewById(R.id.btn_createthread); anrBtn.setOnClickListener(this); showNewsInfoTxt = (TextView) findViewById(R.id.tv_showsth); if(getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE){ Log.i(TAG, "----onCreate - landscape---"); }else if(getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){ Log.i(TAG, "----onCreate - portrait ---"); } } @Override public void onClick(View view) { switch (view.getId()){ case R.id.btn_createthread: excuteLongTimeOperation(); break; } } /** * 點擊按鈕,創建子線程,並顯示一個進度對話框 */ private void excuteLongTimeOperation() { progressDialog = ProgressDialog.show(TestHandlerActivity.this,"Load Info","Loading...",true,true); Thread workerThread = new Thread(new MyNewThread()); workerThread.start(); } class MyNewThread extends Thread{ @Override public void run() { //打印子線程的部分信息 ThreadUtil.logThreadSignature(); //模擬執行耗時操作 ThreadUtil.sleepForInSecs(5); newsInfo = "#搜尋馬航370#【澳聯合協調中心今日記者會要點】1.發現油跡的地點距離信號發現地很近,油跡來源需進一步調查。2.黑匣子一般只有30天壽命,最多40天,今天已經是第38天了,但仍有可能收到信號"; Message message = handler.obtainMessage(); Bundle bundle = new Bundle(); bundle.putString("message",newsInfo); message.setData(bundle); handler.sendMessage(message); } } /** * 以匿名類的形式創建handler */ private Handler handler = new Handler(){ @Override public void handleMessage(Message msg) { progressDialog.dismiss(); //更新界面中TextView中的內容 refreshNewsInfo(msg.getData().getString("message")); } }; /** * 更新界面內容 * @param newsInfo */ private void refreshNewsInfo(String newsInfo) { showNewsInfoTxt.setText(newsInfo); } /** * 只有在AndroidManifest.xml中對該Activity設置android:configChanges,該方法才會被回調 * @param newConfig */ @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); Log.i(TAG, "----onConfigurationChanged---"); } @Override protected void onDestroy() { super.onDestroy(); Log.i(TAG, "====onDestroy===="); } @Override protected void onStart() { super.onStart();