今天看啥  ›  专栏  ›  CokeNello

AsyncTask - 进阶篇

CokeNello  · 掘金  ·  · 2020-04-03 02:30
阅读 110

AsyncTask - 进阶篇

概述

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 的使用:

  1. 及时地 cancel 任务,不说说 cancel 没有效果吗?是没有效果,但我们通过 isCancelled 来判断,在 doInBackground 中取消后续的任务,参考上面的第一点,然后在 Activity 销毁 destroy 中,主动调用 AsyncTask.cancel()。

  2. 对传入的控件,采用弱引用包裹起来,杜绝强引用。

/* 为避免内存泄漏,引用应该用弱引用包裹 */
    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");
    }
}
复制代码

相关文章推荐:

AsyncTask - 基础篇

AsyncTask - 进阶篇

AsyncTask - 源码篇


技术酱专注 Android 技术,工作日不定时推送新鲜文章,如果你有好的文章想和大家分享(有稿费哦),欢迎关注投稿!

技术酱




原文地址:访问原文地址
快照地址: 访问文章快照