15. Handler与多线程:从原理到协程的进阶之路
多线程,是Android开发里绕不开的一道坎。我记得刚入行那会儿,最怕的就是ANR(应用无响应)。说白了,就是你在主线程里干了太多活,系统看不下去了。今天我们就来聊聊Android里那些处理多线程的“兵器”,从最经典的Handler,到现在的Kotlin协程。
15.1 Handler机制:Android的“消息快递员”
Handler是什么?我习惯把它理解成一个“消息快递员”。你在子线程里做完耗时操作,比如网络请求、数据库读写,然后通过Handler把结果“快递”到主线程,更新UI。
为什么不能直接在子线程更新UI?因为Android的UI控件不是线程安全的。如果多个线程同时操作一个TextView,画面就乱了。所以Android规定:只有创建View的那条线程(通常是主线程)才能修改它。
15.1.1 核心三件套:Handler、MessageQueue、Looper
这三兄弟缺一不可。我画个图帮你理解:
你看,流程其实很清晰:
- 子线程:干活的地方,比如下载图片
- MessageQueue:消息队列,先进先出,排队等着被处理
- Looper:一个死循环,不断从MessageQueue里取消息
- Handler:负责发送消息和处理消息
核心要点:主线程默认自带Looper,子线程需要手动调用Looper.prepare()和Looper.loop()才能使用Handler。
15.1.2 实战:子线程发消息到主线程
来个最简单的例子。假设你在子线程里下载了一张图片,想显示到ImageView上:
// 主线程创建Handler
Handler mainHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
// 这里已经在主线程了
Bitmap bitmap = (Bitmap) msg.obj;
imageView.setImageBitmap(bitmap);
}
};
// 子线程里干活
new Thread(() -> {
Bitmap bitmap = downloadImage();
Message msg = Message.obtain();
msg.obj = bitmap;
mainHandler.sendMessage(msg);
}).start();
嗯,这里要注意:Message.obtain()比直接new Message()好,因为它会复用对象,减少内存分配。我在项目里见过有人疯狂new Message导致内存抖动,后来全改成obtain了。
15.2 AsyncTask:曾经的“宠儿”,如今的“弃子”
AsyncTask,说白了就是Handler的封装版。它让你在后台做耗时操作,然后自动把结果抛回主线程。我刚学Android那会儿,几乎每个网络请求都用它。
但为什么废弃了?我总结几个坑:
- 生命周期问题:Activity销毁了,AsyncTask还在跑,结果更新UI时直接崩溃
- 串行执行:Android 3.0以后默认串行,多个任务会排队,不是你以为的并行
- 内存泄漏:内部类持有Activity引用,Activity销毁不了
警告:新项目不要再使用AsyncTask。如果你还在维护老项目,尽快迁移到其他方案。
15.3 ThreadPoolExecutor:真正的“线程管理大师”
AsyncTask不行了,那用什么?我个人强烈推荐ThreadPoolExecutor。它就像一个“线程管家”,帮你管理线程的创建、复用、销毁。
15.3.1 核心参数
| 参数 | 含义 | 我的建议 |
|---|---|---|
| corePoolSize | 核心线程数 | CPU密集型设为CPU核心数+1,IO密集型可以设大些 |
| maximumPoolSize | 最大线程数 | 一般不超过corePoolSize的2倍 |
| keepAliveTime | 非核心线程空闲存活时间 | 默认60秒,看场景调整 |
| workQueue | 任务队列 | LinkedBlockingQueue比较常用 |
15.3.2 实战:自定义线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, // 核心线程数
8, // 最大线程数
60L, // 空闲存活时间
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(128), // 任务队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
// 使用
executor.execute(() -> {
// 你的耗时操作
String result = doHeavyWork();
runOnUiThread(() -> textView.setText(result));
});
小技巧:拒绝策略我常用CallerRunsPolicy,意思是线程池满了,就让提交任务的线程自己跑。这样不会丢任务,但要注意别阻塞主线程。
15.4 Kotlin协程:新时代的“异步王者”
如果你现在开始学Android,我建议直接上协程。它比线程池更轻量,写法更简洁。说白了,协程就是“用户态的线程”,由编译器帮你管理切换,而不是操作系统。
15.4.1 入门:GlobalScope.launch
// 在协程里做网络请求
GlobalScope.launch(Dispatchers.IO) {
val data = fetchDataFromNetwork()
withContext(Dispatchers.Main) {
textView.text = data
}
}
你看,代码是不是很清爽?Dispatchers.IO表示在IO线程池执行,Dispatchers.Main切回主线程。不需要Handler,不需要回调。
15.4.2 避坑:不要滥用GlobalScope
我曾经在项目里看到有人到处用GlobalScope.launch,结果Activity销毁了协程还在跑,导致内存泄漏。正确的做法是用viewModelScope或者lifecycleScope:
// 在ViewModel里
viewModelScope.launch {
val data = repository.fetchData()
_uiState.value = data
}
// 在Activity/Fragment里
lifecycleScope.launch {
// 当Lifecycle销毁时,协程自动取消
}
记住:协程的生命周期一定要和组件绑定。ViewModel销毁,协程就取消。这是防止内存泄漏的关键。
15.5 总结:我该怎么选?
你想想看,现在有这么多方案,到底用哪个?我个人的经验是这样的:
- 简单场景:比如延迟执行、子线程发消息,用Handler就够了
- 复杂并发:多个任务需要控制并发数,用ThreadPoolExecutor
- 新项目:直接上Kotlin协程,没有理由不用
- 老项目维护:如果还在用AsyncTask,尽快迁移到协程或线程池
嗯,多线程这块内容确实不少,但核心思想就一个:别在主线程干重活,别在子线程更新UI。记住这个原则,再配合合适的工具,你就能写出流畅不卡顿的App。
公众号:蓝海资料掘金营,微信deep3321