轉載自: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,然後自己隨意修改。