作为一名Android开发者,你一定经常在思考如下场景:如果在一个Activity中我们发送一个请求,然后请求返回之前,由于某种原因(比如:旋转屏幕或者更改语言),导致Activity重新创建,此时如何优雅地复用之前的请求,同时也不导致Activity泄漏?
对于Android中这个典型的场景,我列举了如下比较常见的解决方案,当然每个方案都有其优缺点,文末会推荐其中两种作者认为较好的实现方式。
模拟场景
网络请求
一个简单的模拟网络请求Task,每隔200ms更新一下进度,直到操作完成。
public class NetWorkTask extends Thread {
private volatile ProgressUpdateLinster progressUpdateLinster;
private Handler handler = new Handler(Looper.getMainLooper());
public NetWorkTask(ProgressUpdateLinster progressUpdateLinster) {
this.progressUpdateLinster = progressUpdateLinster;
}
private int progress = 0;
@Override
public void run() {
while (progress <= 100) {
if(progressUpdateLinster != null) {
handler.post(new Runnable() {
@Override
public void run() {
progressUpdateLinster.updateProgress(progress);
}
});
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
return;
}
progress += 2;
}
}
public interface ProgressUpdateLinster {
void updateProgress(int progress);
}
public void cacel() {
interrupt();
}
public void setProgressUpdateLinster(ProgressUpdateLinster progressUpdateLinster) {
this.progressUpdateLinster = progressUpdateLinster;
}
}
简单的布局,包括一个进度条和一个展示进度的文案
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<ProgressBar
android:layout_width="200dp"
android:layout_height="20dp"
style="@android:style/Widget.ProgressBar.Horizontal"
android:id="@+id/progressbar"
android:max="100"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/progressbar"
android:layout_marginLeft="10dp"
android:textColor="#ff0051"
android:id="@+id/tv_progroess"
/>
</RelativeLayout>
一个非常简单的Activity
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private ProgressBar progressBar;
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
progressBar = (ProgressBar) findViewById(R.id.progressbar);
textView = (TextView) findViewById(R.id.tv_progroess);
new NetWorkTask(new NetWorkTask.ProgressUpdateLinster() {
@Override
public void updateProgress(int progress) {
progressBar.setProgress(progress);
textView.setText(progress+"%");
Log.d(TAG,MainActivity.this.toString());
}
}).start();
}
在修改手机的语言环境时,Activity会被重建,存在两个问题:
- 重建之前旧的Activity会被Thread持有,(活的Thread是一个GC Root,是不能回收的),导致Activity内存泄漏。
- 网络请求会被发送两次
我们可以通过改一下手机语言,导致Activity重建看一下效果:
default02.gif可以看到Activity重建之后,NetWorkTask会重新执行(进度从0开始),而且旧的Activity并不会马上销毁(可以看log)。注意Activity的标签,在重建之后自动由“Label”变成了“标签”,我们对标签配置了中文和英文两种语言。
为了解决上面两个问题,我们有如下方案:
一 不让Activity重建
既然系统会让Activity进行重建,那我们是不是可以拦截这些变化,不让Activity重建呢?自然而然就想到了配置Activity的android:configChanges
,比如在我们这个案例里我们配置android:configChanges="locale|layoutDirection"
,然后把语言环境从US切换到天朝,效果如下。
注意两个细节:1、由于Activity并没有重建,所以Activity的标签栏在语言环境改变之后并没有自动如愿地改成我们想要的“标签”。我们需要在onConfigurationChanged中手动更新设置一下Activity的标签字符串。2、我们拦截了两种设备配置变化,
locale|layoutDirection
,为什么要拦截layoutDirection
呢?因为语言环境的改变也会触发layoutDirection条件变化...此外,比如我们常用的屏幕翻转也会触发屏幕大小配置变化,如果我们漏掉其中任何一个属性配置,那么Activity还是会被重建。
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
getSupportActionBar().setTitle(R.string.lab_str);
}
优点
简单,只需要在manifest文件中增加一些属性即可
缺点
系统在设备配置变化时,默认需要重建的原因是可以为新的属性适配相应的资源,在我们的列子中,只是简单的适配了一下ActionBar的title,但是如果我们当前页面需要适配各种布局,像素大小就悲剧了,这些适配都需要我们自己手动在onConfigurationChanged里面进行;同时,Activity重建的相关配置属性都需要在manifest中配置,配置越多,onConfigurationChanged中的处理逻辑就越复杂。
二 onRetainNonConfigurationInstance
在Activity由于设备配置导致重建,系统会调用onRetainNonConfigurationInstance
方法,我们可以返回任何类型的对象进行保存,在Activity被重建之后,我们可以调用getLastNonConfigurationInstance
来获取保持的对象。我们修改Activity的代码如下,注意,在FragmentActivity中把上面两个方法包装成了onRetainCustomNonConfigurationInstance和getLastCustomNonConfigurationInstance
,如果你是直接继承自系统Activity,使用上面所说的方法。
private ProgressBar progressBar;
private TextView textView;
private static final String TAG = "MainActivity";
NetWorkTask netWorkTask = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
progressBar = (ProgressBar) findViewById(R.id.progressbar);
textView = (TextView) findViewById(R.id.tv_progroess);
if(getLastCustomNonConfigurationInstance() != null
&& getLastCustomNonConfigurationInstance() instanceof NetWorkTask) {
= (NetWorkTask) getLastCustomNonConfigurationInstance();
}else {
= new NetWorkTask();
netWorkTask.setProgressUpdateLinster(linster);
netWorkTask.start();
}
}
private NetWorkTask.ProgressUpdateLinster linster = new NetWorkTask.ProgressUpdateLinster() {
@Override
public void updateProgress(int progress) {
progressBar.setProgress(progress);
textView.setText(progress+"%");
Log.d(TAG,MainActivity.this.toString());
}
};
@Override
public Object onRetainCustomNonConfigurationInstance() {
return netWorkTask;
}
效果如下,界面进行了重建,同时network
也只被执行了一次,同时由于我们重新set了linster,network所在的Thread不再持有原来Activity的引用,所以不会导致Activity泄漏。
优点
简单。。。
缺点
- 只能处理由于设备配置变化导致的Activity重建的情况
- Activity需要参与状态保存和恢复
三 Retain Fragment
在Android3.0之后,官方建议我们使用Retain Fragment来处理这种场景。增加一个work Fragment:
public class WorkFragment extends Fragment {
NetWorkTask netWorkTask = null;
/**
* 重建之后这里的Context会自动替换成新的Activity
* @param context
*/
@Override
public void onAttach(Context context) {
super.onAttach(context);
//第一次启动的时候,这里network还没有初始化
//Activity重建之后,更新回调
if(netWorkTask != null) {
netWorkTask.setProgressUpdateLinster((NetWorkTask.ProgressUpdateLinster) context);
}
}
@Override
public void onDetach() {
super.onDetach();
netWorkTask.setProgressUpdateLinster(null);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//设置为retain instance Fragment
setRetainInstance(true);
netWorkTask = new NetWorkTask();
netWorkTask.setProgressUpdateLinster((NetWorkTask.ProgressUpdateLinster) getActivity());
netWorkTask.start();
}
}
修改Activity代码:
public class MainActivity extends AppCompatActivity implements NetWorkTask.ProgressUpdateLinster {
private ProgressBar progressBar;
private TextView textView;
private static final String TAG = "MainActivity";
private static final String TAG_TASK_FRAGMENT = "work";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
progressBar = (ProgressBar) findViewById(R.id.progressbar);
textView = (TextView) findViewById(R.id.tv_progroess);
//如果已经有了work fragment,那就不需要再新建了
if(getSupportFragmentManager().findFragmentByTag(TAG_TASK_FRAGMENT) == null) {
getSupportFragmentManager().beginTransaction().add(new
}
}
@Override
public void updateProgress(int progress) {
progressBar.setProgress(progress);
textView.setText(progress+"%");
}
}
Retain Fragment的生命周期可以跨越Activity的重建周期,相比较第二种方式,使用Work Fragment可以将各种状态保存和恢复模块到Fragment中,代码组织更加合理,同时它的应用场景不仅仅再针对于由于设备配置变化导致的重建。
以上三种方式都是基于Android自身API实现,其中第三种是官方推荐的方式。
四 EventBus
如果我们使用了EventBus之类的第三方库,我们可以这样做:
-
新建一个事件类型
public class ProgressUpdateEvent { public final int progress; public ProgressUpdateEvent(int progress) { this.progress = progress; } }
2.修改Activity代码
public class MainActivity extends AppCompatActivity {
private ProgressBar progressBar;
private TextView textView;
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
progressBar = (ProgressBar) findViewById(R.id.progressbar);
textView = (TextView) findViewById(R.id.tv_progroess);
//将当前Activity注册到EventBus
EventBus.getDefault().register(this);
//如果是重建的Activity不需要再次新建NetworkTask
if(savedInstanceState == null) {
new NetWorkTask(l).start();
}
}
/**
* 接受到事件
* @param event
*/
public void onEventMainThread(ProgressUpdateEvent event) {
progressBar.setProgress(event.progress);
textView.setText(event.progress+"%");
}
/**
* 注意,这里是一个静态内部类,不会持有任何外部类的引用
*/
static NetWorkTask.ProgressUpdateLinster l = new NetWorkTask.ProgressUpdateLinster() {
@Override
public void updateProgress(int progress) {
EventBus.getDefault().post(new ProgressUpdateEvent(progress));
}
};
@Override
protected void onDestroy() {
super.onDestroy();
//注销订阅
EventBus.getDefault().unregister(this);
}
}
总结
Activity在重建时,有可能导致请求重新发送和Activity内存泄漏,针对这两个问题,本文提出了四种方式来处理这种场景,其中第一种和第二种只能处理由于设备配置变化导致的Activity重建,第三种是官方推荐的一种方式,第四种使用第三方库Eventbus来处理,建议大家尽量使用第三种和第四种处理方式。