Android 中 LoaderManager 用 restartLoader 频繁启动 AsyncTaskLoader 产生 RejectedExecutionException 的解决方案

在项目过程中遇到 AsyncTaskLoader 出现 RejectedExecutionException 报错应用退出的问题,问题是偶现的,后面发现根本原因是因为 AsyncTask 的 cancel(false) 方法并没有真正的取消掉任务。下面我写了一个会出现这个问题的 demo,看看这个问题是如何产生的。

问题分析

demo 的关键代码如下:

DataLoader.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public abstract class DataLoader<T> extends AsyncTaskLoader<T>{
private T mData;
public DataLoader(Context context){
super(context);
}
@Override
public void deliverResult(T data) {
if(isReset()) {
// An async query came in while the loader is stopped. We
// don't need the result.
if(null != data){
onReleaseResources(data);
}
}
T oldData = mData;
mData = data;
if(isStarted()) {
// If the Loader is currently started, we can immediately
// deliver its results.
super.deliverResult(data);
}
if(null != oldData && oldData != mData) {
onReleaseResources(oldData);
}
}
@Override
protected void onStartLoading() {
if(null != mData) {
// If we currently have a result available, deliver it
// immediately.
deliverResult(mData);
}
if(takeContentChanged() || null == mData) {
// If the data has changed since the last time it was loaded
// or is not currently available, start a load.
forceLoad();
}
}
@Override
protected void onStopLoading() {
// Attempt to cancel the current load task if possible.
cancelLoad();
}
@Override
public void onCanceled(T data) {
super.onCanceled(data);
// At this point we can release the resources
// if needed.
onReleaseResources(data);
}
@Override
protected void onReset() {
super.onReset();
// Ensure the loader is stopped
onStopLoading();
// At this point we can release the resources
// if needed.
if(null != mData) {
onReleaseResources(mData);
mData = null;
}
}
protected void onReleaseResources(T data) {
// like a cursor, we should close it here.
}
}

DataLoader类是继承AsyncTaskLoader类的一个通用的类,便于使用。

DemoLoader.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class DemoLoader<T> extends DataLoader<T> {
private int mID;
public DemoLoader(Context context, int id){
super(context);
mID = id;
}
@Override
public T loadInBackground() {
Log.e("loader ","currently executing the task " + mID);
try {
Thread.currentThread().sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.e("loader ","just completed the task " + mID);
return null;
}
}

DemoLoader类设计的只是在后台 sleep 500ms,一个非常简单的测试的 Loader。

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class MainActivity extends ActionBarActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.textView).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startLoading(50);
}
});
}
private void startLoading(int num) {
for(int i = 0; i < num; i ++) {
getLoaderManager().restartLoader(i, null, new DataLoaderCall());
}
}
class DataLoaderCall implements LoaderManager.LoaderCallbacks<String> {
@Override
public Loader<String> onCreateLoader(int id, Bundle args) {
return new DemoLoader<>(MainActivity.this, id);
}
@Override
public void onLoadFinished(Loader<String> loader, String data) {}
@Override
public void onLoaderReset(Loader<String> loader) {}
}

