「译」通过Fragment处理配置变化

原文来自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!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,431评论 25 707
  • afinalAfinal是一个android的ioc,orm框架 https://github.com/yangf...
    passiontim阅读 15,396评论 2 45
  • 《Android Fragment完全解析,关于碎片你所需知道的一切》 我们都知道,Android上的界面展示都是...
    cxm11阅读 2,174评论 2 19
  • 能走路绝不跑步,能站着绝不走路,能坐着绝不站着,能躺着绝不坐着,说的不就是像小编一样的懒癌患者吗?大概只能被封印在...
    潮流一起说阅读 237评论 0 0
  • 也许我们并不认为自己的谈话方式是“暴力”的,但我们的语言确实常常引发自己和他人的痛苦。 我们每个人或许都遇到给自己...
    成龙XYZ阅读 790评论 0 0