对于AsyncTask的理解一直是浮于表面,讲道理有了轮子之后基础都掌握不好了……源码虽然也看过也都是一知半解,所以这一次来完整的总结一下吧。
首先AsyncTask,意如其名是一种异步任务,主要来实现异步操作的,我们知道在Android系统中,异步操作可以通过Handler来完成,但是Handler这个东西吧,我是不太喜欢的,我觉得写起来很古怪,而且维护也不太有感觉,对于更新UI,更喜欢用runOnUiThread,尽管Android系统中,大部分可以达成异步效果的类和方法都是基于Handler实现的。
对于AsyncTask其实也不例外, 从AsyncTask的"小源"里,我们可以看到Handler的影子。
AsyncTask内部类
1. 使用回顾
看一下源码是如何定义的。
public abstract class AsyncTask<Params, Progress, Result>
关键点在:抽象,三个泛型。
抽象的方法如下:
通过复写此方法传入Params,来在后台完成计算(耗时)工作,在这个方法中可以调用更新进度方法publishProgress来更新进度,然后可以通过Result来携带参数返回。
那么现在就有了,新建一个自己的AsyncTask,模拟一个耗时操作。
public class MyAsyncTask extends AsyncTask<String,Integer,String> {
@Override
protected String doInBackground(String... strings) {
//打印传入的参数
for (String str : strings){
Log.d("async",str);
}
//模拟耗时操作
int i = 0;
while (i < 100){
try {
Thread.sleep(100);
//更新进度
publishProgress(i + 1);
} catch (InterruptedException e) {
e.printStackTrace();
}
i++;
}
if(true){
return "//执行成功的数据";
}else{
return "//执行失败的数据";
}
}
}
然后执行:
new MyAsyncTask().execute("count_down","p1","p2","p3");
执行后即可在doInBackground中,打印传入的参数。
此外,我们经常需要在界面上更新进度,这也是publishProgress方法诞生的原因,但是需要重写onProgressUpdate方法,这里的值就是publishProgress传递来的。
这样就可以区别了,耗时操作必然不能在UI线程执行,即doInBackground是在子线程,onProgressUpdate是在主线程执行的。
除了这两个比较重要的方法之外,还有两个比较好用的方法
第一个是onPreExecute,运行在UI线程,在doInBackground执行之前可以做一些准备工作,比如说初始化一些标记,一些界面元素
第二个是onPostExecute,运行在UI线程,在doInBackground之后调用,其中Result参数,就是doInBackground所计算,所返回的参数, 比较重要的就是,当任务被取消这个方法是不会执行的,注意套路。
对于这个值是怎么进去的,简单看下。
首先找到调用方,是一个finish方法传入了result,同时印证了如果取消了任务,会回调onCancelled而不是onPostExecute
向上查找finish的调用方,发现是在InternalHandler里执行的,也就意味着,此时已经切换了线程了。
由于是Handler,所以要找message是在哪里发送的。
这里也不用翻着去找了,直接在find我们的doInBackground即可,因为他可是数据源。
可以很清楚的看到,第333行首先执行了doInBackground方法获取了结果,然后在返回result的同时还调用了postResult()将数据发送出去。
如果不出所料,这里就是Message的舞台了。
流程大概就这样,其实看起来也没有什么高难度操作。
最后就简单提一句取消的操作,需要我们复写下面的方法
这两个方法其实是类似的,只不过一个可以让你对参数进行操作,但是看到方法体你要注意了,如果你重写了这个方法不要super,因为父类就是调用了空参数的onCancelled。
2. 使用要点
这里才是这篇文章的开始。
主要是记录在使用AsyncTask中遇到的,几个问题。
Q1:一个AsyncTask只能执行一次?
上面两个bug就已经说明了,运行中的或者运行结束的AsyncTask都不可以再次运行了,所以在上面的Demo例子中,我使用的是
new MyAsyncTask().execute("count_down","p1","p2","p3");
下面就探索下到底是为啥?
首先我们刚才查看AsyncTask发现了不少枚举的量。
这三个状态分别代表了任务的三种状态,并且注释中说了在生命周期内这三种状态只将被设置一次。
确定状态后,进入调用方法。
这就是在MainActivity中调用的execute方法,由于注释太长,这里直截取了方法体部分,关于注释里的内容,在下面说几点。
- 需要在主线程执行
- 方法返回的是this,用于调用者保持引用状态。
- 任务是交给线程或者线程池来执行的,而这个选择是跟系统平台有关
,在历史版本中,执行方式有各种实现,看起来在高性能设备上并行越多越好的样子,但其实由于是执行任务,如果并行太多很有可能会影响到数据的准确性。
上面说的比较重要的一点就是执行方式,可以看到execute,使用的是一个sDefaultExecutor的线程池 ,而sDefaultExecutor又是一个SERIAL_EXECUTOR,
而SERIAL_EXECUTOR又是一个串行执行的SerialExecutor
串行,顾名思义就是排好队挨个挨揍。
貌似扯得有点不对目前的问题,但是为啥只能执行一次,这个error从哪里抛出来的,还要去找找。
找啥找啊……
就在前面,刚才看到了execute调用的就是executeOnExecutor,其中exec参数,就是系统默认的串行线程池。
这里我圈出了三个部分,第一个是状态判断,如果当前不是等待状态,说明在运行中或者运行结束,此时就是问题的关键了,抛出异常。
这样做的目的其实可以简单联想一下,比如说界面上有一个按钮,点击执行task,假如说出现按键抖动(搞的跟单片机似的),就是不小心点了两次,那这两个任务就出问题了,同时对一个UI操作……
如果抛异常就不同了,开发者可以catch住,而不怕UI上出问题。【仅个人猜想】
那对于这个方法,我们要看的点不仅仅是这个,包括第二个框中,我们知道了onPreExecute的调用时机,是在execute之后立马调用,然后第三个框,是串行执行mFuture这个任务。
什么,你问任务在哪创建出来的?刚才分析result的时候,就遇到过,就在终极构造方法里。
public AsyncTask(@Nullable Looper callbackLooper) { …… }
此外,这个方法允许我们传入自己的线程池执行器,但是引发的问题人家可不保证,特别在并行情况下,任务执行的顺序不敢保证,万一有需要同步顺序执行的任务,那就凉凉了。
总结下。
- 任务执行过或者执行中不能再次调用excute了,但是可以通过new一个新的,来实现多次执行。
- 慎重选择线程池来替代官方所用的串行线程池,除非需求场景使用串行会出现问题,如下面。
Q2: 拒绝执行?宝宝也是有脾气的!
首先这是一个在任务要运行时候抛出的异常RejectedExecutionException,bug产生的场景是在后台JobService不断循环(有时间间隔)执行此任务。
关注第4行,方括号里的内容意味着ScheduleThreadPoolExecutor已经终止。
回想一下Java里的线程池知识,拒绝执行的一个场景就是在shutdown()的情况下,旧的任务可在shutdown()之后运行,但是新的任务就不可以在进入了。另一个场景就是当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务。
这样看来像是第一种情况导致的。
一开始是以为任务调度不开而导致的拒绝服务,后来根据日志又想了下,应该是shutdown导致的,那如果是shutdown导致的,那么得找为什么会shutdown,很可惜,这里的日志我并没确定问题。如果有解决过类似问题的小伙伴希望能分享下经验。
Q3: 拒绝服务的另一种场景
上面说过,当shutdown()调用后,就会导致拒绝服务,同样当任务量达到某线程池的容量上线,也会导致拒绝服务。
首先我们前面说了,AsyncTask里面有一个SerialExecutor,但实际上他只是一个调度用的线程池,而不是执行用的线程池。
源码如下:
仔细看,发现这个方法只是一个调度方法,也怪不得sDefaultExecutor要声明成静态的,所有的AsyncTask使用的是一个公用的调度线程池sDefaultExecutor,真正的执行任务的大佬是THREAD_POOL_EXECUTOR,有点混乱,下面梳理一下。
AsyncTask = 两个ThreadPool + 一个Handler,其中,SerialExecutor是用来调度任务的,是一个静态的线程池,所有AsyncTask共用,而THREAD_POOL_EXECUTOR是一个自定义的线程池,用于执行任务,也是所有AsyncTask共用。
但是对于ThreadPoolExecutor也不是有线程数量限制的,核心线程保证在2-4个。
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;
这样你就产生疑问了,既然SerialExecutor负责调度,是串行的,一次就调度一个任务,那ThreadPoolExecutor能并行执行的再多也没用啊,任务都不够用。
确实是这样,在目前的版本中,一次只能运行一个AsyncTask,其实这样做也是代码的调整后的结果,因为在现在的代码中,我们可以用自己定义的线程池去调度任务,这样就可以根据自己的应用场景来选择合适线程池。
使用自己的线程池就可以并行去调度任务,然后ThreadPoolExecutor就能发挥它的价值,而这样出现因容量满了而导致的RejectedExecutionException可能性就降低了。
所以在上面的Q2中,暂时的解决方案如上图。
3. 总结
这篇文章主要总结了AsyncTask的使用要点和一些常见的问题,其中只能执行一次,拒绝服务的触发点这些地方不遇到还真不一定能知道,如果你感兴趣,可以自行写demo测试一下到底是不是这样。
网友评论