原文来自ANDROID DESIGN PATTERNS
相关译文:「译」Fragment事务与Activity状态丢失
欢迎转载,但请保留译者链接:http://www.jianshu.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!