概述
AsyncTask是一个轻量级选手,适合处理轻量级的后台任务。处理过程中还可以把处理的进度反馈到主线程中,方便我们更新UI,不需要我们去操作 handler,在早期 Android 版本中是十分方便的工具。但是如果用的不好会引入不少奇怪的问题!
1. Cancel 取消任务不生效
在基础篇 AsyncTask - 基础篇 介绍 Cancel 的时候:
if (myAsyncTask!=null)
myAsyncTask.cancel(true);
通过 cancel 来取消任务,最终会调用到,FutureTask 里面的 cancel 方法,可这样真的会被终止吗?可不一定。FutureTask 可以理解为实际上就是一个线程,cancel() 方法改变了 futureTask 的状态位,
- 如果传入的是false并且业务逻辑已经开始执行,当前任务是不会被终止的,而是会继续执行,直到异常或者执行完毕。
- 如果传入的是true,会调用当前线程的interrupt()方法,把中断标志位设为true。
而 Thread.interrupt() 方法,是提示一个线程应该终止,但不强制该线程终止。所以,AsyncTask.cancel 并不能保证取消任务。
那我们该怎么解决呢?其实,AsyncTask.cancel 在调用后,会改变 AsyncTask.isCancelled() 的返回值,我们在 doInBackground 中就需要时刻去判断这个返回值,放判断到已经取消了,我们要及时地退出任务:
@Override
protected Boolean doInBackground(String... strings) {
Log.i(TAG,"doInBackground...");
int i=0;
while(i<100){
if (isCancelled())
break;
i++;
//任务执行过程进度/参数的回调,对应于泛型:Progress
publishProgress(i);
try {
Thread.sleep(200);
} catch (InterruptedException ignored) {
}
}
//返回值代表最后的结果,对应于泛型:Result
return true;
}
上面程序模拟耗时操作,在每次for循环的开始,都去判断是否已经取消了任务,当取消了任务后就没必要继续执行了,直接break结束。
2. 内存泄漏
AsyncTask 提供了几个运行在主线程的方法给我们重写,我们可以在里面进行UI的更新,但是呢,因为我们要更新UI对吧,那就得把控件引用传进去,这样一来,控件持有着Activity的引用,当,页面退出了,但是 AsyncTask 的线程任务还在后台跑,没有及时地停止,当 Activity 想要销毁的时候,JVM 发现它的引用被 AsyncTask 持有,所以 JVM 不能回收 Activity,因而造成泄漏。
想要解决,我们要规范好 AsyncTask 的使用:
-
及时地 cancel 任务,不说说 cancel 没有效果吗?是没有效果,但我们通过 isCancelled 来判断,在 doInBackground 中取消后续的任务,参考上面的第一点,然后在 Activity 销毁 destroy 中,主动调用 AsyncTask.cancel()。
-
对传入的控件,采用弱引用包裹起来,杜绝强引用。
/* 为避免内存泄漏,引用应该用弱引用包裹 */
private String TAG = "MyAsyncTask";
private WeakReference<Context> contextWeakReference;
private WeakReference<ProgressBar> progressBarWeakReference;
private WeakReference<TextView> textViewWeakReference;
public MyAsyncTask(Context context, ProgressBar progressBar, TextView tvProgress) {
contextWeakReference = new WeakReference<>(context);
progressBarWeakReference = new WeakReference<>(progressBar);
textViewWeakReference = new WeakReference<>(tvProgress);
}
然后在用到控件的地方,要先判空再使用,因为控件有可能已经被回收了:
@Override
protected void onProgressUpdate(Integer... values) {
TextView tvProgress = textViewWeakReference.get();
ProgressBar progressBar = progressBarWeakReference.get();
if (tvProgress!=null)
tvProgress.setText(values[0]+"%");
if (progressBar!=null)
progressBar.setProgress(values[0]);
}
3. 多次调用 AsyncTask 出现问题
同一个 AsyncTask 任务是不能被多次启动,若多次会直接抛出异常,这部分可以直接从源码看出来:
@MainThread
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
return executeOnExecutor(sDefaultExecutor, params);
}
@MainThread
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
Params... params) {
if (mStatus != Status.PENDING) {
switch (mStatus) {
case RUNNING:
throw new IllegalStateException("Cannot execute task:"
+ " the task is already running.");
case FINISHED:
throw new IllegalStateException("Cannot execute task:"
+ " the task has already been executed "
+ "(a task can be executed only once)");
}
}
//...省略
}
在任务的开始直接判断 mStatus 的状态,mStatus 是 AsyncTask 的状态机控制变量:
public enum Status {
PENDING,
RUNNING,
FINISHED,
}
在初始化的时候, private volatile Status mStatus = Status.PENDING
mStatus 为待开始状态,在启动 executeOnExecutor 的时候变成了 RUNNING 状态:mStatus = Status.RUNNING
,在最后执行完成 mStatus = Status.FINISHED
。
4. AsyncTask 并行还是串行
这个问题很微妙,因为 Google 对这个改动了很多次,在源码的注释中:
* <h2>Order of execution</h2>
* <p>When first introduced, AsyncTasks were executed serially on a single background
* thread. Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed
* to a pool of threads allowing multiple tasks to operate in parallel. Starting with
* {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are executed on a single
* thread to avoid common application errors caused by parallel execution.</p>
* <p>If you truly want parallel execution, you can invoke
* {@link #executeOnExecutor(java.util.concurrent.Executor, Object[])} with
* {@link #THREAD_POOL_EXECUTOR}.</p>
在一开始,AsyncTask 任务的执行是串行执行,使用一条工作线程,从 Android-DONUT,AndroidVersion4 开始,使用线程池来创建线程,和允许并行执行任务,而从 AndroidHoneyComb,AndroidVersion11开始,支持串行和并行,默认提交是串行执行任务,避免了多线程并发导致脏资源的情况出现。当然,如果你想使用并行执行,可以调用 executeOnExecutor(THREAD_POOL_EXECUTOR) 提交。
MyAsyncTask myAsyncTask = new MyAsyncTask();
myAsyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,"strings");
简单来说,AsyncTask 串行还是并行跟版本有关,AndroidVersion<4,串行,11>AndroidVersion>=4,并行,并行后,出现很多多线程并发访问资源的问题,AndroidVersion>=11后,可以是并行,也可以是串行。
5. 其他的注意事项
AsyncTask 需要遵循一些线程规则才能正常工作。
- AndroidVersion<16的时候,AsyncTask 必须在UI线程中加载,因为需要获取到UI线程的Handler,不过在16后就不需要了,因为16后面默认加载主线程的Handler。
- 实例必须在UI线程上被创建
- execute 必须在UI线程上被调用
- 请不要人为调用:onPreExecute,doInBackgroud,onProgressUpdate,onPostExecute
- 任务只能被执行一次,执行多次会抛出异常
6. 标准写法
这里给出一个常用的写法:
private static class MyAsyncTask extends AsyncTask<String,Integer,Boolean> {
/* 为避免内存泄漏,引用应该用弱引用包裹 */
private String TAG = "MyAsyncTask";
private WeakReference<Context> contextWeakReference;
private WeakReference<ProgressBar> progressBarWeakReference;
private WeakReference<TextView> textViewWeakReference;
public MyAsyncTask(Context context, ProgressBar progressBar, TextView tvProgress) {
contextWeakReference = new WeakReference<>(context);
progressBarWeakReference = new WeakReference<>(progressBar);
textViewWeakReference = new WeakReference<>(tvProgress);
}
/**
* 运行在UI线程中,用于参数初始化
* 在 doInBackground 前调用
*/
@Override
protected void onPreExecute() {
Log.i(TAG,"onPreExecute...");
Context context = contextWeakReference.get();
if (context!=null)
Toast.makeText(context,"开始执行",Toast.LENGTH_SHORT).show();
}
/**
* 后台任务处理方法,运行在
* 子线程,可以做一些耗时任务
* @param strings 输入参数,对应于泛型:Params
* @return 结果
*/
@Override
protected Boolean doInBackground(String... strings) {
Log.i(TAG,"doInBackground...");
int i=0;
while(i<100){
if (isCancelled())
break;
i++;
//任务执行过程进度/参数的回调,对应于泛型:Progress
publishProgress(i);
try {
Thread.sleep(200);
} catch (InterruptedException ignored) {
}
}
//返回值代表最后的结果,对应于泛型:Result
return true;
}
/**
* 任务执行过程中的更新,
* 由子方法:publishProgress()
* 触发,运行在UI线程中
* @param values 参数
*/
@Override
protected void onProgressUpdate(Integer... values) {
TextView tvProgress = textViewWeakReference.get();
ProgressBar progressBar = progressBarWeakReference.get();
if (tvProgress!=null)
tvProgress.setText(values[0]+"%");
if (progressBar!=null)
progressBar.setProgress(values[0]);
}
/**
* 执行结束的回调,运行在UI线程
* @param bool 参数
*/
@Override
protected void onPostExecute(Boolean bool) {
Log.i(TAG,"onPostExecute, " + bool);
Context context = contextWeakReference.get();
if (context!=null)
Toast.makeText(context,"执行完毕",Toast.LENGTH_SHORT).show();
}
/**
* 执行 task.cancel() 方法后,
* 会回调此方法
*/
@Override
protected void onCancelled() {
Log.i(TAG,"onCancelled");
}
}
技术酱专注 Android 技术,工作日不定时推送新鲜文章,如果你有好的文章想和大家分享(有稿费哦),欢迎关注投稿!
技术酱
网友评论