【源码】康一康过时的→AsyncTask

3,605 阅读9分钟

这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战

AsyncTask 对很多老Android来说,是一个很有年代感的东西了,想当年毕业找工作,AsyncTask可是面试必问。

随着 EventBus、RxJava、Kotlin协程 等的出现,它渐渐淡出了我们的视野,面试八股文也少了它的身影,很多新晋的Android开发估计都没听过它。不禁感叹:技术更新迭代真他么快,学不动了

面试不问,但一些老旧项目中还有用到它,接盘维护难免要学下,索性花一丢丢时间过一下:废弃原因 + 核心原理 + 演进历史


0x1、Deprecated 原因

AsyncTask,Android(API 3) 引入,一个轻量级的异步任务库,允许以 非线程堵塞 的方式执行操作。经过了好几个版本的调整,比如:

  • Android 1.6前,串行执行,原理:一个子线程进行任务的串行执行;
  • Android 1.6到2.3,并行执行,原理:一个线程数为5的线程池并行执行,但如果前五个任务执行时间过长,会堵塞后续任务执行,故不适合大量任务并发执行;
  • Android 3.0后,串行执行,原理:全局线程池进行串行处理任务;

还有一些小调整,比如Android 10后,线程池核心数变成1,线程队列从BlockingQueue变成SynchronousQueue等。

真够折腾的,打开 AsyncTask的官方文档 一行橙字映入眼帘:

AsyncTask类在 Android 11(API 30) 中被废弃了,推荐使用java.util.concurrent或Kotlin协程来替代。

em...就废弃了啊,往下看是废弃原因的解释:

大概意思:

  • AsyncTask本意是使得UI线程的使用变得简单正确,但最常见的用法是集成到UI中,反而导致了 Context泄露、忘记回调、configuration变化Crash 问题。
  • 不同版本AsyncTask的兼容问题;
  • 吞掉了来自doInBackground的异常;
  • 不能提供比直接使用Executor更多的功能;
  • Thread和Handler的辅助类,并非线程框架,主要用于执行一些时间不太长的异步任务;
  • 用法复杂,三个通用参数(Params、Progress、Result) + 四个回调方法;

简单概括下:版本兼容功能少用起来还麻烦


0x2、AsyncTask 用法详解

Talk is cheap,show you the Code,了解完过时原因,接着温习下它的用法:

    // 继承AsyncTask抽象类,建议声明为Activity的静态内部类,避免context泄露
    // 泛型参数依次为:
    // 
    // Params → 开始异步任务时传入的参数类型 → execute(Params)
    // Progress → 异步任务执行过程,返回进度值
    // Result → 异步任务执行完成后,返回结果类型 → doInBackground()
    // 
    class MyAsyncTask: AsyncTask<Task, Int, String>() {

        // 必须重写!在此执行耗时操作,返回执行结果
        override fun doInBackground(vararg params: Task?): String { return "执行耗时操作" }

        // 执行线程任务前的回调,即执行execute()前自动调用,常用作界面初始化操作
        override fun onPreExecute() { }

        // 在主线程中显示任务执行的进度,自动调用啊,按需重写
        override fun onProgressUpdate(vararg values: Int?) { }

        // 接受线程任务执行结果,线程任务结束时自动调用,可在此将结果更新到UI组件
        override fun onPostExecute(result: String?) { }

        // 异步任务被取消时自动调用,用于将异步任务设为取消状态,需在doInBackground()中停止
        // 此方法调用onPostExecute就不会被调用
        override fun onCancelled() {}
    }

    // 在主线程中初始化自定义AsyncTask实例后,调用execute(XXX)
    // 每个AsyncTask实例只能调用一次execute(),多次调用会抛出异常
    
    // 注①:Activity重建(如屏幕旋转),之前持有的Activity引用已失效,任务正常运行,
    // 但任务执行完成后,在onPostExecute中修改UI不会生效,建议在Activity恢复的相关方法
    // 中重启AsyncTask任务;
    
    // 注②:最好在Activity、Fragment的onDestory()中调用AsyncTask.cancle()将AsyncTask设置
    // 为canceled状态,然后在doInBackground中去判断这个AsyncTask的status是否为:
    // AsyncTask.Status.RUNNING,是,直接返回,结束任务
}

em...用法看着也不算太复杂 (不和rx、协程等对比的话),接着以Android 9.0源码为例,讲解一波核心原理~

0x3、核心原理讲解

① 单个任务的流转

execute()executeOnExecutor()

AsyncTask内部定义了一个状态字段 mStatus,可选值有:PENDING(挂起)RUNNING(运行中)FINISHED(结束)

从上述代码也可以看出为何**不能重复调用AsyncTask实例的execute()**方法,接着看下工作线程 → mWorker

看完定义,看实现:

往下看是**mFuture** 的实现:

任务包装类,添加了任务执行完后的回调,调用返回结果的处理方法,跟下:

最终调用的都是 postResult() 方法,利用 Handler 发送了一个标志位 MESSAGE_POST_RESULTMessage

往下走,跟下自定义Handler → InternalHandler 的具体实现细节:

重写 handleMessage() 对下述两种标记的Message进行处理:

  • MESSAGE_POST_RESULT → 任务结束;
  • MESSAGE_POST_PROGRESS → 任务进度更新;

任务进度更新那里,回调了 onProgressUpdate(),在跟下 finish()

判断任务取消标记是否为True,是回调 onCancelled(),否则回调 onPostExecute(),最后将AsyncTask状态字段设置为**FINISHED**。

② 多个任务的调度

跟下线程池 Executor,发现类中定义了 两个静态线程池 (实例共享),先看 THREAD_POOL_EXECUTOR

线城池配置:

核心池数(最少2,最多4)最大线程数(CPU数*2+1),非核心线程空闲存活时间(30s)、堵塞队列(LinkedBlockingQueue 堵塞链表队列,上限128,超出会抛异常)

再看另一个线程池 SERIAL_EXECUTOR

实现Executor接口,定义了一个 Runnable队列,在 初始化后一个任务执行结束后,都会从队列中获取 任务,并通过 THREAD_POOL_EXECUTOR 线程池执行。

线程池执行任务,而是把任务丢给另一个线程池完成,这是弄啥呢?

em...其实就是保证任务的 串行执行 (队列加同步锁),不想这样玩也也可以,开头这样写道:

调用下 executeOnExecutor(THREAD_POOL_EXECUTOR) 直接用 THREAD_POOL_EXECUTOR 线程池处理任务,就变成 并行执行 了。

以上就是Android 9.0中AsyncTask的实现原理,两个静态线程池,一个串行拿任务,丢到另一个线程池执行,非常简单。接着过下其他版本的AsyncTask的演进历史~


0x4、演进历史

Android 1.6

详细源码:core/java/android/os/AsyncTask.java

改动如下:

// ① 只用到一个线程池,并行执行任务,配置如下
// 核心线程池→5、线程数最大值→128,非核心线程空闲存活时长→10s
// 堵塞队列:LinkedBlockingQueue → 最大值10

// ② 消息类型有三种:
// MESSAGE_POST_RESULT、MESSAGE_POST_PROGRESS、MESSAGE_POST_CANCEL

// ③ 总结:5个核心线程 + 123个非核心线程 + 10个任务 → 最多138个任务,牛啊
// handler还需处理AsyncTask取消的消息,后续变成了一个标志位~

Android 3.2

详细源码:core/java/android/os/AsyncTask.java

改动如下:

// ① 用到了两个线程池,串行执行任务,配置如下:
// 核心线程池-5、线程数最大值-128,非核心线程空闲存活时长-1s
// 堵塞队列:LinkedBlockingQueue → 最大值10

// ② 串行线程池同9.0

// ③ 消息类型少了取消,通过判断mFuture.isCancelled()来判断取消状态

Android 4.4

详细源码:core/java/android/os/AsyncTask.java

改动如下:

// ① 同样是串行,配置不再粗暴写死,而是基于当前可用的处理器数量进行计算:
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;

// ② 堵塞队列上限变成128

// ③ 提供了串行变并行的setDefaultExecutor(),但被注解为@hide

Android 7.0

详细源码:core/java/android/os/AsyncTask.java

改动如下:

// 动了下线程池配置,看注释

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;
private static final int KEEP_ALIVE_SECONDS = 30;

// ① 核心线程池改成了2到4之间,不再是粗暴的CPU_COUNT+1,避免极端情况下占用
// 所有CPU影响到其他后台线程的调度~
// ② 线程空闲时间也调整为30s
// ③ 调用allowCoreThreadTimeOut(true),空闲时间过长连核心线程也会回收

Android 8.0

详细源码:core/java/android/os/AsyncTask.java

改动如下:

// 线程池没动,多了两个AsyncTask的构造方法,而且用了@hide注解
public AsyncTask(@Nullable Handler handler) {
    this(handler != null ? handler.getLooper() : null);
}
public AsyncTask(@Nullable Looper callbackLooper) {
    mHandler = callbackLooper == null || callbackLooper == Looper.getMainLooper()
            ? getMainHandler()
            : new Handler(callbackLooper);
}

// 这里猜测是为了扩展,支持外部传入一个Handler,做一些定制化处理,hide说明是想给系统自用的~

Android 10.0

上面的原理基于9.0讲解的,直接跳过讲10.0,详细源码:core/java/android/os/AsyncTask.java

改动如下:

// ① 线程池策略,发生了变化:
private static final int CORE_POOL_SIZE = 1;
private static final int MAXIMUM_POOL_SIZE = 20;
private static final int BACKUP_POOL_SIZE = 5;
private static final int KEEP_ALIVE_SECONDS = 3;

public static final Executor THREAD_POOL_EXECUTOR;

static {
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>(), sThreadFactory);
    threadPoolExecutor.setRejectedExecutionHandler(sRunOnSerialPolicy);
    THREAD_POOL_EXECUTOR = threadPoolExecutor;
}

// ② 核心线程数→1、最大线程数→20、空闲线程存活时间→3s、堵塞队列变成了SynchronousQueue
// 无缓冲等待队列(不存储元素的堵塞队列,添加元素后等待其他线程去走后才能继续添加)

// ③ 没有采用ThreadPoolExecutor已有的拒绝策略,自定义了一种拒绝策略:
private static final RejectedExecutionHandler sRunOnSerialPolicy =
        new RejectedExecutionHandler() {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        android.util.Log.w(LOG_TAG, "Exceeded ThreadPoolExecutor pool size");
        // As a last ditch fallback, run it on an executor with an unbounded queue.
        // Create this executor lazily, hopefully almost never.
        synchronized (this) {
            if (sBackupExecutor == null) {
                sBackupExecutorQueue = new LinkedBlockingQueue<Runnable>();
                sBackupExecutor = new ThreadPoolExecutor(
                        BACKUP_POOL_SIZE, BACKUP_POOL_SIZE, KEEP_ALIVE_SECONDS,
                        TimeUnit.SECONDS, sBackupExecutorQueue, sThreadFactory);
                sBackupExecutor.allowCoreThreadTimeOut(true);
            }
        }
        sBackupExecutor.execute(r);
    }
};

// 定义了一个核心池和最大核心池都为5的线程池,当拒绝策略处理任务,最多可以另外开5个线程
// 来执行任务,如果还不够的话,会添加到一个几乎无限大的LinkedBlockingQueue队列中

Android 11.0

详细源码:core/java/android/os/AsyncTask.java

改动如下:

// 为AsyncTask 添加 @Deprecated注解,一些描述注释

0x5、小结

有关AsyncTask最后的提交定格在2019.12.19,时代的车轮只会滚滚向前,而我们终将化为一粒尘埃。了解源码可以看到工程师对线程池策略的不断调整,很适合背完Java并发八股文后学习巩固~

参考文献