从上面MainActivity类可以看出,界面上有一个加载的按钮,点击就会 restart 1 ~ 50 的 loader,在很快地点击这个按钮多次就会出现下面的报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
02-18 16:52:44.788 27635-27635/com.johnnyshieh.demo E/AndroidRuntime: java.util.concurrent.RejectedExecutionException: Task android.os.AsyncTask$3@3dcdd7e4 rejected from java.util.concurrent.ThreadPoolExecutor@26c4944d[Running, pool size = 17, active threads = 17, queued tasks = 128, completed tasks = 0]
02-18 16:52:44.788 27635-27635/com.johnnyshieh.demo E/AndroidRuntime: at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2011)
02-18 16:52:44.788 27635-27635/com.johnnyshieh.demo E/AndroidRuntime: at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:793)
02-18 16:52:44.788 27635-27635/com.johnnyshieh.demo E/AndroidRuntime: at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1339)
02-18 16:52:44.788 27635-27635/com.johnnyshieh.demo E/AndroidRuntime: at android.os.AsyncTask.executeOnExecutor(AsyncTask.java:594)
02-18 16:52:44.788 27635-27635/com.johnnyshieh.demo E/AndroidRuntime: at android.content.AsyncTaskLoader.executePendingTask(AsyncTaskLoader.java:235)
02-18 16:52:44.788 27635-27635/com.johnnyshieh.demo E/AndroidRuntime: at android.content.AsyncTaskLoader.onForceLoad(AsyncTaskLoader.java:166)
02-18 16:52:44.788 27635-27635/com.johnnyshieh.demo E/AndroidRuntime: at android.content.Loader.forceLoad(Loader.java:347)
02-18 16:52:44.788 27635-27635/com.johnnyshieh.demo E/AndroidRuntime: at com.johnnyshieh.demo.DataLoader.onStartLoading(DataLoader.java:45)
02-18 16:52:44.788 27635-27635/com.johnnyshieh.demo E/AndroidRuntime: at android.content.Loader.startLoading(Loader.java:290)
02-18 16:52:44.788 27635-27635/com.johnnyshieh.demo E/AndroidRuntime: at android.app.LoaderManagerImpl$LoaderInfo.start(LoaderManager.java:285)
02-18 16:52:44.788 27635-27635/com.johnnyshieh.demo E/AndroidRuntime: at android.app.LoaderManagerImpl.installLoader(LoaderManager.java:574)
02-18 16:52:44.788 27635-27635/com.johnnyshieh.demo E/AndroidRuntime: at android.app.LoaderManagerImpl$LoaderInfo.onLoadCanceled(LoaderManager.java:417)
02-18 16:52:44.788 27635-27635/com.johnnyshieh.demo E/AndroidRuntime: at android.content.Loader.deliverCancellation(Loader.java:156)
02-18 16:52:44.788 27635-27635/com.johnnyshieh.demo E/AndroidRuntime: at android.content.AsyncTaskLoader.dispatchOnCancelled(AsyncTaskLoader.java:247)
02-18 16:52:44.788 27635-27635/com.johnnyshieh.demo E/AndroidRuntime: at android.content.AsyncTaskLoader$LoadTask.onCancelled(AsyncTaskLoader.java:103)
02-18 16:52:44.788 27635-27635/com.johnnyshieh.demo E/AndroidRuntime: at android.os.AsyncTask.finish(AsyncTask.java:634)
02-18 16:52:44.788 27635-27635/com.johnnyshieh.demo E/AndroidRuntime: at android.os.AsyncTask.access$500(AsyncTask.java:177)
02-18 16:52:44.788 27635-27635/com.johnnyshieh.demo E/AndroidRuntime: at android.os.AsyncTask$InternalHandler.handleMessage(AsyncTask.java:653)
02-18 16:52:44.788 27635-27635/com.johnnyshieh.demo E/AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:111)
02-18 16:52:44.788 27635-27635/com.johnnyshieh.demo E/AndroidRuntime: at android.os.Looper.loop(Looper.java:199)
02-18 16:52:44.788 27635-27635/com.johnnyshieh.demo E/AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:5754)
02-18 16:52:44.788 27635-27635/com.johnnyshieh.demo E/AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method)
02-18 16:52:44.788 27635-27635/com.johnnyshieh.demo E/AndroidRuntime: at java.lang.reflect.Method.invoke(Method.java:372)
02-18 16:52:44.788 27635-27635/com.johnnyshieh.demo E/AndroidRuntime: at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:982)
02-18 16:52:44.788 27635-27635/com.johnnyshieh.demo E/AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:777)

从上面的报错信息可以看到,是由于ThreadPoolExecutor.execute产生的异常,会出现这个异常的原因有两个:1) 在 shutdown 之后执行新的任务,2) 等待队列满了之后执行新的任务。关于RejectedExecutionException的更多内容可以看这篇文章 How to solve RejectedExecutionException。而在这里出现这个问题原因是因为等待队列已经满了,可是我们只有 50 个id的 loader,而等待队列的长度是 128,为什么会出现队列满的情况的呢?

上面 AsyncTask$InternalHandler.handleMessage 的消息是 MESSAGE_POST_RESULT,是由postResult方法发出的消息,在这个方法上打上断点,看下更进一步的堆栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
java.lang.Thread.State: WAITING
at android.os.AsyncTask.postResult(AsyncTask.java:324)
at android.os.AsyncTask.postResultIfNotInvoked(AsyncTask.java:316)
at android.os.AsyncTask.access$400(AsyncTask.java:177)
at android.os.AsyncTask$3.done(AsyncTask.java:307)
at java.util.concurrent.FutureTask.finishCompletion(FutureTask.java:355)
at java.util.concurrent.FutureTask.cancel(FutureTask.java:151)
at android.os.AsyncTask.cancel(AsyncTask.java:472)
at android.content.AsyncTaskLoader.onCancelLoad(AsyncTaskLoader.java:193)
at android.content.Loader.cancelLoad(Loader.java:320)
at android.app.LoaderManagerImpl$LoaderInfo.cancel(LoaderManager.java:350)
at android.app.LoaderManagerImpl.restartLoader(LoaderManager.java:701)
at com.johnnyshieh.demo.MainActivity.startLoading(MainActivity.java:58)

