原文來自ANDROID DESIGN PATTERNS
相關譯文:「譯」Fragment事務與Activity狀態丟失
歡迎轉載,但請保留譯者鏈接:http://www.lxweimin.com/p/53bfd7206c66
這篇文章面向的目標是一個經常在StackOverflow上被問到的普適性問題:
What is the best way to retain active objects—such as running Threads, Sockets, and AsyncTasks—across device configuration changes?
在設備配置發生變化的時候,什么是用來保持活動對象(比如說運行中的線程、Sockets還有AsyncTasks)的最佳方法呢?
要回答這個問題,我們先討論一下開發者在Activity生命周期中使用長時間運行的后臺任務時會面臨 的普遍困難,然后我們描述一下兩種解決這一問題的常用途徑的瑕疵在哪里,最后我們以示例代碼作結用以闡明推薦的解決方案——也就是使用保留的Fragment來達到我們的目標。
配置變化與后臺任務
配置變化會引發Activity經受銷毀-重建循環(the destroy-and-create cycle),而這一問題起源于這一事實:配置變化不可預測,任何時候都可能發生。并發的后臺任務加重了這一問題。舉一個例子,試想在一個Activity中啟動了一個AsyncTask
然后用戶很快就旋轉了屏幕(譯者注:對于配置變化來說,屏幕旋轉是一個具有視覺效果變化且手動可控的操作,由于許多應用很明智地壓根只支持豎屏,實際開發中我們遇見的更可能是語言變化、SIM卡掉卡、網絡狀態變化等,視情況而定,Monkey test有可能很好地把問題暴露出來也可能不行,但作為開發者我們應該保持警惕),這將會導致Activity被銷毀并且重建。當這個AsyncTask
最后完成它的工作并返回時,它將會錯誤地報告它的結果給舊的Activity實例,完全不知道有一個新的Activity實例被創建了。似乎這并不完全是個問題,但是這個新的Activity實例可能會重新啟動一個后臺任務(因為它不知道舊的AsyncTask
尚在運行),從而浪費寶貴的資源。
出于這些因素,所以說這是一件重要的事:當配置發生變化時,我們需要正確且有效率地在Activity實例之間保持活動對象。
糟糕的實踐:保持Activity
也許最hack并且最被廣泛濫用的權宜之計就是禁止默認的銷毀-重建行為,具體來說就是通過設置你的Android manifest中的android:configChanges
屬性。這種解決途徑如此顯眼的簡單以致對開發者非常有吸引力,可是Google工程師們不推薦使用。這主要關系到使用這種方法會需要你在代碼中手工進行配置變化處理。而處理配置變化需要你提供許多額外的步驟用來確保每一個 string, layout, drawable, dimension, etc. 與設備的當前配置保持一致。如果你不夠小心,那么造成的后果就是你的應用很容易就會存在一系列的資源特定的bug(resource-specific bugs)。
另一個原因Google為什么不推薦這種方法,則是許多開發者錯誤地假想設置android:configChanges=orientation
(for example)將會如魔法般地從那些會導致運行中的Activity銷毀并且重建的不可預期劇本中保護他們的應用。情況并不是這樣的。配置變化產生的原因有很多——并不只是屏幕方向變化。把你的設備插入一個顯示插槽、改變默認語言以及修改設備的默認字體放縮因素只是能夠觸發配置變化的所有事件中的三個例子而已,這類事件會發信號給系統讓它對所有現在正在運行的Activity作這樣一個操作:在Activity下一次重新恢復的時候對其進行銷毀與重建。(Inserting your device into a display dock, changing the default language, and modifying the device's default font scaling factor are just three examples of events that can trigger a device configuration change, all of which signal the system to destroy and recreate all currently running Activitys the next time they are resumed.)
結果就是,設置android:configChanges
屬性總體來說不是一種好的實踐。
廢棄了的:覆蓋onRetainNonConfigurationInstance()
在Honeycomb(譯者注:Android 3.0)版本之前,在Activity實例間傳遞活動對象的推薦解決方案是覆蓋onRetainNonConfigurationInstance()
還有getLastNonConfigurationInstance()
方法。使用這種方案,需要處理的要緊事僅僅只有在onRetainNonConfigurationInstance()
中返回活動對象并且在getLastNonConfigurationInstance()
中獲取它。自API 13起,這些方法被廢棄了,因為Fragment的setRetainInstance(boolean)
能力更強,能夠提供更加清潔以及模塊化的方法來在配置變化期間保持活動對象。我們下一節將討論基于Fragment的解決方案。
推薦:在保留的Fragment
內部管理對象
自從Android 3.0引入了Fragment后,在Activity實例間傳遞活動對象的推薦解決方案就變成了在一個保留的“工作”Fragment內部包裝與管理它們。(Ever since the introduction of Fragments in Android 3.0, the recommended means of retaining active objects across Activity instances is to wrap and manage them inside of a retained "worker" Fragment.)默認情況下,當配置變化發生時Fragment會伴隨著宿主Activity銷毀與重建。調用Fragment#setRetainInstance(true)
允許我們繞開銷毀-重建循環,發信號給系統讓其在宿主Activity被重建時保留現在的Fragment實例。我們將會見到,這一特性對于Fragment持有對象比如說運行中的線程、Sockets還有AsyncTasks等極其有用。
以下示例代碼展現的是一個如何用保留的Fragment在配置變化時保持AsyncTask
的基礎示例。這份代碼保證了進度更新還有結果會被回傳給當前正被顯示的Activity,并且確保了在配置變化期間我們不會意外地泄漏宿主Activity。其設計由兩個類組成,一個MainActivity
...
/**
* This Activity displays the screen's UI, creates a TaskFragment
* to manage the task, and receives progress updates and results
* from the TaskFragment when they occur.
*/
public class MainActivity extends Activity implements TaskFragment.TaskCallbacks {
private static final String TAG_TASK_FRAGMENT = "task_fragment";
private TaskFragment mTaskFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
FragmentManager fm = getFragmentManager();
mTaskFragment = (TaskFragment) fm.findFragmentByTag(TAG_TASK_FRAGMENT);
// If the Fragment is non-null, then it is currently being
// retained across a configuration change.
if (mTaskFragment == null) {
mTaskFragment = new TaskFragment();
fm.beginTransaction().add(mTaskFragment, TAG_TASK_FRAGMENT).commit();
}
// TODO: initialize views, restore saved state, etc.
}
// The four methods below are called by the TaskFragment when new
// progress updates or results are available. The MainActivity
// should respond by updating its UI to indicate the change.
@Override
public void onPreExecute() { ... }
@Override
public void onProgressUpdate(int percent) { ... }
@Override
public void onCancelled() { ... }
@Override
public void onPostExecute() { ... }
}
還有一個TaskFragment
...
/**
* This Fragment manages a single background task and retains
* itself across configuration changes.
*/
public class TaskFragment extends Fragment {
/**
* Callback interface through which the fragment will report the
* task's progress and results back to the Activity.
*/
interface TaskCallbacks {
void onPreExecute();
void onProgressUpdate(int percent);
void onCancelled();
void onPostExecute();
}
private TaskCallbacks mCallbacks;
private DummyTask mTask;
/**
* Hold a reference to the parent Activity so we can report the
* task's current progress and results. The Android framework
* will pass us a reference to the newly created Activity after
* each configuration change.
*/
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mCallbacks = (TaskCallbacks) activity;
}
/**
* This method will only be called once when the retained
* Fragment is first created.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Retain this fragment across configuration changes.
setRetainInstance(true);
// Create and execute the background task.
mTask = new DummyTask();
mTask.execute();
}
/**
* Set the callback to null so we don't accidentally leak the
* Activity instance.
*/
@Override
public void onDetach() {
super.onDetach();
mCallbacks = null;
}
/**
* A dummy task that performs some (dumb) background work and
* proxies progress updates and results back to the Activity.
*
* Note that we need to check if the callbacks are null in each
* method in case they are invoked after the Activity's and
* Fragment's onDestroy() method have been called.
*/
private class DummyTask extends AsyncTask<Void, Integer, Void> {
@Override
protected void onPreExecute() {
if (mCallbacks != null) {
mCallbacks.onPreExecute();
}
}
/**
* Note that we do NOT call the callback object's methods
* directly from the background thread, as this could result
* in a race condition.
*/
@Override
protected Void doInBackground(Void... ignore) {
for (int i = 0; !isCancelled() && i < 100; i++) {
SystemClock.sleep(100);
publishProgress(i);
}
return null;
}
@Override
protected void onProgressUpdate(Integer... percent) {
if (mCallbacks != null) {
mCallbacks.onProgressUpdate(percent[0]);
}
}
@Override
protected void onCancelled() {
if (mCallbacks != null) {
mCallbacks.onCancelled();
}
}
@Override
protected void onPostExecute(Void ignore) {
if (mCallbacks != null) {
mCallbacks.onPostExecute();
}
}
}
}
事件流
當MainActivity
第一次啟動時,它實例化并添加了TaskFragment
到Activity的狀態中。TaskFragment
創造并啟動了一個AsyncTask
,它代理了進度更新和結果通過TaskCallbacks
接口回傳給MainActivity
。當配置變化發生時,MainActivity
經歷通常的生命周期循環,新創建的Activity實例馬上就會被傳遞到onAttach(Activity)
方法中,因此確保了TaskFragment
總會持有當前顯示的Activity實例的引用,即使是在配置變化之后。生成的設計即簡單又可靠;當Activity實例銷毀重建時application framework會處理它們的重新賦值,而TaskFragment
與它的AsyncTask
從來不需要擔憂不可預期的配置變化的發生。還需注意的一點是,在調用onDetach()
與onAttach()
之間onPostExecute()
是不可能運行的,其解釋可以參見this StackOverflow answer 還有this Google+ post中我對 Doug Stevenson 的回復(there is also some discussion about this in the comments below)。
結論
同步后臺任務伴隨著Activity生命周期將變得棘手,而且配置變化將加大這一困惑。幸運的是,保留的Fragment令處理這些事變得非常簡單——通過堅實地持有其宿主Activity的引用,即使該Activity經歷了銷毀與重建。
A sample application illustrating how to correctly use retained Fragments to achieve this effect is available for download on the Play Store. The source code is available on GitHub. Download it, import it into Eclipse, and modify it all you want!
As always, leave a comment if you have any questions and don't forget to +1 this blog in the top right corner!