StackOerflow上经常看到类似如下的问题: 在设备配置变更时保持激活对象比如运行中的Threads,Sochets还有AsyncTasks的最好办法是什么?
本文讨论这个问题。首先讨论对于开发者来讲比较难的在Activity生命周期中的长运行后台线程问题。然后讨论两种解决方案。最终给出实例代码。
配置变化和后台任务
首先一个问题是配置改变和Activity建立摧毁这些事件是不可预知随时都有可能发生的。同时发生的后台进程也会出现问题。比如,一个Activity启动了一个AsyncTask然后突然用户旋转了屏幕,这会造成Activity摧毁和重建。当AsyncTask完成时会把返回的结果传递给旧的Activity实例,完全不知到新的Activity已经被建立起来了。就像这不是个问题一样,新的线程可能会再次浪费客观的资源重新开启后台任务,并不知道旧的AsyncTask还在运行。因此,当配置变化时我们需要正确的有效率的维持不同Activity间的激活的对象。
坏的实践:维持Activity
可能最常见和滥用的方法通过更改AndroidManifest的android:configChanges属性是去禁用摧毁重建流程。这种方法明显很简单,让开发者很容易实现。但是谷歌的工程师不鼓励使用这种方法。首要问题是它需要你手动修改代码来控制设备的配置变化。控制设备变更需要你去做许多额外的步骤来保证每一个string字符串、layout,drawable,dimension等等保持和当前设备的配置同步,如果一不小心就会出来一系列的资源相关的bug。
另一个反对的原因是许多开发者不正确的假设比如修改android:configChanges=“orientation”会神奇的保证他们的应用不会被未知的情景摧毁重构Activity。这是不行的。配置变化可能有一系列的原因导致,不仅仅是屏幕方向变化。比如把设备插到显示架、改变设备默认语言、改变默认字体大小等事件都会改变配置信息。也都会造成当前运行的Acitivity摧毁重建。因此改这个属性是不好的实践。
Deprecated:Override
onRetainNonConfigurationInstance()
在Honeycomb出来前,比较好的办法是override重写onRetainNonConfigurationInstance()和getLastNonConfigurationInstance()方法。用这种方法,在activity中改变激活的对象就是在onRetainNonConfigurationInstance()方法里返回激活的对象然后getLastNonConfigurationInstance()里取回它。API13之后这种方法被deprecated,因为Fragment的更多的setRetainInstance(boolean)能力,提供了更干净模块化的方法来维持设备配置变化时的对象。下章讨论基于Fragment的方法。
推荐:在Retained Fragment里管理对象
自从安卓3.0之后,最好的办法就是在保持的工人fragment里保持激活的对象。配置变化时,默认的情况是Fragment随着它的Activity一起摧毁重建。调用Frament的setRetainInstance(True)方法可以避免这个过程,通知系统在activity重建的工程中保持当前的fragment实例。这对保持的running Treads,AsyncTask,Sockets对象非常有用。
下边的例子是在配置变化时在Fragment里维持一个AsyncTask任务,保证异步任务更新返回的结果回到当前的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.
*/
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然后执行AsyncTask,AsyncTask把结果返回给MainActivity并且刷新UI。当配置发生变化时,MainActivity按照正常的生命周期走,新建里的activity走到 onAttach方法,保证TaskFragment能持有当前显示中的Activity实例。设计很简单但很可可靠,应用框架将会处理新的Activity实例然后TaskFragment和它的异步任务也不用担心配置变更。
总结
同步后台任务和Activity生命周期一起会很麻烦,配置变更也会怎加困惑。幸运的事保持的Fragment可以简单的处理这些事件,保持当前的父Activity实例就算它被摧毁重建。
Ref:http://www.androiddesignpatterns.com/2013/04/retaining-objects-across-config-changes.html