博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
【译】用Fragment解决屏幕旋转(状态发生变化)状态不能保持的问题
阅读量:5941 次
发布时间:2019-06-19

本文共 6316 字,大约阅读时间需要 21 分钟。

这篇文章解决了在StackOverflow上一个经常被提到的问题。

在配置发生变化(Configuration changs)时,什么是最好的保存活动对象方法,比如运行中的线程,Sockets,AsyncTask

要回答这个问题,我们要先讨论一些开发者在Activity生命周期中使用长时间后台任务时遇到的共同困难。然后,我们将介绍常见的两种能解决问题但有不好的方法。最后,我们会用一个示例代码说明推荐的解决方案,它用retained fragment来达到我们的目标。

配置改变&后台线程(Configuration Changes & Background Tasks)

配置发生变化以及销毁和重新创建穿越了整个Activity的生命周期,并且引出一个问题,那就是这些事件的发生是不可预测并且在任何时候都可能触发。并发的后台线程只加剧了这个问题。假设在Activity中启动了一个AsyncTask,然后用户马上旋转屏幕,这会导致Activity被销毁和重新创建。当AsyncTask最后完成它的任务,它会将结果反馈到旧的Activity实例,完全没有意识到新的activity已经被创建了。似乎这不是一个问题,新的Activity实例又会让浪费宝贵的资源重新启动一个后台线程,而不知道旧的AsyncTask已经在运行。由于这些原因,在配置变化的时候我们需要正确、有效地保存在Activity实例的活动对象。

不好的实践:保存整个Activity

可能最有效和最常被滥用的解决方法是通过在Android manifest中设置android:configChanges属性禁止默认的销毁和重新创建行为。这个简单的方法使得它对开发者很有吸引力;然而建议不这么做。主要的担忧是:配置后,需要你在代码中手动处理设备的配置变化。处理配置变化需要你采取很多额外的处理,以确保每一个字符串、布局、绘图、尺寸等与当前设备的配置一致。如果你不小心,那么你的应用程序可能会有一系列与资源定制方面有关的Bug。

另一个Google不鼓励使用它的原因是许多开发者错误地认为,设置android:configChanges = "orientation"(这只是举例说明),会神奇地避免他们的Activity在不可预知的场景中被销毁和重新创建。其实不是这样的。有多种原因可能导致配置发生变化,而不单单是屏幕横竖屏的变化。将你的手机中的内容显示在显示器上,更改默认语言,修改设备默认的字体缩放,这三个简单的例子都有可能触发设备的配置变化。这些事件会向系统发出信号,销毁并重建所有正在运行的Activity,在它们下一次resume的时候。所以设置android:configChanges属性一般不是好的做法。

已经被弃用的方法:重写onRetainNonConfigurationInstance()

在Honeycomb发布前,跨越Activity实例传递活动对象的推荐方法是重写onRetainNonConfigurationInstance()getLastNonConfigurationInstance()方法。使用这种方法,传递跨越Activity 实例的活动对象仅仅需要在onRetainNonConfigurationInstance()将活动对象返回,然后在getLastNonConfigurationInstance()中取出。截止API 13,这些方法都已经被弃用,以支持更有效的Fragment的setRetainInstance(boolean)方法。它提供了一个更简洁,更模块化的方式在配置变化的时候保存对象。我们将在下一节讨论以Fragment为基础的方法。

推荐的方法:在Retained Fragment中管理对象

自从Android3.0推出Fragment。跨越Activity保留活动对象的推荐方法是在一个Retained Fragment中包装和管理它们。默认情况下,但配置发生变化时,Fragment会随着它们的宿主Activity被创建和销毁。调用Fragment#setRetaininstance(true)允许我们跳过销毁和重新创建的周期。指示系统保留当前的fragment实例,即使是在Activity被创新创建的时候。不难想到使用fragment持有像运行中的线程、AsyncTask、Socket等对象将有效地解决上面的问题。

下面代码演示如何使用fragment在配置发生变化的时候保存AsyncTask的状态。这段代码保证了最新的进度和结果能够被传回更当前正在显示的Activity实例,并确保我们不会在配置发生变化的时候丢失AsyncTask的状态。下面代码包含两个类,一个MainActivity...

