【译】用Fragment解决屏幕旋转(状态发生变化)状态不能保持的问题
这篇文章解决了在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属性禁止默认的销毁和重新创建行为。这个简单的方法使得它对开发者很有吸引力;然而Google的工程师建议不这么做。主要的担忧是:配置后,需要你在代码中手动处理设备的配置变化。处理配置变化需要你采取很多额外的处理,以确保每一个字符串、布局、绘图、尺寸等与当前设备的配置一致。如果你不小心,那么你的应用程序可能会有一系列与资源定制方面有关的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...
/**
* 这个Activity主要用来展示UI,创建一个TaskFragment来管理任务,
* 从TaskFragment接收进度以及执行结果.
*/
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); //如果Fragment不为null,那么它就是在配置变化的时候被保存下来的
if (mTaskFragment == null) {
mTaskFragment = new TaskFragment();
fm.beginTransaction().add(mTaskFragment, TAG_TASK_FRAGMENT).commit();
} // TODO: 初始化View, 还原保存的状态, 等等.
} //下面四个方法将在进度需要更新或者返回结果的时候被调用。
//MainActivity需要更新UI来反应这些变化。
@Override
public void onPreExecute() { ... } @Override
public void onProgressUpdate(int percent) { ... } @Override
public void onCancelled() { ... } @Override
public void onPostExecute() { ... }
}
...和一个 TaskFragment...
/**
* 这个Fragment管理一个后台任务,在状态发生变化的时候能够保存下来,不被销毁
*/
public class TaskFragment extends Fragment { /**
* 让Fragment通知Activity任务进度和返回结果的回调接口
*/
static interface TaskCallbacks {
void onPreExecute();
void onProgressUpdate(int percent);
void onCancelled();
void onPostExecute();
} private TaskCallbacks mCallbacks;
private DummyTask mTask; /**
* 持有一个父Activity的引用,以便在任务进度变化和需要返回结果的时候通知它。
* 在每一次配置变化后,Android Framework会将新创建的Activity的引用传递给我们
*/
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mCallbacks = (TaskCallbacks) activity;
} /**
*这个方法只会被调用一次,只在这个被保存Fragment第一次被创建的时候
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); //在配置变化的时候将这个fragment保存下来
setRetainInstance(true); // 创建并执行后台任务
mTask = new DummyTask();
mTask.execute();
} /**
* 设置回调对象为null,防止我们意外导致Activity实例泄露(leak the Activity instance)
*/
@Override
public void onDetach() {
super.onDetach();
mCallbacks = null;
} /**
* 一个示例性的任务用来表示一些后台任务并且通过回调函数向Activity
* 报告任务进度和返回结果
*
* 注意:我们需要在每一个方法中检查回调对象是否为null,以防它们
* 在Activity或Fragment的onDestroy()执行后被调用。
*/
private class DummyTask extends AsyncTask<Void, Integer, Void> { @Override
protected void onPreExecute() {
if (mCallbacks != null) {
mCallbacks.onPreExecute();
}
} /**
* 注意:我们不在后台线程的doInbackground方法中直接调用回调
* 对象的方法,因为这样可能产生竞态条件
*/
@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并通过TaskCallBack接口将任务处理进度和结果回传给MainActivity。当配置变化时,MainActivity正常地经历它的生命周期事件(销毁、重新创建、onResume等),但是一旦新Activity实例被重新创建,那它就会被传递到onAttach(Activity)方法中,这就保证了TaskFragment会一直持有当前显示的新Activity实例的引用,即使是在配置发生变化的时候。另外值得注意的是,onPostExecute()不会在onDetach()和onAttach()的之间被调用。具体的解释参考StackOverflow的回答以及我在Google+文章中给Doug Stevenson的回复(在评论中也有一些关于它的讨论)。
总结
在Activity的生命周期(涉及旧、新Activity的销毁和创建)中同步后台任务的运行状态可能非常棘手,并且配置发生变化加剧了这一麻烦。幸运的是使用一个retain fragment可以非常轻松地处理这些事件。它只要始终持有父Activity的引用,即使在父Activity被销毁和重新创建之后。
一个示例型的App演示怎么正确地使用Retained Fragment来达到我们的目的,Play Store下载地址。托管在Github上的源代码。下载它,用Eclipse执行Import,然后自己随意修改。

本文永久链接:http://www.cnblogs.com/kissazi2/p/4116456.html
【译】用Fragment解决屏幕旋转(状态发生变化)状态不能保持的问题的更多相关文章
- Runtime解决屏幕旋转问题
前言 大家或许在iOS程序开发中经常遇到屏幕旋转问题,比如说希望指定的页面进行不同的屏幕旋转,但由于系统提供的方法是导航控制器的全局方法,无法随意的达到这种需求.一般的解决方案是继承UINavrgat ...
- AJ学IOS 之控制器view显示中view的父子关系及controller的父子关系_解决屏幕旋转不能传递事件问题
AJ分享,必须精品 一:效果 二:项目代码 这个Demo用的几个控制器分别画了不通的xib,随便拖拽了几个空间,主要是几个按钮的切换,主要代码展示下: // // NYViewController.m ...
- Android动态禁用或开启屏幕旋转工具
package com.gwtsz.gts2.util; import android.content.Context; import android.provider.Settings; impor ...
- 禁止屏幕旋转并同时解决以至于导致Activity重启的方法
1.禁止屏幕旋转在AndroidManifest.xml的每一个需要禁止转向的Activity配置中加入android:screenOrientation属性. //landscape(横向)port ...
- 监听iOS检测屏幕旋转状态,不需开启屏幕旋转-b
-(void)rotation_icon:(float)n { UIButton *history_btn= [self.view viewWithTag:<#(NSInteger)#>] ...
- 监听iOS检测屏幕旋转状态,不需开启屏幕旋转
-(void)rotation_icon:(float)n { UIButton *history_btn= [self.view viewWithTag:<#(NSInteger)#>] ...
- Android 屏幕旋转 处理 AsyncTask 和 ProgressDialog 的最佳方案
的最佳方案 标签: Android屏幕旋转AsyncTaskProgressDialog 2014-07-19 09:25 39227人阅读 评论(46) 收藏 举报 分类: [android 进阶之 ...
- android学习---屏幕旋转
/** *问题:今天学习android访问Servlet,Servlet给返回一个xml格式的字符串,android得到数据后将其显示到一个TextView中,发现Activity得到数据显 * 示到 ...
- iOS学习笔记(3)— 屏幕旋转
一.屏幕旋转机制: iOS通过加速计判断当前的设备方向和屏幕旋转.当加速计检测到方向变化的时候,屏幕旋转的流程如下: 1.设备旋转时,系统接收到旋转事件. 2.系统将旋转事件通过AppDelegate ...
随机推荐
- ue4 plugin的编译加载
插件Plugin: 本来应该是指一种纯以接口与外界打交道的程序模块,在同一接口背后可以有多种实现,更换实现完全不影响客户端代码(不用重编). 但是在ue4的世界里,插件似乎不是这个意思,仅仅是一种可以 ...
- if语句
Python是一门用于编程的语言,所以必要的判断是一定有的,本章介绍的就是Python的判断语句if判断. 因为Python在一句代码结束的时候没有符号来明确的标记,这就造成了Python的if语句和 ...
- leetcode解题:Add binary问题
顺便把之前做过的一个简单难度的题也贴上来吧 67. Add Binary Given two binary strings, return their sum (also a binary strin ...
- Linux添加/删除用户和用户组
声明:现大部分文章为寻找问题时在网上相互转载,在此博客中做个记录,方便自己也方便有类似问题的朋友,故原出处已不好查到,如有侵权,请发邮件表明文章和原出处地址,我一定在文章中注明.谢谢. 本文总结了Li ...
- 虚拟机上安装ArchLinux笔记
安装前的自白: 想使用ArchLinux,就直接在虚拟机上先装一个玩起来先.虚拟机使用的是Vmware,下载免费的个人版本就可以了. Arch Linux的版本为2016.4.1 内核为4.4.5 在 ...
- Runloop 深入理解(转)
RunLoop 是 iOS 和 OSX 开发中非常基础的一个概念,这篇文章将从 CFRunLoop 的源码入手,介绍 RunLoop 的概念以及底层实现原理.之后会介绍一下在 iOS 中,苹果是如何利 ...
- third class
09remain timer 1.button的背景改变:放在背景图片里面,改变背景图片的位置,这样更简洁 08 simple clock 1.上下padding一样,居中2.setInterval( ...
- win7/win8远程桌面 server 2003 卡的问题
原因在于从vista开始,微软在TCP/IP协议栈里新加了一个叫做“Window Auto-Tuning”的功能.这个功能本身的目的是为了让操作系统根据网络的实时性能(比如响应时间)来动态调整网络上传 ...
- 通过实现Countable接口来调用count函数
周六我一大早就来到公司,还有些客户工作没有收尾,还有写文档没写,还有写计划需要完善,我得抓紧.到了下午我发现大家陆陆续续的都到公司来了,有几个兄弟一来就开始工作了,每当有人自愿投入某一项工作时,我基本 ...
- iOS-三方框架AFNetworking基本使用
AFNetworking 是基于NSURLConnection, NSOperation开发的一款三方框架,主要用于处理一些关于网络请求上的业务,下文会简单介绍框架中经常使用的功能,如文件的上传,下载 ...