(上篇)解读AsyncTask

一、前言

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

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

二、原理

我们可以从使用方法切入,看下几个常用的方法:

  • execute 发起任务;
  • cancel 取消任务
  • onPreExecute 执行任务前回调(UI线程)
  • doInBackground 执行任务,可发布进度 (后台线程)
  • onProgreessUpdate 显示进度(UI线程)
  • onPostExecute 任务结束后回调(UI线程)
  • onCancelled 任务取消后回调(UI线程)

从API文档我们可以知道:
前两个需要主动调用,后面是回调方法。
常规流程:execute -> onPreExecute -> doInBackground -> onPostExecute;
doInBackground的过程中可以发布进度,发布后会回调 onProgreessUpdate;
如果调用了cancel,执行结束时回调onCancelled。

那么,这些方法的背后是如何运作的呢? 接下来我们对关键代码稍作分析。
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” 了。
所以,如果要分析实现,抓住流程,然后对这几个部分各个击破即可。

  • AsyncTask的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知识扫盲》,笔者做了部分补充。

三、存在的问题

AsyncTask用法很简单,对Android开发中常见的“数据加载+界面刷新”的场景提供了友好的API。
然而随着使用的深入,渐渐地大家发现AsyncTask有不少问题:

  • 不好好工作的cancel():有时候起作用;
  • 串行or并行:经历过串行-并行-串行(默认)的迭代;
  • 内存泄漏:若持有Activity引用,且生命周期比Activity的长,则Activity无法被及时回收;
  • 生命周期:不会随着Activity的销毁而销毁;
    ……还有其他的一些,就不逐一列举了。

上面这些陈述是不争的事实,然而对这些事实的定性(是不是问题),仁者见仁,智者见智。

下面是笔者的分析:

3.1 cancel() or not

AsyncTask的cancel确实是不一定能立即取消任务,但这其实是合理的。
当调用AsyncTask的cancel(mayInterruptIfRunning), 并传入true时,会触发interrupt()。
interrupt() 虽然不能保证马上终止任务,但是能够中断sleep(), wait()等方法;
比如使用OkHttp时, interrupt()能够中断网络请求,因为 OkHttp在等待网络数据时用了wait方法。
为什么不用Thread.stop()呢? Thread.stop()是个危险的方法。
比方说一个线程正在写入数据,如果突然中止,可能会导致数据不争取,甚至文件格式被破坏。

3.2 串行or并行, that is a question

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

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)。
(翻译比较直白,比如parallel译成并行,虽然按情景似乎并发更合理一点,先按文档来吧)

API其实提供了串行和并行两种选择的,但默认方式只能选一种;
改来改去确实不厚道,但是自从3.0之后就没有改过了(现在minSdkVersiond基本都4.0以上),所以算是曾经坑过;
最后,默认串行好还是默认并行好?官方人员看来很纠结,但最终选择了串行。

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

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

串行和并行其实都是有需求的,需具体问题具体分析。
以现在主流App的复杂度,任务并发几乎是无法避免的。
总是要经历大风大浪的,注意(线程)安全就好。

3.3 内存泄漏 & 生命周期

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

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

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

3.4 框架定位

前面提到的问题,除了第3点外,其他都是比较模糊的。
在我看来,最大的问题,是AsyncTask的框架定位。
看一段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不是“通用的线程框架”,承认自己是“线程框架”,但又觉得自己不“通用”;
自己其使用了“Executor,ThreadPoolExecutor和FutureTask”这些API,又建议别人去使用这些API。
太过保守了!

里面还提到,AsyncTask适用于短操作,不适合长时间运行的任务。为什么呢?
可以结合代码(Android 9.0)看一下AsyncTask的线程池配置:

    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;
    }

其实CORE_POOL_SIZE大小的设置也是经过很几个版本的:
先是固定等于5,后来是CPU_COUNT + 1,如今是 max(2, min(CPU_COUNT - 1, 4)
LinkedBlockingQueue的capacity曾经一度是10,然后MAXIMUM_POOL_SIZE是128(这个版本,有人认为是SDK开发者写反了)。

要理解这些参数配置,需熟悉Java线程池,这里推荐阅读笔者的另一篇文章《速读Java线程池》

做完广告,我们来继续分析代码-_-
首先是CORE_POOL_SIZE,max(2, min(CPU_COUNT - 1, 4), 非常经典的用法, 最终2<=coreSize<=4。
coreSize太小,几个任务下来就满了,如果任务又比较耗时,比如说网络请求,后面的任务就要等很久了。
这样的话即使调用executeOnExecutor(THREAD_POOL_EXECUTOR),还是会碰到前面提到的那个“坑”(相互阻塞)。
这就是API文档说“AsyncTask应该用于短操作”的原因。

那为什么coreSize不设置大一点呢?两个原因:
1、如果是执行的是计算密集型任务,线程数大于CPU核心反而会更慢,因为线程上下文切换还是有不少消耗的;
2、给UI线程保留计算资源,“to avoid saturating(饱和) the CPU with background work”。

而实际上,开发中需要用到异步的,很多情况下是IO密集型操作,尤其是网络请求。

“夫鸡肋,弃之如可惜,食之无所得。”
AsyncTask可以用来做很多事,但是很多类型的任务又不适合做,这么一来,就显得很鸡肋了。

四、Left or Right?

古往今来,面临选择总是让人痛苦的。

黄色的树林里分出两条路,
可惜我不能同时去涉足。
—— 弗罗斯特

世间安得双全法,不负如来不负卿。
—— 仓央嘉措

其实,开发过程中也经常会遇到需要选择的地方。

面对选择,内心就像天平一样,要么左,要么右,要么端平;
如果某一边有绝对的优势,这时候没有疑问会往一边倾倒;
而比较常见的情况是,随着需要衡量的参考因素不断增加而摇摆不定,当然最终还是会有结果。

从前面的分析我看到,AsyncTask在Executor默认串行还是并行方面犹豫不决,最终选择了串行,
不过还好,同时也提供了executeOnExecutor(THREAD_POOL_EXECUTOR)来支持并行。

coreSize也是经历过几个版本,但看得出来SDK的开发者是很纠结的:
模糊地设一个值似乎不太科学,设置大了怕线程池切换和CPU饱和,小了吞吐率又不高……

那是否有“双全法”,既有效利用CPU,又支持高吞吐率呢?

幸运的是,在这个问题上,有解决方案。
但为了控制篇幅,我们放在下一篇文章中讲解。
预知后事如何,且听下回分解。

传送门:(下篇)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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章