并发的学习与使用系列 第六篇

关于Asnynck有很多的源码解读,但一些解读现在看来已经不在适用了,比如AsyncTask类必须在UI Thread当中加载,AsyncTask的对象必须在UI Thread当中实例化等一些结论都是基于以前版本的代码来解读的,现在看来已经不是这样的了。同时分析了在AsyncTask的使用中存在的以及以及应该注意的问题,基于一些实际存在的问题尤其是并行串行问题,写了一个类似AsyncTask的库AsyncTaskScheduler,处理了上述的一些实际存在的问题。

AsyncTask源码解读


一些结论解读(基于android-23源码):

1. API 16 以前必须在主线程加载 AsyncTask,API 16 以后就不用了。

1
2
3
4
5
6
private static class InternalHandler extends Handler {
public InternalHandler() {
super(Looper.getMainLooper());
}
...
}

因为AsyncTask主要目的是在后台执行异步任务,然后和UI线程进行交互,所以需要得到UI线程的Handler,之前的AsyncTask加载时,是得到当前加载线程的Handler,而最新的源码中,总是得到UI线程的Looper来创建和UI交互的Handler。

2. 因为每个AsyncTask只能执行一次,多次调用同一个AsyncTask对象会出现异常。但如果要处理多个后台任务,你需要创建多个AsyncTask并执行execute()。
AsyncTask有三种状态,就绪PENDING,运行RUNNING,结束FINISHED,只有PENDING(就绪状态)的可以正确执行,执行后将状态置为Running,可知,每个AsyncTask只能执行一次,所有要想执行多个异步任务,只能新建多个AsyncTask对象。

3. API 4-11 默认是AsnckTask任务并发执行,API11后默认是顺序执行,任务是顺序执行,必须等一个任务结束才能执行下一个。但是可以通过executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)来进行并行执行任务,在并行执行任务时,有最大执行个数的限制

THREAD_POOL _EXECUTOR是默认的并行执行任务的线程池,BlockingQueue队列的长度是128。以自己的8核手机为例,其核心线程数是9个,最大线程是17,所能最大加入的任务数是128+17=145,如果超出这个任务数,就会报出RejectExecutionException异常(经过测试)。关于线程池的知识更多可看从使用到原理学习Java线程池
这也是为什么新的API将任务默认为串行的原因。

4. AsyncTask需要在UI线程调用execute()函数

调用execute()函数时调用了onPreExecute(),而onPreExecute()需要在UI线程中执行,所以AsyncTask需要在UI线程调用execute()函数 。(但是如果你不适用onPreExecute在主线程做一些操作的话,execute()可以在任意线程调用)

AsyncTask执行任务的过程原理

提取核心部分,简化后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mWorker = new WorkerRunnable<Params, Result>() {
public Result call() throws Exception {
Result result = doInBackground(mParams);
Binder.flushPendingCommands();
return postResult(result);
}
};
mFuture = new FutureTask<Result>(mWorker) {
@Override
protected void done() {
postResultIfNotInvoked(get());
}
};

AsyncTask的任务执行原理是新建一个有返回值的任务线程FuntureTask和一个Callabler任务WorkerRunnable,在任务中从doInBackground得到的结果Result通过Handler进行处理,调用onProgress和onPostResult(Result result)关于FuntureTask和Callable更多可看Callable和Future、FutureTask的使用

AsyncTask的黑暗面


如果搜索关键词”AsyncTask的缺陷”,会有很多关于AsyncTask缺陷的文章,很多是基于以下几个问题,但这些问题真的是AsyncTask自身的问题还是使用不当导致的呢?这真的需要好好分析一下,不能人云亦云,强行甩锅。

使用不当带来的问题

1. 生命周期和内存泄漏

“当Activity结束或者退出应用时AsyncTask会一直执行doInBackground()方法直到方法执行结束,这可能会导致在onPostExecute时view不存在而导致崩溃溃,以及可能的内存泄露”。
如果退出Activity时AsyncTask扔在执行,上面说的的却会发生,但这些问题需要由使用者来解决而不是AsyncTask来解决,因为AsyncTask只是执行后台任务,它怎么知道你什么时候要终止,要退出,确定使用Handler时不会出现这样问题???

所以应该在相应的生命周期如onDestory调用cancel取消任务,上面的问题就不会发生了,所有这个锅不应该AsyncTask来背。

2. cancel不能正常取消的问题

首先调用cancel终止AsyncTask的原理是对执行异步任务的线程调用interrupt()函数。每个线程内部都有一个boolean型变量表示线程的中断状态,true代表线程处于中断状态,false表示未处于中断状态。
而interrupt()方法的作用只是用来改变线程的中断状态(把线程的中断状态改为true,即被中断)。Thread.interrupt()只在Object.wait() .Object.join(), Object.sleep()几个方法会主动抛出InterruptedException异常,从而结束阻塞状态。而在其他的时候,只是通过设置了Thread的一个标志位信息,需要程序自我进行处理。

所以如果你的AsyncTask后台任务有未做中断的处理肯定会一直执行这个线程。所以这需要你自己在doInbackground里进行中断处理,即使你认为这是个缺陷也应该是Thread类的缺陷,因为要用到线程处理异步任务,AsyncTask无法选择其他方式,而且调用cancel后onPostExecute也不会在执行了,不会导致UI线程的问题,所以这个锅也不应该AsyncTask来背。

