Android Configuration change引發的問題及解決方法(轉)


之前在學習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();   Log.i(TAG, "----onStart---");  }  @Override  protected void onResume() {   super.onResume();   Log.i(TAG, "----onResume---");  }  @Override  protected void onRestart() {   super.onRestart();   Log.i(TAG, "----onRestart---");  }  @Override  protected void onPause() {   super.onPause();   Log.i(TAG, "----onPause---");  }  @Override  protected void onStop() {   super.onStop();   Log.i(TAG, "----onStop---");  }  @Override  protected void onSaveInstanceState(Bundle outState) {   super.onSaveInstanceState(outState);   Log.i(TAG, "----onSaveInstanceState---");  } }

 

解決方案

一、禁止屏幕旋轉

禁止屏幕旋轉,也就無需考慮Configuration Change引發的問題

在AcndroidManifest.xml里設置Activity的screenOrientation屬性為landscape(橫屏)或者portrait(豎屏)

<activity android:name="com.aliao.myandroiddemo.view.handler.TestHandlerActivity" android:label="@string/title_activity_animation" android:screenOrientation="portrait"> </activity>

 

二、避免Activity重建

1.配置andoird:configChanges屬性並回調onConfigurationChanged()手動處理

 

Handling the Configuration Change Yours       里指出如果應用程序不需要在一個特定的configuration change期間更新資源(例如程序在橫屏和豎屏不同屏幕大小下不考慮資源調整),以及有防止activity重啟的性能限制,就可以通過該方法來阻止系統重啟activity。但是Google並不推薦使用該方法。   

 

上一節討論了在某些情況下由於橫豎屏切換導致的一系列問題,引起這些問題的源頭是因為Configuration Change會導致Activity被重建。如果Activity不被銷毀再重建也就沒有所謂的數據丟失,異步操作過程中內存泄露程序終止等問題了。Android提供了一種方法來避免Activity被重建:

在AndroidManifest.xml里通過android:configChanges指定要忽略的配置,例如:

<activity android:name="com.aliao.myandroiddemo.view.handler.TestHandlerActivity" android:configChanges="orientation|keyboardHidden" android:label="@string/title_activity_animation"> </activity>

Caution:需要注意的是,在Android 3.2(API Level 13)開始,橫豎屏切換也會導致"screen size"(Configuraion的一個屬性)改變,所以要在android:configChanges加上該值    android:configChanges="orientation|screenSize",否則當切換屏幕時,activity仍會被重建。   

<activity android:name="com.aliao.myandroiddemo.view.handler.TestHandlerActivity" android:configChanges="orientation|screenSize|keyboardHidden" android:label="@string/title_activity_animation"> </activity>

設置了android:configChanges后,Activity會在配置改變時只回調onConfigurationChanged(Configuration newConfig),不會重新走一遍Activity的生命周期:

啟動TestHandlerActivity顯示界面Activity生命周期為:onCreate->onStart->onResume:

04-14 22:56:18.092  32095-32095/com.aliao.myandroiddemo I/testhandler﹕ ----onCreate - portrait --- 04-14 22:56:18.092 32095-32095/com.aliao.myandroiddemo I/testhandler﹕ ----onStart--- 04-14 22:56:18.092 32095-32095/com.aliao.myandroiddemo I/testhandler﹕ ----onResume---

豎屏切橫屏,只回調了    onConfigurationChanged:   

04-14 22:59:29.912  32095-32095/com.aliao.myandroiddemo I/testhandler﹕ ----onCreate - portrait --- 04-14 22:59:29.912 32095-32095/com.aliao.myandroiddemo I/testhandler﹕ ----onStart--- 04-14 22:59:29.912 32095-32095/com.aliao.myandroiddemo I/testhandler﹕ ----onResume--- 04-14 22:59:33.442 32095-32095/com.aliao.myandroiddemo I/testhandler﹕ ----onConfigurationChanged---

切回豎屏,同樣只回調    onConfigurationChanged:

04-14 23:01:02.232  32095-32095/com.aliao.myandroiddemo I/testhandler﹕ ----onCreate - portrait --- 04-14 23:01:02.232 32095-32095/com.aliao.myandroiddemo I/testhandler﹕ ----onStart--- 04-14 23:01:02.232 32095-32095/com.aliao.myandroiddemo I/testhandler﹕ ----onResume--- 04-14 23:01:04.192 32095-32095/com.aliao.myandroiddemo I/testhandler﹕ ----onConfigurationChanged--- 04-14 23:01:05.492 32095-32095/com.aliao.myandroiddemo I/testhandler﹕ ----onConfigurationChanged---

