1. 概述
本文主要讨论如何利用 Fragment 来处理 configuration 的改变的问题,即在 configuration 发生改变的时候,我们怎样保持 objects (如正在运行的线程、Sockets 及 AsyncTasks),以便能够再次恢复它们的状态。
为了回答这个问题,我们会先谈谈开发者面临的一个难题 :在 Activity 的生命周期变迁期间,如何处理需要长时间运行的后台任务 。然后我们再讨论一下两种通常的解决方案的弊端 。最后,我们再提出一种较好的解决方案:即用户 worker fragments 来解决上边的问题,并附上源码 。
2. Configuration Changes & Background Tasks
configuration 的改变和 Activitys 的生命周期的变迁是不可预测的,且它们随时可能发生 。假设一个 Activity 刚刚启动了一个 AsynTask ,紧接着屏幕旋转了,这会导致 Activity 被销毁并重新创建 。当这个 AsynTask 完成其任务后,它会尝试把其结果报告给老的 Activity 实例 。其实这个实例已不存在,但是 AsynTask 并不知道这个事情),AsynTask 也完全不会意识到新的 Activity 实例已经被创建了 。这样新的 Activity 实例会浪费掉已有的资源,然后又去触发一次后台任务,但是它却不会意识到已经有一个 AsynTask 执行这个任务了。所以,当 configuration 发生改变的时候,我们有必要保持好各 objects 的状态,以便新的 activity 实例能够通过它们恢复到 configuration 改变前的正确状态。
3. 保存 Activity (不好的解决方案)
也许最滥用的解决方案是设置 manifest 中的 android:configChanges 属性,让它禁止 configuration 改变时销毁再重新创建 activity 。这种做法看起来非常方便快捷,对很多开发者来说诱惑很大 。但是 Google 的工程师却不建议这么做,因为这样根本行不通 。所以看来我们还是得自己编码来处理 configuration 的改变,但是这样我们就得保存所有 string、layout、drawable、dimension 等等 。这样非常麻烦,而且一不小心就会露掉一些细节,这样会导致错误的结果 。
很多人误以为设置
android:configChanges="orientation" 就可以实现当屏幕方向改变, Activity 被销毁时对其状态的保护,这样就万事大吉了,但是事实并非如此 。因为我们往往误以为只有屏幕旋转会导致 configuration 的改变,但是实际上导致 configuration 改变的原因是很多的,如当设备接入显示器时、改变默认语言时、改变设备的字体时都会导致这种情况的发生 。当 configuration 改变时,系统就会收到相应信号,并把所有的 activities 都销毁,然后再重新创建它们 。所以设置
android:configChanges 的值并不能解决我们的问题 。
4. 一种过时的方案:Override onRetainNonConfigurationInstance()
在
Honeycomb
发布以前,推荐的解决方式是重写
onRetainNonConfigurationInstance() 方法,具体实现细节略 。但是到了 API 13 发布后,这种方式被抛弃了 。因为 Fragment 的 setRetainInstance(boolean) 方法也能实现这种功能,而且更加强大 。
5. 推荐的方式:在 Fragment 中保存 Object
自从 Android 3.0 发布后,我们就可以利用它新添加的 Fragment 来保存 activity 的相关状态 。默认情况下,Fragments 会随着它的 parent Activity 的销毁而销毁,也随之重新创建 。但是调用 Fragment 的 serRetainInstance(true) 方法能让我们改变这种默认的行为,做这样的调用后,当 activity 被销毁时,系统会保留它的相应 fragments ,而不是销毁它们 。后面我们可以通过实例看到,这对于保持类似于 Threads、AsyncTasks、Sockets 等 objects 是非常有效的 。
下面的这个例子演示了当 configuration 改变时,保存 Fragments 的方法 。它保证了我们总是能够把进度的更新信息传递给正确的 activity ,而且不会因 configuration 的改变而出错,也不会 leak 掉某个 AsynTask 。
(1)MainActivity.java
/**
* 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() { ... }
}
(2)TaskFragment.java
/**
* 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.
*/
static 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 。当 configuration 改变时,MainActivity 会执行其相应的生命周期方法,即被销毁后再被重新创建 。新创建的 Activity 实例会由 onAttach(Activity) 方法传递给
TaskFragment
,所以
TaskFragment 总是能引用到当前的 Activity 实例 ,这种实例即简单又可靠 。framework 会负责 activity 实例的销毁和重新创建的工作,而且
TaskFragment 和它的 AsyncTask 不会被销毁(不管 configuration 如何改变) 。注意:在 onDetach() 和 onAttach() 的执行期间,onPostExecute() 是不可能被执行的,系统会保证这一点 。
6. 结语
最后写个 demo 来演示一下在 configuration 变化时,保持后台任务状态的具体做法,代码就不贴出来了,下载地址
http://download.csdn.net/detail/huanfengyun/8097837
运行效果图