3. Activity意外重启,状态消失问题
比如当用户旋转屏幕的时候Activity就会重新启动,如果之前有AsyncTask正在异步加载处理数据,那么之前的数据就会消失,而新的AsyncTask重新创建,这的却是个问题,但你用其他的方式进行请求同样会发生这个问题啊。

其实可能是我们的要求太多了,AsyncTask只是一个处理异步任务的工具,很多逻辑上的东西需要我们自己来处理,就像使用Handler和Thread,不正确处理同样会出现上述问题,这些总得来说就是异步带来的问题,这是一个时间和性能的选择问题,AsyncTask就是简化了包装了Handler的处理步骤而已。上面的这些更应该是一些重要的注意事项,而不是AsyncTask的问题

实际存在的问题

1. 并行串行问题

上面的一些可能是由于使用不当导致的,但并行串行问题方面AsyncTask问题很大。看AsyncTask源码文档时看到这样建议”AsyncTasks should ideally be used for short operations (a few seconds at the most”,就是尽量执行一个短时间的任务,最对也就几秒的任务。当初还很疑惑,AsyncTask这玩意不就是用来处理后台任务的吗,又不是在主线程,为什么还限制短时间的任务,那要你何用啊。

上面分析过在api11后AsyncTask默认的是串行执行任务,基本现上市面上的设备上都将是串行执行。自己可以写个Demo试试看。

这些串行执行共用的AsyncTask的是一个线程池,这真的很严重。因为是顺序执行,导致你调用execute() 可能 没法立刻执行,也可能就执行不了,因为谁知道有没有其他的AsyncTask任务在执行啊,或者任务还是个很耗时的任务,或者就是个while(ture)循环或者for(;;)来一直处理一种后台任务,那么同一进程内的AsyncTask在这之后调用execute的都将无法执行。

想当初初学Android时觉得AsyncTask真是个方便的东西,手机写了个基于socket通信的应用,doInbackground处理,然后直接通知UI。在doInbackground函数里处理一些连接以及数据流的接收及发送,socket的等待连接和数据结束都是阻塞的啊,现在回想起来当初真是年轻😃。

当然你可以立刻执行一个任务通过调用executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)通过AsyncTask的THREAD_POOL_EXECUTOR线程池或者传入其他的线程池来立刻执行任务,但THREAD_POOL_EXECUTOR有最大并发数的限制,但只执行一个任务时通过线程池来管理的也是无奈之举,而且这种方式不是默认方案啊,不容易发现啊。

2. 错误处理问题

AsyncTask没有对发生的一些异常进行处理,你只能在onBackground里进行一些判断,但之外的一些异常情况发生你都无法了解,比如线程异常退出等。

3. 多个任务的管理问题

如果需要多个后台任务,需要新建多个AsyncTask来执行任务,在需要退出的时候你需要对每一个都进行一定的处理来避免内存泄露以及UI问题,这是一个很麻烦的事情。

如果你使用AsyncTask默认的执行方式,出了问题都很难排查。你可以保证你能正确使用AsyncTask,但你没法保证别人也能正确使用啊,这就是别人给你挖的坑,但是你跳了进去啊,关键你可能都不知道到底哪个AsyncTask在执行,可能引用就发生在第三方库的也有可能啊。

替代方案


基于上述实际存在的问题尤其是并行串行问题,写了一个类似AsyncTask的库AsyncTaskScheduler,处理了上述的一些实际存在的问题。

特点

  • 默认多个任务并行处理。
  • 执行单个任务无需使用线程池。
  • 支持自定义线程池。
  • 支持错误处。
  • 多个任务的管理管理方便。
  • 支持任何线程处使用,结果都会在UI线程处理。

方法介绍

很多方法都是和AsyncTask类似。

  • doInBackground
  • onProgressUpdate
  • onExecuteSucceed
  • onExecuteCancelled
  • onExecuteFailed
    发生异常时回调

使用

1.单个任务——是在单个线程里执行,不需要线程池。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SingleAsyncTask singleTask = new SingleAsyncTask<Void,String>() {
@Override
public String doInBackground() {
return null;
}
@Override
public void onExecuteSucceed(String result) {
super.onExecuteSucceed(result);
}
@Override
public void onExecuteFailed(Exception exception) {
super.onExecuteFailed(exception);
Log.i(TAG,"onExecuteCancelled:"+exception.getMessage()+Thread.currentThread());
}
};
singleTask.executeSingle();
//取消通过executeSingle执行的任务
mSingleAsyncTask.cancel(true);

2.多个任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//多个任务新建一个任务调度器
AsyncTaskScheduler mAsyncTaskScheduler = new AsyncTaskScheduler();
SingleAsyncTask singleTask1 = new SingleTask() { ... };
SingleAsyncTask singleTask2 = new SingleTask() { ... };
SingleAsyncTask singleTask3 = new SingleTask() { ... };
...
//并行执行多个任务
mAsyncTaskScheduler.execute(singleTask1)
.execute(singleTask2).execute(singleTask3).
//设置默认的线程池
Executor defaultPoolExecutor = ...
AsyncTaskScheduler mAsyncTaskScheduler = new AsyncTaskScheduler(Executor defaultPoolExecutor);
//取消通过AsyncTaskScheduler任务
mAsyncTaskScheduler.cancelAllTasks(true);

3.确保正确的取消任务以防止避免内存泄露或其他问题