12

In Android 4.4 there seems to be a change in the code that causes list icons to be loaded using AsyncTasks. The result is that many of my users on Android 4.4 get RejectedExecutionException since the queue size limit is exceeded.

A clever user at Code Google discovered this, and explained it in this way:

ResolverActivity will throw RejectedExecutionException on Android 4.4.

I viewed the code of latest ResolverActivity and noticed that in ResolveListAdapter.bindView method it is using new LoadIconTask().execute(info), this should be the root cause. LoadIconTask is a subclass of AsyncTask, too many AsyncTask running at the same time will cause RejectedExecutionException.

The ResolverActivity change can be found at the Android GitHub repo.

My app currently has 82 stack traces for RejectedExecutionException, all of which are for Android 4.4. Example start of stack:

java.util.concurrent.RejectedExecutionException: Task android.os.AsyncTask$3@41d44580 rejected from java.util.concurrent.ThreadPoolExecutor@41a575c0[Running, pool size = 5, active threads = 5, queued tasks = 128, completed tasks = 140]
 at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2011)
 at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:793)
 at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1339)
 at android.os.AsyncTask.executeOnExecutor(AsyncTask.java:590)
 at android.os.AsyncTask.execute(AsyncTask.java:535)
 at com.android.internal.app.ResolverActivity$ResolveListAdapter.bindView(ResolverActivity.java:716)
 at com.android.internal.app.ResolverActivity$ResolveListAdapter.getView(ResolverActivity.java:702)
 at android.widget.AbsListView.obtainView(AbsListView.java:2255)
...

Is there any way to sidestep or handle this change?

Halvor Holsten Strand
  • 19,829
  • 17
  • 83
  • 99

1 Answers1

21

The problem lies with the different Executors that AsyncTask uses depending on targetSdkVersion of the app:

1) targetSdkVersion <= 12

AsyncTask.execute() uses the AsyncTask.THREAD_POOL_EXECUTOR. The queue in AsyncTask.THREAD_POOL_EXECUTOR is limited to 128 items. If the queue is full RejectedExecutionException is thrown. This is what happens here

2) targetSdkVersion > 12

AsyncTask uses the AsyncTask.SERIAL_EXECUTOR. AsyncTask.SERIAL_EXECUTOR has an unbounded queue. So in this scenario RejectedExecutionException is never thrown.

Solution 1 (AKA the "clean" solution)

Use a separate APK with targetSdkVersion > 12 and a higher versionCode so that is preferred for HONEYCOMB_MR2 and later versions of Android. This will cause AsyncTask to use ThreadPool.SERIAL_EXECUTOR on HONEYCOMB_MR2 and later version of Android.

Solution 2 (AKA the dirty hack)

Just make AsyncTask.SERIAL_EXECUTOR the default using Reflection.

AsyncTask.class.getMethod("setDefaultExecutor", Executor.class).invoke(null, AsyncTask.SERIAL_EXECUTOR);
HHK
  • 4,852
  • 1
  • 23
  • 40
  • 1
    Thank you. Do you know the reasoning behind earlier versions using the POOL vs SERIAL? Could it break for some phones to force SERIAL? Also, [source](https://github.com/android/platform_frameworks_base/blob/master/core/java/android/app/ActivityThread.java#L4170). – Halvor Holsten Strand Jun 26 '14 at 15:02
  • 1
    The reasoning for switching back and forth is at the [`AsyncTask.execute()`](http://developer.android.com/reference/android/os/AsyncTask.html#execute(Params...)) documentation. Basically using SERIAL_EXECUTOR is the safer default because it leads to less hard to reproduce threading issues so they switched to it. No, I don't think you will break anything by switching to SERIAL_EXECUTOR. – HHK Jun 26 '14 at 15:13
  • Am I correct in assuming that since SERIAL_EXECUTOR was added in API level 11 the "hack" only really works for minSdkVersion 11 or 12 (or above, but then use "clean" anyway)? While your "clean" solution will work for minSdkVersions less than 11, but won't actually change anything if the API is that low. – Halvor Holsten Strand Jun 27 '14 at 15:00
  • Yes, that is correct. Your original bug report was for your app running on KitKat. Do you have problems with RejectedExecutionException on other Android versions as well? – HHK Jun 27 '14 at 15:13
  • No, just clarifying when the hack may be useful. Setting targetSdkVersion is a solid option. Thank you. – Halvor Holsten Strand Jun 27 '14 at 16:01