AsyncTask - 进阶篇

作者: CokeNello | 来源:发表于2020-03-30 23:56 被阅读0次

    概述

    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 技术,工作日不定时推送新鲜文章,如果你有好的文章想和大家分享(有稿费哦),欢迎关注投稿!

    技术酱

    相关文章

      网友评论

        本文标题:AsyncTask - 进阶篇

        本文链接:https://www.haomeiwen.com/subject/yfzhuhtx.html