我们之前在很多方案都会提到该方案有AsyncTask和Activity LifeCycle的问题,但是之前都没有详细展开说,今天我们就讨论一下到底AsyncTask和Activity配合使用时会发生什么问题。
Activity Leaks
我们参照Retrofit的示例代码写一个简单的Demo,这个Demo用来获取GitHub中square用户下的retrofit项目的所有贡献者的昵称,主要代码如下:
public class MainActivity extends ActionBarActivity {
TextView mTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView) findViewById(R.id.tv_content);
// the AsyncTask will cause memory leaks
new ShowContributorsMemoryLeak().execute();
}
private class ShowContributorsMemoryLeak extends AsyncTask<Void, Void, List<Contributor>> {
@Override
protected List<Contributor> doInBackground(Void... params) {
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint("https://api.github.com")
.build();
return restAdapter.create(GitHub.class).contributors("square", "retrofit");
}
@Override
protected void onPostExecute(List<Contributor> contributors) {
super.onPostExecute(contributors);
StringBuilder stringBuilder = new StringBuilder();
for (Contributor contributor : contributors) {
stringBuilder.append(contributor.login).append(System.getProperty("line.separator"));
}
mTextView.setText(stringBuilder.toString());
}
}
}
代码很简单,我们实现了一个AsyncTask的子类ShowContributorsMemoryLeak
,在该类内部请求网络数据,并将结果处理然后展示到界面上。该代码运行效果良好,但是,可能会造成Activity对象泄漏。
原因很简单,我们都知道内部类会一直保持一个父类的实例的引用,所以AsyncTask中的Thread运行多长时间他就会保存其父对象多长时间。如果用户频繁关闭、打开该界面或者频繁旋转屏幕,可能会造成程序Crash。
知道了问题之后,那么如何解决呢?
锁定屏幕方向?嗯,这是个好实践,如果你不觉得旋转屏幕能给用户提供更好的体验的话,确实应该锁死屏幕。不过这个解决用户频繁打开关闭的问题。
设置请求超时?你不是说AsyncTask运行多长时间就保存多长时间父对象嘛,我让AsyncTask运行时间短点不就行了。但是其实这仍然只是个治标不治本的方案,虽然这样做可能程序Crash几率降低很多,但是仍然有一段时间你的程序会占用很多额外内存,考虑到Java GC中的generations的话,这一段时间可能会很长。
其实这个问题有个比较简单的方案,就是WeakReference
,修改原有代码,
1. 将AsyncTask的子类设置为静态。
2. 将TextView传递进去,并使用WeakReference<TextView>
保存起来。
修改如下:
public class MainActivity extends ActionBarActivity {
TextView mTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
mTextView = (TextView) findViewById(R.id.tv_content);
// the AsyncTask will not cause memory leak
new ShowContributorsNoMemoryLeak(mTextView).execute();
}
static class ShowContributorsNoMemoryLeak extends AsyncTask<Void, Void, List<Contributor>> {
public WeakReference<TextView> mWeakTextView;
public ShowContributorsNoMemoryLeak(TextView textView) {
this.mWeakTextView = new WeakReference<TextView>(textView);
}
...
@Override
protected void onPostExecute(List<Contributor> contributors) {
...
TextView textView = mWeakTextView.get();
if (textView !=null) {
textView.setText(stringBuilder.toString());
}
}
}
}
其中省略了大多数代码
以上代码就不会造成Activity泄露了。嗯,只是不会造成Activity的泄露。
AsyncTask对象
其实上面的代码中,AsyncTask对象还没被销毁。那AsyncTask怎么办呢?其实AsyncTask对象比Activity对象小很多,AsyncTask留存一段时间也无所谓的……
但是AsyncTask在android4.0以后所采用的线程池改为顺序执行,也就是说你的APP同一时间只能有一个线程在运行。假如用户打开这个Activity之后立马旋转了一下屏幕,造成的结果就是
1. 运行了两次AsyncTask,第一次是完全浪费了。
2. 用户看到的是第二次请求的结果,而且第二次请求必须在第一次请求完成之后才会开始。
Google Developer文档中有详细介绍如何处理这种情况,详细介绍参见Handling Runtime Change,按照文档我们修改一下代码如下:
public class MainActivity extends ActionBarActivity {
TextView mTextView;
private RetainedFragment dataFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// find the retained fragment on activity restarts
FragmentManager fm = getFragmentManager();
dataFragment = (RetainedFragment) fm.findFragmentByTag("data");
// create the fragment and data the first time
if (dataFragment == null) {
// add the fragment
dataFragment = new RetainedFragment();
fm.beginTransaction().add(dataFragment, "data").commit();
// load the data from the web
dataFragment.setData(new ShowContributorsNoMemoryLeakAndSaveInstance(this));
dataFragment.getData().execute();
} else {
ShowContributorsNoMemoryLeakAndSaveInstance task = dataFragment.getData();
if (task.mContributors == null) {
showContributors(task.mContributors);
} else {
task.changeActivity(this);
}
}
}
private void showContributors(String contributor) {
mTextView.setText(contributor);
}
static public class RetainedFragment extends Fragment {
// data object we want to retain
private ShowContributorsNoMemoryLeakAndSaveInstance data;
// this method is only called once for this fragment
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// retain this fragment
setRetainInstance(true);
}
public void setData(ShowContributorsNoMemoryLeakAndSaveInstance data) {
this.data = data;
}
public ShowContributorsNoMemoryLeakAndSaveInstance getData() {
return data;
}
}
static class ShowContributorsNoMemoryLeakAndSaveInstance extends AsyncTask<Void, Void, List<Contributor>> {
private String mContributors;
private WeakReference<Activity> mWeakActivity;
public ShowContributorsNoMemoryLeakAndSaveInstance(Activity activity) {
this.mWeakActivity = new WeakReference<Activity>(activity);
}
public void changeActivity(Activity activity) {
mWeakActivity = new WeakReference<Activity>(activity);
}
...
@Override
protected void onPostExecute(List<Contributor> contributors) {
super.onPostExecute(contributors);
StringBuilder stringBuilder = new StringBuilder();
for (Contributor contributor : contributors) {
stringBuilder.append(contributor.login).append(System.getProperty("line.separator"));
}
mContributors = stringBuilder.toString();
Activity activity = mWeakActivity.get();
if (activity !=null) {
((MainActivity)activity).showContributors(stringBuilder.toString());
}
}
}
}
其中省略了部分代码。呃呃,确实即使省略了很多代码,仍然还有一大坨代码啊,不过好在还是解决了我们之前提到的问题。
总结
在Activity中使用AsyncTask确实会造成很多问题,虽然我们讨论了如何解决这些问题,但是实际上解决这些问题需要额外写一大坨代码,好在在实践中我们有很多lib能减轻不少这些工作。下一篇我们看看我们最终的网络请求框架是什么样子的。
_以上程序涉及到的代码参见NetworkSample