21、AsyncTask与线程池:AsyncTask使用与缺陷、ThreadPoolExecutor配置、Callable与Future
说到Android里的异步任务,很多老开发都会叹口气。AsyncTask,这个曾经被写进官方文档的“亲儿子”,如今已经被标记为废弃。我刚开始做Android那会儿,几乎每个网络请求都用它。后来踩了坑,才明白为什么官方要放弃它。
说白了,AsyncTask的问题在于——它太“黑盒”了。你只管往里面塞任务,它怎么调度、怎么并发,你完全控制不了。这在简单场景下没问题,但项目一复杂,各种诡异Bug就冒出来了。
AsyncTask的“罪与罚”
先说说AsyncTask的正确用法。它本质上是一个封装好的异步框架,内部维护了一个线程池和一个Handler。你只需要继承它,实现几个回调方法就行。
private class DownloadTask extends AsyncTask<String, Integer, String> {
@Override
protected void onPreExecute() {
// 主线程,开始前调用
progressBar.setVisibility(View.VISIBLE);
}
@Override
protected String doInBackground(String... urls) {
// 子线程,真正干活的地方
String result = "";
for (int i = 0; i < urls.length; i++) {
result += downloadUrl(urls[i]);
publishProgress((i + 1) * 100 / urls.length);
}
return result;
}
@Override
protected void onProgressUpdate(Integer... values) {
// 主线程,更新进度
progressBar.setProgress(values[0]);
}
@Override
protected void onPostExecute(String result) {
// 主线程,任务完成
textView.setText(result);
progressBar.setVisibility(View.GONE);
}
}
嗯,这里要注意——doInBackground是在子线程跑的,其他三个回调都在主线程。这个设计本身没问题,问题出在它的线程池策略上。
- 内存泄漏:AsyncTask持有Activity的引用,如果任务还没完成Activity就被销毁了,那这个Activity就泄漏了。我有个项目因此OOM崩溃了好几次。
- 串行执行:在Android 1.6之后,AsyncTask默认是串行执行的。你同时开10个任务,它一个一个跑。你以为在并发,其实在排队。
- 生命周期不同步:任务在后台跑着,用户旋转了屏幕,Activity重建了。这时候
onPostExecute更新的是旧的View,直接崩溃。 - 结果丢失:Activity被销毁后,AsyncTask的结果没人接收了。你辛辛苦苦下载完,发现没地方展示。
为什么会这样?因为AsyncTask把线程管理和UI更新的逻辑耦合在一起了。你想想看,一个工具类既要管线程调度,又要管生命周期,还要管UI刷新——它管得过来吗?
ThreadPoolExecutor:把控制权拿回来
我个人习惯,从2015年开始就全面转向ThreadPoolExecutor了。它才是真正让你掌控一切的线程池方案。
先看一个我常用的配置模板:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // corePoolSize:核心线程数
4, // maximumPoolSize:最大线程数
30, // keepAliveTime:空闲线程存活时间
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<Runnable>(128), // 任务队列
Executors.defaultThreadFactory(), // 线程工厂
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
这几个参数怎么配?我简单说说我的经验:
| 参数 | 说明 | 我的建议 |
|---|---|---|
| corePoolSize | 一直活着的线程数 | CPU密集型设为核心数+1,IO密集型设为核心数*2 |
| maximumPoolSize | 最多能创建的线程数 | 不要超过核心数的2倍,否则线程切换开销太大 |
| keepAliveTime | 超过核心数的线程空闲多久后被回收 | 30秒到60秒比较合适 |
| workQueue | 任务排队的地方 | 有界队列,防止内存被撑爆 |
| RejectedExecutionHandler | 队列满了怎么办 | AbortPolicy直接抛异常,或者自定义策略记录日志 |
Callable与Future:带返回值的异步任务
Runnable有个硬伤——它没有返回值。你需要在外面定义一个变量,然后在Runnable里面赋值,再用各种同步机制去读。麻烦不麻烦?
Callable就是来解决这个问题的。它跟Runnable很像,但可以返回结果,还能抛异常。
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
// 模拟耗时操作
Thread.sleep(2000);
return "下载完成";
}
};
Future<String> future = executor.submit(callable);
// 做其他事情...
try {
String result = future.get(); // 阻塞,直到拿到结果
Log.d("TAG", result);
} catch (ExecutionException e) {
// 处理call()里抛出的异常
Log.e("TAG", "任务执行异常", e);
} catch (InterruptedException e) {
// 线程被中断
Log.e("TAG", "任务被中断", e);
}
Future的get()方法是阻塞的。如果你不想一直等着,可以用get(long timeout, TimeUnit unit)设置超时时间。超时了就抛TimeoutException,你可以决定是重试还是放弃。
还有个实用方法——future.cancel(true)。我在项目中遇到过一种情况:用户下拉刷新时,如果上一个请求还没回来,就直接取消掉。用cancel方法就很方便。
- AsyncTask已废弃,不要再在新项目中使用
- ThreadPoolExecutor是线程池的正确打开方式,参数要按需配置
- Callable+Future适合需要返回结果的异步任务
- 记得给线程池设置有界队列,防止内存溢出
- Activity销毁时,记得取消正在执行的任务
最后说一句,线程池不是银弹。如果你的任务很简单,用Handler+Thread就够了。但如果你需要管理多个并发任务、控制并发数、获取返回值——那ThreadPoolExecutor+Callable就是你的不二之选。