原文地址
在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 {
@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
方法
意见修改
- 欢迎指出翻译有误的地方