1 /** 2  * 这个Activity主要用来展示UI,创建一个TaskFragment来管理任务, 3  * 从TaskFragment接收进度以及执行结果. 4  */ 5 public class MainActivity extends Activity implements TaskFragment.TaskCallbacks { 6  7   private static final String TAG_TASK_FRAGMENT = "task_fragment"; 8  9   private TaskFragment mTaskFragment;10 11   @Override12   protected void onCreate(Bundle savedInstanceState) {13     super.onCreate(savedInstanceState);14     setContentView(R.layout.main);15 16     FragmentManager fm = getFragmentManager();17     mTaskFragment = (TaskFragment) fm.findFragmentByTag(TAG_TASK_FRAGMENT);18 19     //如果Fragment不为null,那么它就是在配置变化的时候被保存下来的20     if (mTaskFragment == null) {21       mTaskFragment = new TaskFragment();22       fm.beginTransaction().add(mTaskFragment, TAG_TASK_FRAGMENT).commit();23     }24 25     // TODO: 初始化View, 还原保存的状态, 等等.26   }27 28   //下面四个方法将在进度需要更新或者返回结果的时候被调用。29   //MainActivity需要更新UI来反应这些变化。30   @Override31   public void onPreExecute() { ... }32 33   @Override34   public void onProgressUpdate(int percent) { ... }35 36   @Override37   public void onCancelled() { ... }38 39   @Override40   public void onPostExecute() { ... }41 }

...和一个 TaskFragment...

1 /**  2  * 这个Fragment管理一个后台任务,在状态发生变化的时候能够保存下来,不被销毁  3  */  4 public class TaskFragment extends Fragment {  5   6   /**  7    * 让Fragment通知Activity任务进度和返回结果的回调接口  8    */  9   static interface TaskCallbacks { 10     void onPreExecute(); 11     void onProgressUpdate(int percent); 12     void onCancelled(); 13     void onPostExecute(); 14   } 15  16   private TaskCallbacks mCallbacks; 17   private DummyTask mTask; 18  19   /** 20    * 持有一个父Activity的引用,以便在任务进度变化和需要返回结果的时候通知它。 21    * 在每一次配置变化后,Android Framework会将新创建的Activity的引用传递给我们 22    */ 23   @Override 24   public void onAttach(Activity activity) { 25     super.onAttach(activity); 26     mCallbacks = (TaskCallbacks) activity; 27   } 28  29   /** 30     *这个方法只会被调用一次,只在这个被保存Fragment第一次被创建的时候 31    */ 32   @Override 33   public void onCreate(Bundle savedInstanceState) { 34     super.onCreate(savedInstanceState); 35  36     //在配置变化的时候将这个fragment保存下来 37     setRetainInstance(true); 38      39  40     // 创建并执行后台任务 41     mTask = new DummyTask(); 42     mTask.execute(); 43   } 44  45   /** 46    * 设置回调对象为null,防止我们意外导致Activity实例泄露(leak the Activity instance) 47    */ 48   @Override 49   public void onDetach() { 50     super.onDetach(); 51     mCallbacks = null; 52   } 53  54   /** 55    * 一个示例性的任务用来表示一些后台任务并且通过回调函数向Activity 56    * 报告任务进度和返回结果 57    * 58    * 注意:我们需要在每一个方法中检查回调对象是否为null,以防它们 59    * 在Activity或Fragment的onDestroy()执行后被调用。 60    */ 61   private class DummyTask extends AsyncTask
{ 62 63 @Override 64 protected void onPreExecute() { 65 if (mCallbacks != null) { 66 mCallbacks.onPreExecute(); 67 } 68 } 69 70 /** 71 * 注意:我们不在后台线程的doInbackground方法中直接调用回调 72 * 对象的方法,因为这样可能产生竞态条件 73 */ 74 @Override 75 protected Void doInBackground(Void... ignore) { 76 for (int i = 0; !isCancelled() && i < 100; i++) { 77 SystemClock.sleep(100); 78 publishProgress(i); 79 } 80 return null; 81 } 82 83 @Override 84 protected void onProgressUpdate(Integer... percent) { 85 if (mCallbacks != null) { 86 mCallbacks.onProgressUpdate(percent[0]); 87 } 88 } 89 90 @Override 91 protected void onCancelled() { 92 if (mCallbacks != null) { 93 mCallbacks.onCancelled(); 94 } 95 } 96 97 @Override 98 protected void onPostExecute(Void ignore) { 99 if (mCallbacks != null) {100 mCallbacks.onPostExecute();101 }102 }103 }104 }

事件流

MainActivity第一次启动的时候,它实例化并将TaskFragment添加到Activity的状态中。TaskFragment创建并执行AsyncTask并通过TaskCallBack接口将任务处理进度和结果回传给MainActivity。当配置变化时,MainActivity正常地经历它的生命周期事件(销毁、重新创建、onResume等),但是一旦新Activity实例被重新创建,那它就会被传递到onAttach(Activity)方法中,这就保证了TaskFragment会一直持有当前显示的新Activity实例的引用,即使是在配置发生变化的时候。另外值得注意的是,onPostExecute()不会在onDetach()onAttach()的之间被调用。具体的解释参考以及我在中给Doug Stevenson的回复(在评论中也有一些关于它的讨论)。

总结

在Activity的生命周期(涉及旧、新Activity的销毁和创建)中同步后台任务的运行状态可能非常棘手,并且配置发生变化加剧了这一麻烦。幸运的是使用一个retain fragment可以非常轻松地处理这些事件。它只要始终持有父Activity的引用,即使在父Activity被销毁和重新创建之后。

一个示例型的App演示怎么正确地使用Retained Fragment来达到我们的目的,。托管在。下载它,用Eclipse执行Import,然后自己随意修改。

 

 

本文永久链接:

转载于:https://www.cnblogs.com/kissazi2/p/4116456.html

你可能感兴趣的文章
缇 、 像素 、 厘米
查看>>
qmail+spamassassin启用spf过滤
查看>>
Oracle PL/SQL Developer集成TFS进行团队脚本文件版本管理
查看>>
自动调整速率的Actor设计模式
查看>>
clouderamanager-server启动,log日志中说需要mysql驱动的解决办法
查看>>
LINQ技术、EF技术都出来蛮久了,软件开发者、软件公司是否还有必要有自己的代码生成器?...
查看>>
认识redis
查看>>
吞吐量、带宽、bps、pps、转发能力、线速转发、交换带宽
查看>>
华为交换机命令
查看>>
Linux下实现多网卡绑定
查看>>
jQuery特性效果与链式调用
查看>>
Git常用命令和Github协同流程
查看>>
0040-如何重置Cloudera Manager的admin密码
查看>>
在可编辑表格EditorGrid中,我选择一行已输入的数据,点击删除按钮,该行数据将被删除,然后当我点击表单提交按钮时,已经被删除的那一行数据仍然被插入数据库中...
查看>>
idea +springMVC搭建项目,新建各种文件基础
查看>>
Dubbo发展史谁知道?
查看>>
PS常用快捷键就这些了,记住绘图事半功倍
查看>>
expect,spawn用法小结
查看>>
代码详解|如何快速从硬盘里找到小电影?
查看>>
通讯录(容量不可变)
查看>>