美文网首页Android开发Android开发经验谈程序员
(二)现状篇:深度解读AsyncTask

(二)现状篇:深度解读AsyncTask

作者: 呼啸长风 | 来源:发表于2019-04-09 08:50 被阅读20次

    一、前言

    为了提高流畅性,耗时任务放后台线程运行,这是APP开发的常识了。
    远古时期,还没有各种库的时候,用来处理异步任务的方法有:
    Thread/ThreadPoolExecutor、Service/IntentService、AsyncTask……

    其中,AsyncTask历经多次迭代,可以说是骨灰级的API了。
    AsyncTask适用于“数据加载+界面刷新”的模式,而对于Android开发而言,这类模式是比较常见的,所以一度还是很多人使用AsyncTask的。
    然而随着RxJava的普及,AsyncTask日渐式微,如今或许还有存在于一些旧代码中,或许还有部分开发者还在使用。
    AsyncTask最终是否会被掩埋于历史的尘埃之中,不得而知;
    事实上,虽是“古董”,搬出来把玩一番,拭去尘埃,你会发现,破旧的表面之下,也有熠熠生辉之处。

    二、用法

    先来看一段展示基本用法的代码:

    public class TestActivity extends AppCompatActivity {
        private TextView mProgressTv;
        private ProgressBar mProgressBar;
        private Button mStartBtn;
        private TestTask mTestTask;
        private long mCount;
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            // ... 
            mStartBtn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    mTestTask = new TestTask();
                    mTestTask.execute(++mCount);
                }
            });
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            if(mTestTask != null){
                mTestTask.cancel(true);
            }
        }
    
        private class TestTask extends AsyncTask<Long, Integer, String>{
            @Override
            protected void onPreExecute() {
                mProgressTv.setVisibility(View.VISIBLE);
                mProgressBar.setVisibility(View.VISIBLE);
            }
    
            @Override
            protected String doInBackground(Long... params) {
                for (int i = 0; i <= 100; i += 2) {
                    // do something, like request data
                    publishProgress(i);
                }
                return params[0] + "st done";
            }
    
            @Override
            protected void onProgressUpdate(Integer... values) {
                mProgressBar.setProgress(values[0]);
                mProgressTv.setText(values[0]+"%");
            }
    
            @Override
            protected void onPostExecute(String s) {
                mProgressTv.setText(s);
            }
    
            @Override
            protected void onCancelled() {
                ToastUtil.shortTips("TestTask cancel");
            }
        }
    }
    

    很简单的一段代码,所实现的是:做任务,刷新进度,显示结果。
    关于用法,掌握几个参数泛型和函数即可。

    • Params 入参类型,入参由execute传入,是可变长参数
    • Progress 进度参数类型
    • Result 结果类型,结果由doInBackground返回
    • execute 发起任务
    • cancel 取消任务
    • publishProgress 发布进度
    • onPreExecute 执行任务前回调(UI线程)
    • doInBackground 执行任务,可发布进度 (后台线程)
    • onProgreessUpdate 显示进度(UI线程)
    • onPostExecute 任务结束后回调(UI线程)
    • onCancelled 任务取消后回调(UI线程)

    从API文档我们可以知道:
    前三个方法需要主动调用,其他是回调方法。
    常规流程:execute -> onPreExecute -> doInBackground -> onPostExecute;
    doInBackground的过程中可以调用 publishProgress 发布进度,然后onProgreessUpdate在UI线程中被回调;
    如果调用了cancel,执行结束时回调onCancelled,而不回调onPostExecute。

    三、原理

    3.1 执行流程

    AsyncTask的实现很简洁,去掉注释,只有两三百行代码;要分析流程,100行左右的核心代码就够了。
    下面是精简后的代码:

    public abstract class AsyncTask<Params, Progress, Result> {
        private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
        private static InternalHandler sHandler;
        private final WorkerRunnable<Params, Result> mWorker;
        private final FutureTask<Result> mFuture;
        private final AtomicBoolean mTaskInvoked = new AtomicBoolean();
    
        public AsyncTask() {
            sHandler = new InternalHandler(Looper.getMainLooper());
    
            mWorker = new WorkerRunnable<Params, Result>() {
                public Result call() throws Exception {
                    mTaskInvoked.set(true);
                    Result result = null;
                    try {
                        result = doInBackground(mParams);
                    } finally {
                        postResult(result);
                    }
                    return result;
                }
            };
    
            mFuture = new FutureTask<Result>(mWorker) {
                @Override
                protected void done() {
                    postResultIfNotInvoked(get());
                }
            };
        }
    
        private void postResultIfNotInvoked(Result result) {
            final boolean wasTaskInvoked = mTaskInvoked.get();
            if (!wasTaskInvoked) {
                postResult(result);
            }
        }
    
        private void postResult(Result result) {
            Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT,
                    new AsyncTaskResult<Result>(this, result));
            message.sendToTarget();
        }
    
        protected final void publishProgress(Progress... values) {
            if (!isCancelled()) {
                getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
                        new AsyncTaskResult<Progress>(this, values)).sendToTarget();
            }
        }
    
        private static class InternalHandler extends Handler {
            public InternalHandler(Looper looper) {
                super(looper);
            }
            @Override
            public void handleMessage(Message msg) {
                AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
                if(msg.what == MESSAGE_POST_RESULT){
                    result.mTask.finish(result.mData[0]);
                }else if(msg.what == MESSAGE_POST_PROGRESS){
                    result.mTask.onProgressUpdate(result.mData);
                }
            }
        }
    
        private void finish(Result result) {
            if (isCancelled()) {
                onCancelled(result);
            } else {
                onPostExecute(result);
            }
        }
    
        public final AsyncTask execute(Params... params) {
            return executeOnExecutor(sDefaultExecutor, params);
        }
    
        public final AsyncTask executeOnExecutor(Executor exec, Params... params) {
            onPreExecute();
            mWorker.mParams = params;
            exec.execute(mFuture);
        }
    }
    

    主要执行流程,简单地说,就是:
    Task.execute -> Executor -> FutureTask -> WorkerRunnable -> Task.postResult -> Handler -> Task.finish
    除去Task, 就只剩 “Executor + FutureTask + WorkerRunnable + Handler” 了。
    所以,如果要分析实现,抓住流程,然后对这几个部分各个击破即可。

    • Executor我们留到下一节,这里只需知道Executor用于执行Runnable即可。
    • FutureTask实现了Runnable和Future,WorkerRunnable实现了Callable。
      FutureTask传给Executor后,会分配线程执行FutureTask的run()方法;
      然后通常情况下经历 FutureTask.run() -> Callable.call() -> FutureTask.done() 的过程,
      如果在 FutureTask.run()之前执行了cancel方法,则call()不会被回调,但done()还是会被回调的。
    • 上面代码中可以看到,call()方法中执行了doInBackground和postResult;
      done()方法中执行了postResultIfNotInvoked,也就是,如果call()没有被调用,则执行postResult。
      所以,无论doInBackground有没有被执行,最终总会执行finish方法,从而执行onPostExecuteonCancelled其中之一。
    • Handler大家都很熟悉了,当传入MainLooper时,handleMessage在主线程被回调。

    下面是AsyncTask的流程图:

    图片出自《AsyncTask知识扫盲》,笔者做了部分补充。

    3.2 任务调度

    上一节我们分析执行流程,了解到AsyncTask主要是通过与“FutureTask+WorkerRunnable+Handler”密切配合,
    实现了在任务执行的不同阶段,回调相应的方法
    如果说这些是“框架”的话,那么Executor就是执行任务的“引擎”。
    任务的调度(包括串行还是并行、并发窗口多大、如何排队……等),由Executor实现。
    要理解AsyncTask的Executor,需要对线程池有一定了解,推荐阅读本系列的上一篇文章:《基础篇:速读Java线程池》

    接下我们继续分析AsyncTask关于Executor的代码。
    先看线程池的参数配置:

        private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
        // We want at least 2 threads and at most 4 threads in the core pool,
        // preferring to have 1 less than the CPU count to avoid saturating
        // the CPU with background work
        private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
        private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
        
        public static final Executor THREAD_POOL_EXECUTOR;
        static {
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                    CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, 30, TimeUnit.SECONDS,
                    new LinkedBlockingQueue<Runnable>(128), sThreadFactory);
            threadPoolExecutor.allowCoreThreadTimeOut(true);
            THREAD_POOL_EXECUTOR = threadPoolExecutor;
        }
    

    为方便分析,我们对命名做一些简化(比如用coreSize代替CORE_POOL_SIZE)。

    根据线程池的特点,当一个任务提交:

    • 如果线程池的线程数小于coreSize,创建新的线程来执行任务(即使有线程是闲着的)。
    • 当线程数大于等于coreSize,若有空闲的线程,则分配任务给空闲的线程,否则尝试放到任务队列。
    • 如果没有空闲线程,而且队列也满了(128个),
      则尝试创建更多线程来消化任务, 但总线程数不能超过maxSize。
    • 如果总线程数等于maxSize,队列也满了,则拒绝提交的任务,
      在这里是执行默认的拒绝策略,也就是抛出RejectedExecutionException。

    如果没有任务提交,线程会在存活30s后销毁;
    由于设置了allowCoreThreadTimeOut(true),即使是“核心线程”也是会销毁的。

    一般情况下任务不会堆积一百多个,这时候coreSize就是线程池的“并发窗口”(同时运行的线程数)。
    coreSize大小的设置其实是经过几个版本的:

    • 先是固定等于5,
    • 后来是CPU_COUNT + 1,
    • 如今是max(2, min(CPU_COUNT - 1, 4)

    注释提到,为了避免后台工作使得CPU饱和,倾向于coreSize “have 1 less than the CPU count”;
    同时还加了道紧箍咒:at least 2 threads and at most 4 threads。
    看得出来,SDK 的开发人员对coreSize设置多少是比较纠结的。

    在线程池之外,AsyncTask还封装了一个SerialExecutor,用于任务的串行调度。
    代码如下:

    public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
    
    private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
    
    private static class SerialExecutor implements Executor {
            final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
            Runnable mActive;
    
            public synchronized void execute(final Runnable r) {
                mTasks.offer(new Runnable() {
                    public void run() {
                        try {
                            r.run();
                        } finally {
                            scheduleNext();
                        }
                    }
                });
                if (mActive == null) {
                    scheduleNext();
                }
            }
    
            protected synchronized void scheduleNext() {
                if ((mActive = mTasks.poll()) != null) {
                    THREAD_POOL_EXECUTOR.execute(mActive);
                }
            }
        }
    

    这是典型的装饰者模式,这种设计模式有以下特点:

    1. 装饰对象和真实对象有相同的接口,这样客户端对象就能以和真实对象相同的方式和装饰对象交互。
    2. 装饰对象包含一个真实对象的引用。
    3. 装饰对象接受所有来自客户端的请求,它把这些请求转发给真实的对象。
    4. 装饰对象可以在转发这些请求以前或以后增加一些附加功能。

    SerialExecutor代码不多,却用了两次装饰者模式:Runnable和Executor。

    • Runnable部分,往任务队列添加的匿名Runnable对象(装饰对象),当被Executor调用run()方法时,
      先执行“真实对象”的run()方法,然后再调用scheduleNext();
    • Executor部分,SerialExecutor实现串行调度的功能,而具体的任务执行转发给THREAD_POOL_EXECUTOR。
      从实现方面看,复用了ThreadPoolExecutor的功能;
      从运行方面看,复用了THREAD_POOL_EXECUTOR中的线程。

    就效果而言,AsyncTask通过最小的代价(添加SerialExecutor), 同时提供了串行和并行两种调度方式。
    (此处用“并发”会更加严谨,但是说“并行”表达更加流畅一些,就先这么说了)

    串行还是并行这个问题,官方文档有交代:

    When first introduced, AsyncTasks were executed serially on a single background thread.
    Starting with Build.VERSION_CODES.DONUT , this was changed to a pool of threads allowing multiple tasks to operate in parallel.
    Starting with Build.VERSION_CODES.HONEYCOMB , tasks are executed on a single thread to avoid common application errors caused by parallel execution.
    If you truly want parallel execution, you can invoke executeOnExecutor(java.util.concurrent.Executor, java.lang.Object[]) with THREAD_POOL_EXECUTOR .

    简单翻译:
    最初的时候,AsyncTask是串行的;
    自1.6之后,改成并行了;
    自3.0之后,“为避免并行导致普遍的应用程序错误”,又改成串行了;
    如果确实想并行,可以调用executeOnExecutor(THREAD_POOL_EXECUTOR)。

    改来改去确实不厚道,但是自从3.0之后就没有改过了,现在minSdkVersiond基本都在4.0以上了,所以算是曾经的坑吧。
    最后,默认串行好还是默认并行好?SDK的人员看来也很纠结,但最终选择了串行。

    这就好比建了一个游泳池,深水区和浅水区隔开,由于怕人溺水,默认开放浅水区。
    如果确实想到深水区也可以,就在隔壁。

    但浅水区也不是绝对安全的,比如有位开发者就遇到过这样的“坑”:
    他同时用了两个SDK,一个用来做图片剪裁,一个是facebook的广告SDK。
    后面发现图片加载不出来,经核查发现两个SDK都用了AsyncTask, 但都是用的串行的Executor。
    国内访问外网速度偏慢,所以facebook的SDK阻塞了图片剪裁的任务。
    后来作者给这个图片剪裁库的开发者提了建议,让其改用THREAD_POOL_EXECUTOR来图片剪裁,方才解了任务阻塞的问题。

    串行和并行其实都是有需求的,需具体问题具体分析。

    四、局限性

    随着使用的深入,大家发现AsyncTask存在一些问题。
    关于这些“问题”,仁者见仁,智者见智。
    下面是笔者的分析:

    4.1 取消任务

    有的文章指出,AsyncTask的cancel()不一定起作用。
    AsyncTask的cancel确实是不一定能立即取消任务,但笔者认为这其实是合理的。

    • AsyncTask的cancel()其实是调用FuturetTask.cancel(mayInterruptIfRunning)。
      若mayInterruptIfRunning=true,会触发interrupt()。
      interrupt() 虽然不能保证马上终止任务,但是能够中断sleep(), wait()等方法;
      比如使用OkHttp时, interrupt()能够中断网络请求,因为 OkHttp在等待网络数据时用了wait方法。
    • 关于“取消任务”,FuturetTask.cancel()确实已经是最好的方案了:
      如果任务还没开始,则Executor调度到这个任务时,不会执行Callable.call();
      如果线程处于阻塞态,则会抛出InterruptedException而退出执行。
    • 为什么不用Thread.stop()呢? Thread.stop()是个危险的方法。
      比方说一个线程正在写入数据,如果突然中止,可能会导致数据不正确,甚至文件格式被破坏。
      犹如空中的飞机,贸然熄火很可能会导致空难。

    4.2 内存泄漏 & 生命周期

    AsyncTask若持有Activity引用,且生命周期比Activity的长,则Activity无法被及时回收。
    看到这段描述,或许很多读者都能想到Handler,Handler也有此问题;其实RxJava也有这个问题。
    所以这个问题不是AsyncTask独有。

    解决此问题通常有两种方案:
    1、声明为静态内部类,用弱引用持有Activity;
    2、解决生命周期问题,随Activity销毁而销毁。

    第一种方案操作成本太高,极其不方便。
    如果解决生命周期问题,不单内存泄漏问题可解,很多其他的问题也会迎刃而解,可谓一石多鸟。
    常见的做法就是在Activity回调函数onDestroy()中调用cancle()方法,
    但是这样的话需要在Activity声明一个AsyncTask的变量,指向AsyncTask的实例,写起来也是很麻烦。

    4.3 通用性

    看一段API文档的描述:

    AsyncTask is designed to be a helper class around Thread and Handler and does not constitute a generic threading framework.
    AsyncTasks should ideally be used for short operations (a few seconds at the most.)
    If you need to keep threads running for long periods of time, it is highly recommended you use the various APIs provided by the java.util.concurrent package such as Executor , ThreadPoolExecutor and FutureTask

    Google译文:
    AsyncTask旨在成为Thread和Handler的辅助类,并不构成通用的线程框架。
    理想情况下,AsyncTask应该用于短操作(最多几秒钟)。
    如果需要保持线程长时间运行,强烈建议您使用concurrent包提供的各种API,例如Executor,ThreadPoolExecutor和FutureTask。

    简而言之就是:AsyncTask不适合执行长时间运行的任务

    AsyncTask自己的实现中明明用了“Executor,ThreadPoolExecutor和FutureTask”,
    却又建议别人用这些API去执行“长时间的运行”的任务,咋一看着实让人困惑。

    如果读者阅读了本系列的上一篇文章,应该能理解其中原因。

    首先,如果用串行的执行器,则会遇到前面提到的“任务阻塞”的问题;
    然后即使使用THREAD_POOL_EXECUTOR,coreSize最大也不超过4,几个任务下来就满了,如果任务又比较耗时,后面的任务就要等很久了。
    所以对于“长时间运行的任务”,THREAD_POOL_EXECUTOR 和 SERIAL_EXECUTOR 不过是五十步和百步的区别。
    那为什么coreSize不设置大一点呢?
    前面3.2节中有提到,“为了避免后台工作使得CPU饱和”,coreSize设置得比较小。
    一方面要给UI线程保留计算资源,另一方面如果是执行的是计算密集型任务,线程数大于CPU核心,CPU利用率(用于执行任务的时间比例)反而更低,因为线程上下文切换也是有不少消耗的。

    可实际上,开发中需要用到异步的,很多情况下是IO密集型操作,尤其是网络请求。
    如果请求数据多,或者网络不稳定,则任务可能会“长时间运行”。
    如今的APP,大量的网络请求是很常见的。
    若不适用于网络请求,要之何用?

    五、总结

    通过前面的分析,我们了解到AsyncTask主要实现了两个方面的功能:
    流程控制:任务执行的不同阶段回调相应的方法;
    任务调度:串行/并行,并发控制,任务缓冲……等等。
    同时,与Handler的配合,使得AsyncTask尤其适用于开发中常见的“数据加载+界面刷新”等场景,可以说是给Android量身定制的异步任务框架。

    总的来说,AsyncTask构思精巧,代码简洁,用法也很简单。
    但是同时AsyncTask也存在一些局限性,比如生命周期导致的内存泄漏问题,以及由于并发窗口太小而导致的通用性问题。

    “夫鸡肋,弃之如可惜,食之无所得。”
    AsyncTask足够的简洁,功能也不错,但通用性的缺陷极大地限制了AsyncTask的适用范围,使其显得很“鸡肋”。

    那是否有办法可以方便地解决AsyncTask的生命周期问题?
    是否有“双全法”,既支持CPU的高利用率,又支持任务的高吞吐率呢?

    方法总比问题的多,AsyncTask其实已经迈出了很大的一步;
    我们只需在其基础上再多走几步,便能突破局限,“遇见更好的AsyncTask”。

    传送门:AsyncTask加强版

    参考资料:
    https://juejin.im/post/5a85a6066fb9a06337573955
    https://cloud.tencent.com/developer/article/1328339
    https://www.jianshu.com/p/94a483b4e26c
    https://www.zhihu.com/question/41048032
    https://www.zhihu.com/question/33515481

    相关文章

      网友评论

        本文标题:(二)现状篇:深度解读AsyncTask

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