在项目过程中遇到 AsyncTaskLoader 出现 RejectedExecutionException 报错应用退出的问题,问题是偶现的,后面发现根本原因是因为 AsyncTask 的 cancel(false) 方法并没有真正的取消掉任务。下面我写了一个会出现这个问题的 demo,看看这个问题是如何产生的。
问题分析
demo 的关键代码如下:
DataLoader.java
|
|
DataLoader
类是继承AsyncTaskLoader
类的一个通用的类,便于使用。
DemoLoader.java
|
|
DemoLoader
类设计的只是在后台 sleep 500ms,一个非常简单的测试的 Loader。
MainActivity.java
|
|
从上面MainActivity
类可以看出,界面上有一个加载的按钮,点击就会 restart 1 ~ 50 的 loader,在很快地点击这个按钮多次就会出现下面的报错
|
|
从上面的报错信息可以看到,是由于ThreadPoolExecutor.execute
产生的异常,会出现这个异常的原因有两个:1) 在 shutdown 之后执行新的任务,2) 等待队列满了之后执行新的任务。关于RejectedExecutionException
的更多内容可以看这篇文章 How to solve RejectedExecutionException。而在这里出现这个问题原因是因为等待队列已经满了,可是我们只有 50 个id的 loader,而等待队列的长度是 128,为什么会出现队列满的情况的呢?
上面 AsyncTask$InternalHandler.handleMessage 的消息是 MESSAGE_POST_RESULT,是由postResult
方法发出的消息,在这个方法上打上断点,看下更进一步的堆栈:
|
|
下面分析下相关的源码,看看问题出在哪里?
LoaderManager 的 restartLoader 方法
|
|
上面的 49 行info.cancel()
是之前相同 ID 的 loader 已经开始执行,就会去取消之前的 loader,然后再启动当前的 loader。
下面再一步步地跟进这个cancel
方法
|
|
从之前的堆栈信息和上面的源码中可以看出,逻辑上是取消之前的 task,然后在执行当前 loader 的 task,mTask.cancel(false)
中mayInterruptIfRunning
为false
,所以之前的 task 还会继续执行,其实没有真正地取消掉,只是完成后不会传递结果而已。这样就会导致线程池里有多个相同 loader 的不同 task 对象,所以会出现 loader 的 ID 只有 50 个,但是等待队列却会满的情况。
至于线程池的等待队列的长度可以从 AysncTask 的下面代码中看出:
|
|
解决方案
AsyncTaskLoader 的 onCancelLoad方法中,在mTask.cancel(false)
之后一般还会走到cancelLoadInBackground()
,所以我的方案是重写这个方法,在里面调用 ThreadPoolExecutor 的 remove 方法彻底地移除已取消的 task。我把这个重写的方法加在一开始用到的通用的 Loader 类中,代码如下:
|
|
小结
经过上面的几番波折,终于弄清楚了问题的原因,最根本的还是因为 AsyncTask 的 cancel 方法取消的不彻底,关于 AsyncTask 的其他问题大家可以看技术小黑屋的译文:Android 中糟糕的 AsyncTask。
需要注意的是,如果短时间运行超过 128 个不同的 Loader,也可能会出现 RejectedExecutionException 报错,不过一般在我们的应用中不会出现这种情况,大家稍微注意下就好了。
推荐阅读: