關於保存狀態的Fragment,setRetainInstance(true)

轉載自:https://blog.csdn.net/codemydream/article/details/53423888

這篇文章解決了在StackOverflow上一個經常被提到的問題。

在配置發生變化(Configuration changs)時,什麼是最好的保存活動對象方法,比如運行中的線程,Sockets,AsyncTask

要回答這個問題,我們要先討論一些開發者在Activity生命週期中使用長時間後臺任務時遇到的共同困難。然後,我們將介紹常見的兩種能解決問題但有不好的方法。最後,我們會用一個示例代碼說明推薦的解決方案,它用retained fragment來達到我們的目標。

配置改變&後臺線程(Configuration Changes & Background Tasks)

配置發生變化以及銷燬和重新創建穿越了整個Activity的生命週期,並且引出一個問題,那就是這些事件的發生是不可預測並且在任何時候都可能觸發。併發的後臺線程只加劇了這個問題。假設在Activity中啓動了一個AsyncTask,然後用戶馬上旋轉屏幕,這會導致Activity被銷燬和重新創建。當AsyncTask最後完成它的任務,它會將結果反饋到舊的Activity實例,完全沒有意識到新的activity已經被創建了。似乎這不是一個問題,新的Activity實例又會讓浪費寶貴的資源重新啓動一個後臺線程,而不知道舊的AsyncTask已經在運行。由於這些原因,在配置變化的時候我們需要正確、有效地保存在Activity實例的活動對象。

不好的實踐:保存整個Activity

可能最有效和最常被濫用的解決方法是通過在Android manifest中設置android:configChanges屬性禁止默認的銷燬和重新創建行爲。這個簡單的方法使得它對開發者很有吸引力;然而Google的工程師建議不這麼做。主要的擔憂是:配置後,需要你在代碼中手動處理設備的配置變化。處理配置變化需要你採取很多額外的處理,以確保每一個字符串、佈局、繪圖、尺寸等與當前設備的配置一致。如果你不小心,那麼你的應用程序可能會有一系列與資源定製方面有關的Bug。

另一個Google不鼓勵使用它的原因是許多開發者錯誤地認爲,設置android:configChanges = "orientation"(這只是舉例說明),會神奇地避免他們的Activity在不可預知的場景中被銷燬和重新創建。其實不是這樣的。有多種原因可能導致配置發生變化,而不單單是屏幕橫豎屏的變化。將你的手機中的內容顯示在顯示器上,更改默認語言,修改設備默認的字體縮放,這三個簡單的例子都有可能觸發設備的配置變化。這些事件會向系統發出信號,銷燬並重建所有正在運行的Activity,在它們下一次resume的時候。所以設置android:configChanges屬性一般不是好的做法。

已經被棄用的方法:重寫onRetainNonConfigurationInstance()

在Honeycomb發佈前,跨越Activity實例傳遞活動對象的推薦方法是重寫onRetainNonConfigurationInstance()getLastNonConfigurationInstance()方法。使用這種方法,傳遞跨越Activity 實例的活動對象僅僅需要在onRetainNonConfigurationInstance()將活動對象返回,然後在getLastNonConfigurationInstance()中取出。截止API 13,這些方法都已經被棄用,以支持更有效的Fragment的setRetainInstance(boolean)方法。它提供了一個更簡潔,更模塊化的方式在配置變化的時候保存對象。我們將在下一節討論以Fragment爲基礎的方法。

推薦的方法:在Retained Fragment中管理對象

自從Android3.0推出Fragment。跨越Activity保留活動對象的推薦方法是在一個Retained Fragment中包裝和管理它們。默認情況下,但配置發生變化時,Fragment會隨着它們的宿主Activity被創建和銷燬。調用Fragment#setRetaininstance(true)允許我們跳過銷燬和重新創建的週期。指示系統保留當前的fragment實例,即使是在Activity被創新創建的時候。不難想到使用fragment持有像運行中的線程、AsyncTask、Socket等對象將有效地解決上面的問題。

下面代碼演示如何使用fragment在配置發生變化的時候保存AsyncTask的狀態。這段代碼保證了最新的進度和結果能夠被傳回更當前正在顯示的Activity實例,並確保我們不會在配置發生變化的時候丟失AsyncTask的狀態。下面代碼包含兩個類,一個MainActivity...

複製代碼
 1 /**
 2  * 這個Activity主要用來展示UI,創建一個TaskFragment來管理任務,
 3  * 從TaskFragment接收進度以及執行結果.
 4  */
 5 public class MainActivity extends Activity implements TaskFragment.TaskCallbacks {
 6 
 7   private static final String TAG_TASK_FRAGMENT = "task_fragment";
 8 
 9   private TaskFragment mTaskFragment;
10 
11   @Override
12   protected void onCreate(Bundle savedInstanceState) {
13     super.onCreate(savedInstanceState);
14     setContentView(R.layout.main);
15 
16     FragmentManager fm = getFragmentManager();
17     mTaskFragment = (TaskFragment) fm.findFragmentByTag(TAG_TASK_FRAGMENT);
18 
19     //如果Fragment不爲null,那麼它就是在配置變化的時候被保存下來的
20     if (mTaskFragment == null) {
21       mTaskFragment = new TaskFragment();
22       fm.beginTransaction().add(mTaskFragment, TAG_TASK_FRAGMENT).commit();
23     }
24 
25     // TODO: 初始化View, 還原保存的狀態, 等等.
26   }
27 
28   //下面四個方法將在進度需要更新或者返回結果的時候被調用。
29   //MainActivity需要更新UI來反應這些變化。
30   @Override
31   public void onPreExecute() { ... }
32 
33   @Override
34   public void onProgressUpdate(int percent) { ... }
35 
36   @Override
37   public void onCancelled() { ... }
38 
39   @Override
40   public void onPostExecute() { ... }
41 }
複製代碼

...和一個 TaskFragment...

複製代碼
  1 /**
  2  * 這個Fragment管理一個後臺任務,在狀態發生變化的時候能夠保存下來,不被銷燬
  3  */
  4 public class TaskFragment extends Fragment {
  5 
  6   /**
  7    * 讓Fragment通知Activity任務進度和返回結果的回調接口
  8    */
  9   static interface TaskCallbacks {
 10     void onPreExecute();
 11     void onProgressUpdate(int percent);
 12     void onCancelled();
 13     void onPostExecute();
 14   }
 15 
 16   private TaskCallbacks mCallbacks;
 17   private DummyTask mTask;
 18 
 19   /**
 20    * 持有一個父Activity的引用,以便在任務進度變化和需要返回結果的時候通知它。
 21    * 在每一次配置變化後,Android Framework會將新創建的Activity的引用傳遞給我們
 22    */
 23   @Override
 24   public void onAttach(Activity activity) {
 25     super.onAttach(activity);
 26     mCallbacks = (TaskCallbacks) activity;
 27   }
 28 
 29   /**
 30     *這個方法只會被調用一次,只在這個被保存Fragment第一次被創建的時候
 31    */
 32   @Override
 33   public void onCreate(Bundle savedInstanceState) {
 34     super.onCreate(savedInstanceState);
 35 
 36     //在配置變化的時候將這個fragment保存下來
 37     setRetainInstance(true);
 38     
 39 
 40     // 創建並執行後臺任務
 41     mTask = new DummyTask();
 42     mTask.execute();
 43   }
 44 
 45   /**
 46    * 設置回調對象爲null,防止我們意外導致Activity實例泄露(leak the Activity instance)
 47    */
 48   @Override
 49   public void onDetach() {
 50     super.onDetach();
 51     mCallbacks = null;
 52   }
 53 
 54   /**
 55    * 一個示例性的任務用來表示一些後臺任務並且通過回調函數向Activity
 56    * 報告任務進度和返回結果
 57    *
 58    * 注意:我們需要在每一個方法中檢查回調對象是否爲null,以防它們
 59    * 在Activity或Fragment的onDestroy()執行後被調用。
 60    */
 61   private class DummyTask extends AsyncTask<Void, Integer, Void> {
 62 
 63     @Override
 64     protected void onPreExecute() {
 65       if (mCallbacks != null) {
 66         mCallbacks.onPreExecute();
 67       }
 68     }
 69 
 70     /**
 71      * 注意:我們不在後臺線程的doInbackground方法中直接調用回調
 72      * 對象的方法,因爲這樣可能產生競態條件
 73      */
 74     @Override
 75     protected Void doInBackground(Void... ignore) {
 76       for (int i = 0; !isCancelled() && i < 100; i++) {
 77         SystemClock.sleep(100);
 78         publishProgress(i);
 79       }
 80       return null;
 81     }
 82 
 83     @Override
 84     protected void onProgressUpdate(Integer... percent) {
 85       if (mCallbacks != null) {
 86         mCallbacks.onProgressUpdate(percent[0]);
 87       }
 88     }
 89 
 90     @Override
 91     protected void onCancelled() {
 92       if (mCallbacks != null) {
 93         mCallbacks.onCancelled();
 94       }
 95     }
 96 
 97     @Override
 98     protected void onPostExecute(Void ignore) {
 99       if (mCallbacks != null) {
100         mCallbacks.onPostExecute();
101       }
102     }
103   }
104 }
複製代碼

事件流

MainActivity第一次啓動的時候,它實例化並將TaskFragment添加到Activity的狀態中。TaskFragment創建並執行AsyncTask並通過TaskCallBack接口將任務處理進度和結果回傳給MainActivity。當配置變化時,MainActivity正常地經歷它的生命週期事件(銷燬、重新創建、onResume等),但是一旦新Activity實例被重新創建,那它就會被傳遞到onAttach(Activity)方法中,這就保證了TaskFragment會一直持有當前顯示的新Activity實例的引用,即使是在配置發生變化的時候。另外值得注意的是,onPostExecute()不會在onDetach()onAttach()的之間被調用。具體的解釋參考StackOverflow的回答以及我在Google+文章中給Doug Stevenson的回覆(在評論中也有一些關於它的討論)。

總結

在Activity的生命週期(涉及舊、新Activity的銷燬和創建)中同步後臺任務的運行狀態可能非常棘手,並且配置發生變化加劇了這一麻煩。幸運的是使用一個retain fragment可以非常輕鬆地處理這些事件。它只要始終持有父Activity的引用,即使在父Activity被銷燬和重新創建之後。

一個示例型的App演示怎麼正確地使用Retained Fragment來達到我們的目的,Play Store下載地址。託管在Github上的源代碼。下載它,用Eclipse執行Import,然後自己隨意修改。

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