在StackOverFlow上這類問題很常見
What is the best way to retain active objects—such as runningThreads,Sockets, andAsyncTasks—across device configuration changes?
回答問題之前,我們先討論開發者通常會遇到的,在處理與Activity生命週期相關的耗時任務的困難。
接着,我們會討論兩種常用方法的缺陷。
最後,我們會使用持久化Framgnet作爲實例代碼,給出值得推薦解決方案。
屏幕旋轉 & 後臺任務
屏幕旋轉的問題在於,Activity必須經歷生命週期的重構,而事件的發生卻是不可預測的。後臺併發任務的處理無異加劇了這個難題。
比如,Activity啓動了AsyncTask之後,用戶旋轉手機屏幕,導致Activity被銷燬和重建。當AsyncTask完成任務後,在並不知道Activity被新建的情況下,會錯誤地把結果交給舊Activity。因爲新Activity並不知道AsyncTask的存在和最後結果,所以會重新啓動AsyncTask,導致資源浪費。因此,在屏幕旋轉的過程中,正確有效地保存Activity信息就顯得尤爲重要。
壞方法:固定Activity的方向
世界上最取巧,最被濫用的方法就是通過固定Activity方向,阻止Activity的重構。
在AndroidManifest.xml文件中設置android:configChanges
這個簡單的方法非常吸引開發者。谷歌工程師並不推薦這種做法。
首當其衝需要使用代碼處理屏幕旋轉,意味着花更多的精力確保每個字符串(string),佈局(layout),尺寸(dimen)等與當前屏幕方向保持同步,處理不當很容易會造成一系列的資源特定的bug。
谷歌另一個不鼓勵使用該方法的原因,很多開發者錯誤地設置android:configChanges="orientation"
(舉例)會意外地保護底層Activity摧毀和重構。不止屏幕旋轉,還有各種各樣的原因導致配置改變,把設備接到顯示器上、改變默認語言、改變默認字體大小隻是三個會改變配置的觸發事件。所以,設置android:configChanges
並不是一個好方法。
過時,重寫onRetainNonConfigurationInstance()
在Android Honeycomb(Android 3.1系統,譯者注)版本之前,推薦重寫onRetainNonConfigurationInstance()
和getLastNonConfigurationInstance()
在多個Activity實例間轉移對象。onRetainNonConfigurationInstance()
用於傳遞對象而getLastNonConfigurationInstance()
用於獲取對象。在API 13(Android 3.2系統,譯者注)這些方法過時,支持使用更方便的模塊化方法Fragment中setRetainInstance(boolean)
來保存對象。下一章節我們會討論這種方法。
推薦:在持久化Fragment中管理對象
從Android 3.0開始引入Fragment的概念,在Activity中持久化對象的方法,是通過持久化Fragment包裝和管理這些對象。默認情況下,在配置發生改變時Fragment的重構是跟隨父Activity的重構的。通過調用Fragment#setRetainInstance(true)
,跳過銷燬重建的過程,告訴系統在Acitivity重建時保持當前Fragment實例的狀態。這在我們運行Thread
,AsyncTask
,Socket
,使用持久化Fragment就變得相當有利。
下面的樣例代碼示範,在配置改變的情況下,怎麼去使用持久化Fragment來保存AsyncTask。代碼保證了進度更新和正確傳遞結果到Activity,不會泄露AsyncTask的引用在配置改變時。
代碼包括兩個類,第一個是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
,將更新結果傳遞迴MainActivity
通過TaskCallbacks
接口。
當配置發生改變時,MainActivity
正常走生命週期方法,一旦新的Activity創建成功後會回調Fragmentd的onAttach(Activity)
方法,即使在配置改變的情況下,保證Fragment當前持有的是最新的Activity的引用。
代碼運行的結果是簡單且可靠的;應用程序框架會處理Activity重建後的實例,TaskFragment
和AsyncTask
無需關注配置的改變。onPostExecute()
可以在onDetach()
和onAttach()
方法回調之間執行。
參考在StackOverFlow上的回答和在Google+回答Doug Stevenson的問題。
結論
與Activity生命週期相關的同步後臺任務的處理是很有技巧的,配置改變也容易令人迷惑。幸運的是,通過長期持有父Activity的引用,即使在被重構的情況下,持久化Fragment使得這些事件的處理變得簡單。
你可以在Play Store上下載到代碼,源碼在github上開源了,下載,import到Eclipse,隨心所欲地改吧;)
譯者注
屏幕旋轉總結
- 不設置Activity的android:configChanges時,切屏會重新調用各個生命週期,切橫屏時會執行一次,切豎屏時會執行兩次
- 設置Activity的android:configChanges=”orientation”時,切屏還是會重新調用各個生命週期,切橫、豎屏時只會執行一次
- 設置Activity的android:configChanges=”orientation|keyboardHidden”時,切屏不會重新調用各個生命週期,只會執行onConfigurationChanged方法
意見修改
- 歡迎指出翻譯有誤的地方