下面分析下相关的源码,看看问题出在哪里?

LoaderManager 的 restartLoader 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public <D> Loader<D> restartLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) {
if (mCreatingLoader) {
throw new IllegalStateException("Called while creating a loader");
}
LoaderInfo info = mLoaders.get(id);
if (DEBUG) Log.v(TAG, "restartLoader in " + this + ": args=" + args);
if (info != null) {
LoaderInfo inactive = mInactiveLoaders.get(id);
if (inactive != null) {
if (info.mHaveData) {
// This loader now has data... we are probably being
// called from within onLoadComplete, where we haven't
// yet destroyed the last inactive loader. So just do
// that now.
if (DEBUG) Log.v(TAG, " Removing last inactive loader: " + info);
inactive.mDeliveredData = false;
inactive.destroy();
info.mLoader.abandon();
mInactiveLoaders.put(id, info);
} else {
// We already have an inactive loader for this ID that we are
// waiting for! What to do, what to do...
if (!info.mStarted) {
// The current Loader has not been started... we thus
// have no reason to keep it around, so bam, slam,
// thank-you-ma'am.
if (DEBUG) Log.v(TAG, " Current loader is stopped; replacing");
mLoaders.put(id, null);
info.destroy();
} else {
// Now we have three active loaders... we'll queue
// up this request to be processed once one of the other loaders
// finishes or is canceled.
if (DEBUG) Log.v(TAG, " Current loader is running; attempting to cancel");
/// M: Setup pending loader first then calls cancel function.
/// M: In cancel function, it will restart pending loader
//info.cancel();
if (info.mPendingLoader != null) {
if (DEBUG) Log.v(TAG, " Removing pending loader: " + info.mPendingLoader);
info.mPendingLoader.destroy();
info.mPendingLoader = null;
}
if (DEBUG) Log.v(TAG, " Enqueuing as new pending loader");
info.mPendingLoader = createLoader(id, args,
(LoaderManager.LoaderCallbacks<Object>)callback);
/// M: Setup pending loader first then calls cancel. @{
LoaderInfo pending = info.mPendingLoader;
info.cancel();
//return (Loader<D>)info.mPendingLoader.mLoader;
return (Loader<D>) pending.mLoader;
/// M: }@
}
}
} else {
// Keep track of the previous instance of this loader so we can destroy
// it when the new one completes.
if (DEBUG) Log.v(TAG, " Making last loader inactive: " + info);
info.mLoader.abandon();
mInactiveLoaders.put(id, info);
}
}
info = createAndInstallLoader(id, args, (LoaderManager.LoaderCallbacks<Object>)callback);
return (Loader<D>)info.mLoader;
}

上面的 49 行info.cancel()是之前相同 ID 的 loader 已经开始执行,就会去取消之前的 loader,然后再启动当前的 loader。

下面再一步步地跟进这个cancel方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
void cancel() {
if (DEBUG) Log.v(TAG, " Canceling: " + this);
if (mStarted && mLoader != null && mListenerRegistered) {
if (!mLoader.cancelLoad()) {
onLoadCanceled(mLoader);
}
}
}
// cancelLoad 方法会一路走到 AsyncTaskLoader 的 onCancelLoad 方法,如下
@Override
protected boolean onCancelLoad() {
if (DEBUG) Log.v(TAG, "onCancelLoad: mTask=" + mTask);
if (mTask != null) {
if (mCancellingTask != null) {
// There was a pending task already waiting for a previous
// one being canceled; just drop it.
if (DEBUG) Log.v(TAG,
"cancelLoad: still waiting for cancelled task; dropping next");
if (mTask.waiting) {
mTask.waiting = false;
mHandler.removeCallbacks(mTask);
}
mTask = null;
return false;
} else if (mTask.waiting) {
// There is a task, but it is waiting for the time it should
// execute. We can just toss it.
if (DEBUG) Log.v(TAG, "cancelLoad: task is waiting, dropping it");
mTask.waiting = false;
mHandler.removeCallbacks(mTask);
mTask = null;
return false;
} else {
boolean cancelled = mTask.cancel(false);
if (DEBUG) Log.v(TAG, "cancelLoad: cancelled=" + cancelled);
if (cancelled) {
mCancellingTask = mTask;
cancelLoadInBackground();
}
mTask = null;
return cancelled;
}
}
return false;
}
// 而 AysncTask 的 cancel 方法有下面的描述
* <p>Attempts to cancel execution of this task. This attempt will
* fail if the task has already completed, already been cancelled,
* or could not be cancelled for some other reason. If successful,
* and this task has not started when <tt>cancel</tt> is called,
* this task should never run. If the task has already started,
* then the <tt>mayInterruptIfRunning</tt> parameter determines
* whether the thread executing this task should be interrupted in
* an attempt to stop the task.</p>

从之前的堆栈信息和上面的源码中可以看出,逻辑上是取消之前的 task,然后在执行当前 loader 的 task,mTask.cancel(false)mayInterruptIfRunningfalse,所以之前的 task 还会继续执行,其实没有真正地取消掉,只是完成后不会传递结果而已。这样就会导致线程池里有多个相同 loader 的不同 task 对象,所以会出现 loader 的 ID 只有 50 个,但是等待队列却会满的情况。

至于线程池的等待队列的长度可以从 AysncTask 的下面代码中看出:

1
2
3
4
5
6
7
8
9
private static final BlockingQueue<Runnable> sPoolWorkQueue =
new LinkedBlockingQueue<Runnable>(128);
/**
* An {@link Executor} that can be used to execute tasks in parallel.
*/
public static final Executor THREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);

解决方案

AsyncTaskLoader 的 onCancelLoad方法中,在mTask.cancel(false)之后一般还会走到cancelLoadInBackground(),所以我的方案是重写这个方法,在里面调用 ThreadPoolExecutor 的 remove 方法彻底地移除已取消的 task。我把这个重写的方法加在一开始用到的通用的 Loader 类中,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
/*
* Copyright (C) 2015 Johnny Shieh Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import com.johnnyshieh.commonlibrary.utils.ReflectionUtils;
import android.content.AsyncTaskLoader;
import android.content.Context;
import android.os.AsyncTask;
import java.util.concurrent.ThreadPoolExecutor;
/**
* @author Johnny Shieh
* @version 1.0
*/
public abstract class DataLoader<T> extends AsyncTaskLoader<T>{
private T mData;
public DataLoader(Context context){
super(context);
}
@Override
public void deliverResult(T data) {
if(isReset()) {
// An async query came in while the loader is stopped. We
// don't need the result.
if(null != data){
onReleaseResources(data);
}
}
T oldData = mData;
mData = data;
if(isStarted()) {
// If the Loader is currently started, we can immediately
// deliver its results.
super.deliverResult(data);
}
if(null != oldData && oldData != mData) {
onReleaseResources(oldData);
}
}
@Override
protected void onStartLoading() {
if(null != mData) {
// If we currently have a result available, deliver it
// immediately.
deliverResult(mData);
}
if(takeContentChanged() || null == mData) {
// If the data has changed since the last time it was loaded
// or is not currently available, start a load.
forceLoad();
}
}
@Override
protected void onStopLoading() {
// Attempt to cancel the current load task if possible.
cancelLoad();
}
@Override
public void onCanceled(T data) {
super.onCanceled(data);
// At this point we can release the resources
// if needed.
onReleaseResources(data);
}
@Override
public void cancelLoadInBackground() {
// AsyncTaskLoader use AsyncTask to do the background task.
// when task started, it may just add to ThreadPoolExecutor's workQueue which wait to execute.
// so when we call LoaderManager's restartLoader method, the last loader may still in the workQueue.
// when workQueue is full, RejectedExecutionException will be thrown.
Object taskObj = ReflectionUtils.getField(this, "android.content.AsyncTaskLoader", "mTask");
if(null != taskObj) {
Object futureObj = ReflectionUtils.getField(taskObj, "android.os.AsyncTask", "mFuture");
if(null != futureObj && futureObj instanceof Runnable && AsyncTask.THREAD_POOL_EXECUTOR instanceof ThreadPoolExecutor) {
((ThreadPoolExecutor) AsyncTask.THREAD_POOL_EXECUTOR).remove((Runnable)futureObj);
}
}
}
@Override
protected void onReset() {
super.onReset();
// Ensure the loader is stopped
onStopLoading();
// At this point we can release the resources
// if needed.
if(null != mData) {
onReleaseResources(mData);
mData = null;
}
}
protected void onReleaseResources(T data) {
// like a cursor, we should close it here.
}
}

小结

经过上面的几番波折,终于弄清楚了问题的原因,最根本的还是因为 AsyncTask 的 cancel 方法取消的不彻底,关于 AsyncTask 的其他问题大家可以看技术小黑屋的译文:Android 中糟糕的 AsyncTask

需要注意的是,如果短时间运行超过 128 个不同的 Loader,也可能会出现 RejectedExecutionException 报错,不过一般在我们的应用中不会出现这种情况,大家稍微注意下就好了。

推荐阅读:

END
Johnny Shieh wechat
我的公众号,不只有技术,还有咖啡和彩蛋!