解决 Android 中 AsyncTask 的多线程阻塞问题

Android 开发中执行耗时操作并更新 UI 时,通常有三种方式:

  1. 直接调用 runOnUiThread(new Runnable(){}) ,使用简单,但不能在 Activity 之外的环境使用,如 ViewDialog 等;

  2. 使用 AsyncTask 实现,通过 onPreExecute() doInBackground()onPostExecute 三个方法能方便的分开 UI 操作和耗时操作,避免 UI 线程阻塞,并且支持参数传递;

  3. Handler 结合 Message 实现,比较重量级,但还需要用到 Looper 等,而且 Message 封装的 Bundle 对象不能太大,否则会抛异常。

几种方式各有利弊,实际开发中应根据需要选取合适方式, 如 Activity 中可采用方法 1 ,其他场合可使用方式 2 和 3 。

公司的项目中最近经常出现数据加载不出来的问题,而且切换网络环境后,出现概率会变小,因此起初一直以为是 HTTP 请求模块的问题,将 Socket 的 timeout 设置为一分钟后,一开始确实没有再重现,但过了几天问题又重现了,于是通过转包并请后台 API 相关开发人员查看日志分析,发现请求根本都没发出去!这样进一步确定是 APP 这边的问题,但究竟出在哪里一片茫然。

今天进行代码重构,涉及到 AsyncTask 这部分代码,通过 Debug 竟然发现 onPreExecute() 方法执行之后迟迟不进入 doInBackground() 方法!看来这就是问题所在。前一个 task 执行后,后一个 task 一直无法从 pending 状态进入 running 状态,说明线程阻塞了。

看了源码之后发现,AsyncTask 内部是通过 java.util.concurrent 下的 ThreadPoolExecutor 线程池实现的:

public static final Executor THREAD_POOL_EXECUTOR = 
	new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
		TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);

不过 2.3 以后新的 API 中线程池容量设置改动了(活动线程数由 10 改为 1),所以很容易阻塞。

2.2.2_r1 :

private static final int CORE_POOL_SIZE = 5;
private static final int MAXIMUM_POOL_SIZE = 128;
private static final int KEEP_ALIVE = 10;

2.3_r1 :

private static final int CORE_POOL_SIZE = 5;
private static final int MAXIMUM_POOL_SIZE = 128;
private static final int KEEP_ALIVE = 1;

所幸 API 11 提供了一个 executeOnExecutor(Executor exec, Params... params) 方法,可以自定义线程池。

并且内部除了前面提到的 THREAD_POOL_EXECUTOR ,还另外提供了一个 Executor 类型的常量 SERIAL_EXECUTOR :

private static class SerialExecutor implements Executor {
	final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
	Runnable mActive;

	public synchronized void execute(final Runnable r) {
		mTasks.offer(new Runnable() {
			public void run() {
				try {
					r.run();
				} finally {
					scheduleNext();
				}
			}
		});
		if (mActive == null) {
			scheduleNext();
		}
	}

	protected synchronized void scheduleNext() {
		if ((mActive = mTasks.poll()) != null) {
			THREAD_POOL_EXECUTOR.execute(mActive);
		}
	}
}

首先字面上就能看出是串行的,然后内部 scheduleNext() 方法的 synchronized 关键字也能看出来不能并发执行多任务。

所以要想比较好的支持并发,还得自己定制线程池:

private static final int corePoolSize = 15;
private static final int maximumPoolSize = 30;
private static final int keepAliveTime = 5;

private static final BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>(maximumPoolSize);
private static final Executor threadPoolExecutor = new ThreadPoolExecutor(
	corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, workQueue);

void addTaskInPool(AsyncTask asyncTask, Params... params) {
	if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB){
		asyncTask.executeOnExecutor(threadPoolExecutor, params);
	}else{
		asyncTask.execute(params);
	}
}

其中 corePoolSizemaximumPoolSize 可以根据处理器数目确定:

int procNum = Runtime.getRuntime().availableProcessors();

这样同时跑多个 task 线程就毫无压力了。