如果橫豎屏的界面布局不同,可以再res下新建layout-land目錄和layout-port目錄,然后把布局文件扔到這兩個目錄文件中:   

res/layout-land/layout_main.xml

res/layout-port/layout_main.xml

當程序運行的時候會自動判斷當前的屏幕方向去layout里調用對應的布局文件。            我們可以在      onConfigurationChanged方法中對某些資源做調整

@Override
public void onConfigurationChanged(Configuration newConfig) {  super.onConfigurationChanged(newConfig);  // Checks the orientation of the screen  if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {   Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show();  } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){   Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show();  }

如果不管屏幕配置變不變化,程序中使用的資源不會改變,可以不用實現    onConfigurationChanged()回調。

2.不推薦<通過設置      android:configChanges屬性的方法來避免activity被銷毀再重建>的原因

這個方法真的很方便,在運行上面的實例代碼時,完全可以正常運行沒有任何錯誤。但是他也是指某種情形下適用,看了以下不推薦適用的原因后,還是掌握第二種方法比較靠譜!

先看Android Developers里怎么說的:

 

Note:         Handling the configuration change yourself can make it muchmore difficult to use alternative resources, because the system does not automatically apply them for you. This technique should be considered a last resort when you must avoid restarts due to aconfiguration change and        is not recommended for most applications        .     

 

However, your application should always be able to shut down and restart with its previous state intact, so you should not consider this technique an escape from retaining your state during normal activity lifecycle. Not only because there are other configuration changes that you cannot prevent from restarting your application, butalso because you should handle events such as when the user leaves your application and it gets destroyed before the user returns to it.

 

 

1.        配置改變和資源調整的問題,因為用這個方法我們需要自己往      onConfigurationChanged()里寫代碼      ,保證所用資源和設備的 當前配置一致,如果一個馬虎程序很容易出現資源指定的bugs,原文:   

 

Google engineers        ,however, discourage its use. The primary concern is that it requires youto handle device configuration changes manually in code. Handling configuration changes requires you to take many additional steps to ensure that each and every string, layout, drawable, dimension, etc.remains in sync with the device's current configuration, and if you aren't careful, your application can easily have a whole series of resource-specific bugs as a result.——        Handling Configuration Changes With Fragments

 

 

2. there are other configuration changes that you cannot prevent from restarting your application.有些configuration      changes沒法阻止應用重啟。(是說的有些android:configChanges的屬性值對避免重建無效?不知道理解是否正確)

3. 很多開發人員會錯誤指定      android:configChanges="        orientation        "來防止activity被銷毀或重建這種不可預知的情況。但是            引起Configuration Changes的情況很多,不止是屏幕旋轉。比如修改設備默認語言,修改設備默認字體比例等等都可會引起配置改變。這種方法只對當前設置的配置有效,除非在manifest里把所有配置都列全。     

4.       當用戶離開應用,在回到應用前被銷毀的話,例如點擊了屏幕的Home鍵或者有個電話打進來,用戶很久之后才回到應用程序,但是在此之前系統因為資源緊張而銷毀了應用進程,當用戶返回還是要重新創建activity,問題等於沒解決。   

 

Your application should be able to restart at any time without loss of user data or state in order to handle events such as configuration changes or when the user receives an incoming phone call and then returns to your application much later after your application process may have been destroyed.——        Handling Runtime Changes

 

          As a user you won't stay on that activity and stare at it. You would switch to the home screen or to another app like a game or a phone call might come in or something else resource hungry that will eventually destroy your activity. And what then? You are facing the same old issue which is NOT solved with that neat little trick. The activity will be recreated all over again when the user comes back.——          How to handle screen orientation change when progress dialog and background thread active?中的一個評論         

 

 

三、覆寫onRetainNonConfigurationInstance()來保留activity中的數據對象

在Android 3.0發布之前,處理Configuration Change的方法是覆寫onRetainNonConfigurationInstance()和getLastNonConfigurationInstance()方法。在onRetainNonConfigurationInstance()中返回對象(持有數據),再通過getLastNonConfigurationInstance()方法獲取該對象,再更新界面數據即可。看個例子就很容易明白了,修改TestHandlerActivity代碼如下:

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 ---");   }   String retain = (String) getLastNonConfigurationInstance();   if (retain != null){    refreshNewsInfo(retain);   }else{    //進入Activity后立馬加載數據    excuteLongTimeOperation();   }  }  /**  * 返回異步操作中獲取到的數據  * @return  */  @Override  public Object onRetainNonConfigurationInstance() {   return newsInfo;  }  /**  * 點擊按鈕,創建子線程,並顯示一個進度對話框  */  private void excuteLongTimeOperation() {   progressDialog = ProgressDialog.show(TestHandlerActivity.this,"Load Info","Loading...",true,true);   Thread workerThread = new Thread(new MyNewThread());   workerThread.start();  } //省略其他代碼 }

 

三、推薦使用Fragment來處理Configuration Change

具體步驟如下:

1. Extend the          Fragment           class and declare references to your stateful objects.       

2. CallsetRetainInstance(boolean) when the fragment is created. 

3. Add the fragment to your activity.

        4. Use         FragmentManager         to retrieve the fragment when the activity is restarted.     

定義一個RetainedFragment類:

package com.aliao.myandroiddemo.view.handler;

import android.app.Activity; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.support.v4.app.Fragment; import com.aliao.myandroiddemo.utils.ThreadUtil; /** * A simple {@link android.support.v4.app.Fragment} subclass. * Activities that contain this fragment must implement the * {@link RetaindFragment.OnFragmentInteractionListener} interface * to handle interaction events. * */ public class RetaindFragment extends Fragment {  private OnFragmentInteractionListener mListener;  @Override  public void onCreate(Bundle savedInstanceState) {   super.onCreate(savedInstanceState);   //retain ths fragment   setRetainInstance(true);  }  /**  * 點擊按鈕,創建子線程,並顯示一個進度對話框  */  public void excuteLongTimeOperation() {   Thread workerThread = new Thread(new MyNewThread());   workerThread.start();  }  class MyNewThread extends Thread{   @Override   public void run() {    //打印子線程的部分信息    ThreadUtil.logThreadSignature();    //模擬執行耗時操作    ThreadUtil.sleepForInSecs(5);    String 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) {    //更新界面中TextView中的內容    if(mListener != null){     mListener.onFragmentInteraction(msg.getData().getString("message"));    }   }  };  // TODO: Rename method, update argument and hook method into UI event  public void onButtonPressed(String string) {   if (mListener != null) {    mListener.onFragmentInteraction(string);   }  }  @Override  public void onAttach(Activity activity) {   super.onAttach(activity);   try {    mListener = (OnFragmentInteractionListener) activity;   } catch (ClassCastException e) {    throw new ClassCastException(activity.toString()      + " must implement OnFragmentInteractionListener");   }  }  @Override  public void onDetach() {   super.onDetach();   mListener = null;  }  /**  * This interface must be implemented by activities that contain this  * fragment to allow an interaction in this fragment to be communicated  * to the activity and potentially other fragments contained in that  * activity.  * <p>  * See the Android Training lesson <a href=  * "http://developer.android.com/training/basics/fragments/communicating.html"  * >Communicating with Other Fragments</a> for more information.  */  public interface OnFragmentInteractionListener {   // TODO: Update argument type and name   public void onFragmentInteraction(String string);  } }

這個Fragment沒有界面,它用來處理異步操作,然后把結果就該界面的部分返回給Activity來處理,Activity會實現Fragment中定義的OnFragmentInteractionListener接口中的onFragmentInteraction(String string)方法(    Fragment與Activity之間的通訊    ),這個接口我們可以自己定義。把之前在TestHandlerActivity中的異步操作移植到RetainedFragemnt類中。   

TestHandlerActivity對應的布局文件activity_handler.xml修改代碼如下:

<?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" />  <ProgressBar   android:id="@+id/progress_circular"   android:layout_width="wrap_content"   android:layout_height="wrap_content"   android:layout_margin="10dp"   style="@android:style/Widget.ProgressBar.Small"   android:visibility="gone"   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.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentManager; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.ProgressBar; 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 FragmentActivity implements View.OnClickListener,RetaindFragment.OnFragmentInteractionListener{  private final String TAG = "testhandler";  private TextView showNewsInfoTxt;  private ProgressDialog progressDialog;  private ProgressBar progressBar;  private String newsInfo;  private RetaindFragment dataFragment;  private static final String KEY_CURRENT_NEWSDATA = "current_nesdata";  @Override  protected void onCreate(Bundle savedInstanceState) {   super.onCreate(savedInstanceState);   setContentView(R.layout.activity_handler);   Log.i(TAG, "----onCreate---");   //打印當前線程的部分信息   ThreadUtil.logThreadSignature();   Button anrBtn = (Button) findViewById(R.id.btn_createthread);   anrBtn.setOnClickListener(this);   showNewsInfoTxt = (TextView) findViewById(R.id.tv_showsth);   progressBar = (ProgressBar) findViewById(R.id.progress_circular);   if(null != savedInstanceState){    refreshNewsInfo((String) savedInstanceState.get(KEY_CURRENT_NEWSDATA));   }   //在activity重啟時獲取到保留的fragment對象   FragmentManager fm = getSupportFragmentManager();   dataFragment = (RetaindFragment) fm.findFragmentByTag("data");   if(null == dataFragment){    //添加fragment    dataFragment = new RetaindFragment();    fm.beginTransaction().add(dataFragment, "data").commit();    //從網上下載數據      }  }  @Override  public void onClick(View view) {   switch (view.getId()){    case R.id.btn_createthread:     progressBar.setVisibility(View.VISIBLE); // progressDialog = ProgressDialog.show(TestHandlerActivity.this,"Load Info","Loading...",true,true);     //控制RetainFragment中的子線程啟動     dataFragment.excuteLongTimeOperation();     break;   }  }  @Override  public void onFragmentInteraction(String newsInfo) {   this.newsInfo = newsInfo;   refreshNewsInfo(newsInfo);  }  /**  * 更新界面內容  * @param newsInfo  */  private void refreshNewsInfo(String newsInfo) {   progressBar.setVisibility(View.GONE);   showNewsInfoTxt.setText(newsInfo);  }  @Override  protected void onSaveInstanceState(Bundle outState) {   super.onSaveInstanceState(outState);   outState.putString(KEY_CURRENT_NEWSDATA,showNewsInfoTxt.getText().toString());//注意不要直接傳newsInfo,否則在異步操作執行完成后旋轉屏幕,內容還是會消失。因為該值只有在屏幕旋轉的時候才賦值,   Log.i(TAG, "----onSaveInstanceState---");  }  /**  * 只有在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();   Log.i(TAG, "----onStart---");  }  @Override  protected void onResume() {   super.onResume();   Log.i(TAG, "----onResume---");  }  @Override  protected void onRestart() {   super.onRestart();   Log.i(TAG, "----onRestart---");  }  @Override  protected void onPause() {   super.onPause();   Log.i(TAG, "----onPause---");  }  @Override  protected void onStop() {   super.onStop();   Log.i(TAG, "----onStop---");  } }

在異步操作還未執行完畢的時候旋轉屏幕,TestHandlerActivity會被銷毀再重建。新的TestHandlerActivity被創建,新的Activity實例會傳送給onAttach(Activity)方法,通過打印onAttach中的activity可以看到屏幕旋轉前后onAttach綁定的activity不同。這樣就確保不管配置是否改變RetainedFragment持有的都是當前展示的Activity的引用。   

在以上的示例中onSaveInstanceState的作用是在異步操作完畢時旋轉屏幕確保屏幕數據不丟失。

 

onSaveInstanceState: it might not be possible for you to completely restore youractivity state with the            Bundle             that the system saves for you with the            onSaveInstanceState()             callback—it is notdesigned to carry large objects (such as bitmaps) and the data within it must be serialized thendeserialized, which can consume a lot of memory and make the configuration change slow.——            Handling Runtime Changes

 

 

異步操作顯示對話框在Configuration Changes時導致程序崩潰

之前看到一篇講內存泄露的文章,其中一個內存泄露的情境和上面的實例代碼情境很類似,大意是:在Activity里創建一個子線程來跑耗時操作,在異步操作沒結束前旋轉屏幕,線程沒執行完,old Activity也就不會被銷毀,會導致內存泄露。摘取部分原文內容:

“由於我們的線程是Activity的內部類,所以MyThread中保存了Activity的一個引用,當MyThread的run函數沒有結束時,MyThread是不會被銷毀的,因此它所引用的老的Activity也不會被銷毀,因此就出現了內存泄露的問題。”

“Thread只有在run函數不結束時才出現這種內存泄露問題”

用圖來表示上述內容的話,應該是:

            看完這圖就delete掉記憶吧,錯滴錯滴         

他這段話誤導了我好一陣:“線程的run函數沒有結束,線程不會被銷毀,他所引用的老activity也不會被銷毀。所以出現了內存泄露。”在這種理解的基礎上,我一直以為帶進度對話框的異步操作在屏幕旋轉的時候出現程序終止,是因為線程沒結束,activity不銷毀,所以導致了內存泄露。而且logcat下還打印了這么一句:       

04-16 00:26:18.703  17075-17075/com.aliao.myandroiddemo E/WindowManager﹕ Activity com.aliao.myandroiddemo.view.handler.TestHandlerActivity has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView@41f9a990 that was originally added here

當時我就一直為了驗證心里早已固定的認同感,看到“          TestHandlerActivity has leaked”TestHandlerActivity已經泄露了...就不加思考的去相信了。       

但是今天在看有關於異步操作帶對話框在configuration change時的處理辦法時,總有一個疑問就是舊的activity會在線程結束的時候被銷毀嗎?后來做了測試,代碼用的還是<Configuration Changes引發的問題>里最后貼的代碼:

 

測試設備:HTC t329d Android4.1

 

測試操作:點擊按鈕啟動線程,旋轉屏幕,記錄Activity被銷毀時間,查看debug模式下的Threads列表記錄線程消失時間

 

測試條件一:異步操作執行時長5秒

 

測試一結果:Activity的onDestroy調用的時間比worker thread結束時間晚或相等(這條件下就測了兩次)。

 

測試條件二:異步操作執行時長20秒

 

            測試二結果:啟動線程的時間:         

 

                     activity的onDestroy()調用時間:            00:09:04

 

                   thread的在Threads列表消失時間:              00:09:18

 

                      可以看到old activity的銷毀時間在thread結束之前!!!             

 

 

          測試結果表明:activity並不是在thread結束后才銷毀。這與之前說的“          thread沒有銷毀導致被持有引用的activity也不會銷毀”相互矛盾!所以因為這個原因導致的內存泄露的說法就更沒有說服力了。

再看之前log打印的錯誤:

 

1. WindowManager﹕ Activity com.aliao.myandroiddemo.view.handler.                TestHandlerActivity has leaked window                com.android.internal.policy.impl.PhoneWindow$DecorView@41f9a990 that was originally added here             

 

不是TestHandlerActivity has leaked

 

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

 

這個錯誤的發生是因為dismiss對話框時,所屬Activity已經不在了。

 

 

經過上面的測試和打印的錯誤log可以得出:這個bug不是因為old activity沒銷毀導致內存泄露,而是activity被銷毀后 progressDialog還持有這個activity的引用。異步任務開始時顯示對話框,任務完成后去取消progressDialog。當任務沒結束時旋轉屏幕,會導致old activity被銷毀,然后到了線程執行結束要dismiss progressDialog的時候發現所屬的activity已經不在了。

解決辦法(如果有其他好方法,推薦下下哇):

1.在布局文件創建progressBar來代替progressDialog

        2. 創建一個AsyncTask的時候把當前Activity的引用傳給其構造函數。onRetainNonConfigurationInstance()中判斷線程是否結束,如果結束了就把progressDialog取消掉,然后將AsyncTask對象mTask返回。在onCreate中通過getLastNonConfigurationInstance()接收        mTask,關聯當前activity——mtask.mContext = this;再重新啟動一個progressDialog。保證了progressDialog在actviity銷毀錢被dismiss掉。from           How to handle screen orientation change when progress dialog and background thread active?中的其中一個回答。單單只是測試progressDialog在橫豎屏切換時是否會崩潰,測試結果是正常的。       

3.網上還有說用IntentService來解決,沒用過這個,先不測了。

 

遺留問題:

1.在Configuration Changes引發的問題一節中 "        there are other configuration changes that you cannot prevent from restarting your application."該怎么正確的翻譯和理解

2. 在Configuration Changes引發的問題一節中的第四點怎么理解才是正確的。          不知道這種情況發生的情境,應用進程被銷毀是等於整個app被kill掉,那不就是又重新打開app,重新進入activity也是正常的步驟。重新進入再重新請求唄。

 

參考資料:

強烈推薦閱讀

Handling Runtime Changes       ——來自Android Developers,介紹Configuration changes及其對數據丟失的解決方法。     

Handling Configuration Changes With Fragments       —— from       Alex Lockwood         的blog,介紹了Configuration Changes的引發的問題、為什么不推薦適用android:configChanges方式來解決問題以及適用Fragments如何處理Configuration Changes(異步操作用的AsyncTask,比Thread+Handler在mainThread和workerThread上的UI更新和耗時處理上更加模塊化,更方便)。     


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM