说说AsyncTask演化历程里的小纠结
Posted 悠然红茶
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了说说AsyncTask演化历程里的小纠结相关的知识,希望对你有一定的参考价值。
笔者以前写过一篇文章《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移到了拒绝策略里。大家是不是也觉得挺有趣?
以上是关于说说AsyncTask演化历程里的小纠结的主要内容,如果未能解决你的问题,请参考以下文章