说说AsyncTask演化历程里的小纠结

原创
2020/11/28 00:22
阅读数 783

    笔者以前写过一篇文章《AsyncTask研究》,阐述了android框架中AsyncTask的实现原理。当时是基于Android 7.0的代码来分析的,后来就没有再跟进AsyncTask的变化了。最近基于Android 10的代码,又看了一下AsyncTask,发现其实现变动了一点点,那么我们不妨重新串一下这几年不同Android版本里AsyncTask的变动,看看会有什么有趣的东西。

    当然,AsyncTask的基本原理是没什么本质变化的,大家如有兴趣,可自行参考《AsyncTask研究》一文,本文不打算全部重述一遍。不过有一张示意图倒是可以拿来供大家复习一下:

    另一个要复习的概念是线程池执行器ThreadPoolExecutor。这个类的构造函数如下:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

    corePoolSize指明了线程池的基本大小,如果线程池里当前存在的线程数小于corePoolSize,那么添加任务时,线程池就会创建新线程来干活。而如果线程数达到了corePoolSize,那么任务会先缓存进workQueue,直到塞满workQueue。塞满队列后,如果还有新的工作添加进来,线程池就会超额创建新线程来干活。当然,超额也是有限度的,最多能达到maximumPoolSize个线程。而如果在队列已满且线程数也到达最大阀值后,还继续添加新工作,那么线程池就会依照某个拒绝策略进行拒绝了。上面构造函数里最后一个参数,就体现了拒绝策略。

    为了更有效地运用线程,线程池还设定了两个关于时间的控制量,一个是keepAliveTime,另一个是allowCoreThreadTimeOut。简单地说,如果线程池里的线程的空闲时长达到keepAliveTime阀值时,线程池就会让超时的线程退出,直到线程数量降到corePoolSize大小为止,此时一般不会再轻易退出线程了,除非allowCoreThreadTimeOut的值为true,这个值明确告诉线程池,即便线程数小于corePoolSize了,也会一直把空闲线程退出去,直到线程数量为0。注意,线程的空闲时长是指做完当前任务后,等待新任务被分配给它的那段时长,不是任务执行过程中sleep的时长。

    线程池的那些关键参数一般都可在运行期动态设置,常见的设置函数有:

  • void setCorePoolSize(int corePoolSize)
  • void setKeepAliveTime(long time, TimeUnit unit)
  • void setMaximumPoolSize(int maximumPoolSize)
  • void setRejectedExecutionHandler(RejectedExecutionHandler handler)
  • void setThreadFactory(ThreadFactory threadFactory)

    现在我们可以绘制一张线程池的示意图:

1. on Android 2.3

    复习完以上这些知识,我们就可以着手看AsyncTask的变动历史了。我们先看Android2.3上的线程池:
【AsyncTask.java on Android2.3】

private static final int CORE_POOL_SIZE = 5;
private static final int MAXIMUM_POOL_SIZE = 128;
private static final int KEEP_ALIVE = 1;

private static final BlockingQueue<Runnable> sWorkQueue = new LinkedBlockingQueue<Runnable>(10);
. . . . . .
private static final ThreadPoolExecutor sExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, 
                                MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, 
                                sWorkQueue, sThreadFactory);

也就是说,尝试形成一个核心池大小为5的线程池。如果有5个任务尚在执行时,又来了第6个任务,则新任务会缓存进sWorkQueue队列,不过这个队列其实也不怎么大,最多能缓存10个任务。在队列塞满之后,如果还有新任务到来,则开始创建新线程来做事,而且最多再创建123(即128-5)个线程。于是,在极端情况下线程池会是下图这个样,怎么看都觉得线程数挺壮观了:

2. on Android 4.4

    到了Android 4.4,Google的工程师似乎发现,用户在使用AsyncTask时,大多数情况下是希望那些被添加的任务能够一个个串行执行的,只有较少的情况是希望多线程并行执行的。所以,新AsyncTask里的默认执行器在声明时就写为“串行执行器”了。

private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;

但是,早期Android版本里AsyncTask已经写成多线程并行执行的鬼样子了,这总得兼顾一下嘛。于是,AsyncTask内部搞了两个静态的执行器,分别表示成AsyncTask.THREAD_POOL_EXECUTOR 和刚刚看到的 AsyncTask.SERIAL_EXECUTOR,前者是可并行执行的执行器,后者是串行执行的执行器。这个在《AsyncTask研究》一文中已有阐述。

    串行执行时,示意图如下:

串行时其实最终也会用到线程池,只是这个线程池已经退化到只有一个线程在干活了。

    并行执行时,AsyncTask也稍微变化了一点儿。估计是为了改变以前那种线程满天飞的壮观场面,同时又考虑到多核CPU已经比较普遍,于是开始让AsyncTask在不过度产生线程的情况下,充分利用一下多核,所以AsyncTask里线程池的相关代码变成了这样:
【AsyncTask.java on Android4.4】

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;
private static final int KEEP_ALIVE = 1;
. . . . . .
. . . . . .
private static final BlockingQueue<Runnable> sPoolWorkQueue =
        new LinkedBlockingQueue<Runnable>(128);

public static final Executor THREAD_POOL_EXECUTOR
        = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
                TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);

可以看到,会先调用availableProcessors()获取虚拟机当前可用的处理器数量。再基于这个数量计算Core Pool和线程池最大的线程数。比如CPU数为2时,满负荷时的线程池示意图如下:

看到了吧,最多才会有5个线程在同时干活。也就是说,线程有点儿贵,要谨慎地给。

    但是,设计师很明显不希望普通用户能设定AsyncTask的默认行为,所以setDefaultExecutor()成员函数被注解为@hide,就是不让别人用嘛。

/** @hide */
public static void setDefaultExecutor(Executor exec) {
    sDefaultExecutor = exec;
}

这个函数主要在ActivityThread里调用了一下,做了一点兼容性处理:

if (data.appInfo.targetSdkVersion <= android.os.Build.VERSION_CODES.HONEYCOMB_MR1) {
    AsyncTask.setDefaultExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}

可以看到,如果所运行的App是那种针对旧系统(Android 3.1(HONEYCOMB MR1)之前的系统)的应用,则把运行该应用的进程里的AsyncTask的默认行为设为“按多线程并行执行”,而如果运行的是针对新系统的应用,则AsyncTask的默认行为统统按串行执行。

    当然,上面只是修改了默认行为,如果某个新版应用明确要求其AsyncTask按多线程并行处理,它可以直接调用executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)。

3. on Android 7.0

    待到发展到Android 7.0,在并行执行时,又变化了一点儿。仍然会先调用availableProcessors()获取虚拟机当前可用的处理器数量,然而CORE_POOL_SIZE不再只是简单地按CPU_COUNT + 1来计算啦,这可能是因为发现在极端情况下,把所有的CPU都占上有点儿太狠了,这肯定会影响到其他后台线程的调度。新的CORE_POOL_SIZE被控制在2到4之间,说明在占用资源方面下手的确轻了少许。相关的代码截选如下:
【AsyncTask.java on Android7.0】

private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
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;
. . . . . .
. . . . . .
private static final BlockingQueue<Runnable> sPoolWorkQueue =
        new LinkedBlockingQueue<Runnable>(128);


public static final Executor THREAD_POOL_EXECUTOR;
static {
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
            sPoolWorkQueue, sThreadFactory);
    threadPoolExecutor.allowCoreThreadTimeOut(true);
    THREAD_POOL_EXECUTOR = threadPoolExecutor;
}

    串行执行的情况没什么变化,我们就不赘述了。我们还以CPU数为2为例,并行执行的示意图如下:

    另外,在Android 7上,AsyncTask线程池对线程的空闲时长也更加容忍了。以前空闲1秒钟,就会终止线程,现在最多允许空闲30秒。这是为了保证不会出现频繁快速地终止、创建线程。而且,线程池执行器还调用了allowCoreThreadTimeOut(true),也就是说,如果空闲时间过长,连CorePool里的线程也可以终止。

4. on Android 8.0

    Android 8.0上的AsyncTask和Android 7.0的差不多,在线程池的调度方面没什么变化。只是在创建AsyncTask时,做了点小手脚。从代码上看,AsyncTask多了两个隐藏(@hide)的构造函数:

  • public AsyncTask(@Nullable Handler handler)
  • public AsyncTask(@Nullable Looper callbackLooper)

    也就是说,在系统内部可以指定一个looper,处理AsyncTask的MESSAGE_POST_RESULT和MESSAGE_POST_PROGRESS。

public AsyncTask(@Nullable Looper callbackLooper) {
    mHandler = callbackLooper == null || callbackLooper == Looper.getMainLooper()
        ? getMainHandler()
        : new Handler(callbackLooper);

如果looper是UI线程里的looper,这两个事件由getMainHandler()返回的InternalHandler处理,而如果是其他线程的looper,那么就是用一个最普通的Handler来处理,相当于什么事都不做。这难道不让人困惑吗?

5. on Android 10.0

    Android 9上的AsyncTask和Android 8的完全一样,所以我们直接看Android 10上的AsyncTask。在Android 10上:
1)主线程池的队列从LinkedBlockQueue改成了SynchronousQueue;
2)主线程池的CorePool大小改成了1;
3)明确设定了新的拒绝策略sRunOnSerialPolicy;

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

    以前使用LinkedBlockQueue的情况,我们已经了解了,此处不再赘述。现在改成SynchronousQueue后,线程池的行为会有什么不同吗?我们可以这样理解,SynchronousQueue的内部是没有任务缓存队列的,所以当CorePool线程用完后,其实是立即起新线程来做事的,直到线程数达到MAXIMUM_POOL_SIZE。这就不像以前那样,还有个填充任务队列的过程。在线程数达到MAXIMUM_POOL_SIZE之后,如果还有新任务,则会按拒绝策略处理。    Android 10上没有采用ThreadPoolExecutor已有的拒绝策略,而是专门设计了一个自定义的拒绝策略:

private static ThreadPoolExecutor sBackupExecutor;
private static LinkedBlockingQueue<Runnable> sBackupExecutorQueue;

private static final RejectedExecutionHandler sRunOnSerialPolicy =
        new RejectedExecutionHandler() {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        android.util.Log.w(LOG_TAG, "Exceeded ThreadPoolExecutor pool size");

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

看到了吗,在拒绝策略中又用到了一个备用的线程池,线程池的Core Pool大小和最大线程数都是5(BACKUP_POOL_SIZE),也就是说,当拒绝策略处理任务时,最多还可再启动5个线程来干活,如果仍然不够用的话,新任务就会记录进一个几乎无限大的LinkedBlockingQueue。示意图如下:

6. 小结

    经过本文的阐述,大家是不是能感到维护AsyncTask的工程师的一点小纠结呢?一开始希望做事的线程多一点,后来发现太多了,要按CPU数限制一下。一开始在主线程池里用LinkedBlockingQueue来缓存任务,后来干脆把LinkedBlockingQueue移到了拒绝策略里。大家是不是也觉得挺有趣?

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部