使用AsyncTask时需要注意的隐含bug

1,018 阅读4分钟

最近再一次遇到AsyncTask的bug,发现这个问题还比较普遍,而且比较隐含,在此记录一下。

之前写过一个跟图片相关的app,需要对图片进行裁剪,自然而然去GitHub上寻找开源的图片剪裁库。

先用了UCrop,结果发现在我的app里,进入图片裁剪页面后图片一直加载不出来,运行demo可以正常加载;然后又换了Android-Image-Cropper,发现也是同样的情况,在我的app里裁剪页面加载不出来图片,而demo里都可以正常运行。

然后检查了一遍自己其他地方的代码,感觉没什么问题,令我非常的不解。再看了下这两个项目的issues,发现都有人提出和我一样的问题,但作者都无法复现,没人知道原因。这样就不得不 read the fucking souce code了。

简单浏览后,发现这两个项目都是通过AsyncTask来异步加载图片的,调用的是AsyncTaskexecute(Params... params)方法,整个流程似乎没什么问题。最后就怀疑是不是AsyncTask这个炸弹出了问题,但是我自己的代码压根就没用到AsyncTask

已知AsyncTask的问题是默认它会线性执行每个任务,就是上一个任务执行完成了才会去执行下一个任务,前面的任务没有执行完,后面的任务就一直等待。

想了想我在裁剪图片之前的页面做的事,发现也就是请求加载了一下facebook广告,这里用的是facebook的广告sdk,并不知道sdk的源码,然后我把广告加载给去掉,图片裁剪就正常了。搞半天原来是facebook广告sdk搞的鬼,因为我在国内没翻墙去访问facebook自然一直龟速,广告sdk内部肯定是用AsyncTask执行网络请求的,就导致阻塞了。

虽然现在代替AsyncTask的东西有很多,但是在某些情景下我们还是会用到它。比如刚才的facebook广告sdk,我猜测他们使用AsyncTask是因为他们想尽量保持sdk体积的精简,如果依赖其他第三方的东西来实现网络请求那sdk最终体积就大了,所以只用系统的类。那两个图片剪裁库都使用AsyncTask应该也是同样的原因。

所以我们还是需要好好去了解一下AsyncTask的用法,别被它坑了。

按照AsyncTask官方文档的描述

AsyncTasks should ideally be used for short operations (a few seconds at the most.) If you need to keep threads running for long periods of time, it is highly recommended you use the various APIs provided by the java.util.concurrent package such as Executor, ThreadPoolExecutor and FutureTask.

大意就是AsyncTask只适合做短暂的、最多几秒钟的耗时操作,长时间的耗时操作还是要自己去实现。注意,这里说的是执行AsyncTaskexecute(Params... params)方法的规则,因为这个方法里使用的线程池是AsyncTask.SERIAL_EXECUTOR,特性就是上一个任务执行完成了才会去执行下一个任务。所以一些IO耗时操作就别用execute()执行了。

如果非要执行一些耗时操作,可以调用executeOnExecutor(Executor exec, Params... params),传入一个自定义的线程池。不熟悉线程池概念的自行查询资料。AsyncTask还有个静态常量THREAD_POOL_EXECUTOR,这是一个预定义好的线程池,核心线程数是CPU数量加1,最大线程数量是2倍的CPU数量再加1,传入这个线程池可以让AsyncTask并行执行任务。不过一旦执行任务的数量超过最大线程数量,后续的任务也要等待前面的任务执行完了才行,这时就要自己new一个ThreadPoolExecutor来控制参数了,或者调用Executors.newCachedThreadPool()创建一个0核心线程数、无最大线程数量限制的线程池。建议大家可以简单看看AysncTask源码。

有个小插曲,在Android-Image-Cropper, issues 183里,我跟作者提议使用自定义线程池去执行AsyncTask,他说根据AsyncTask官方文档,我不应该在其他地方把长耗时的工作放在AsyncTask里,但我自己并没有用过AsyncTask,是被facebook广告sdk坑了,我改不了sdk的源码,所以只能让他改下裁剪库的代码。今后我们自己做规范了还不行,还得谨防被别人的库给坑了,特别是引用超多依赖的大项目。

关于线程池的使用,也有一些注意事项,滥用的话会导致内存溢出,我会再另写文章详细阐述。

参考资料

  1. AsyncTask
  2. StackOverflow: Android SDK AsyncTask doInBackground not running
  3. 使用AsyncTask时出现doInBackground没有调用的处理方法
  4. 为什么AsyncTask调用execute方法后很久才执行doInBackground方法??
  5. Github: UCrop, issue 141
  6. Github: Android-Image-Cropper, issues 183