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

这三兄弟缺一不可。我画个图帮你理解:

Handler 机制核心流程 子线程 执行耗时任务 sendMessage() MessageQueue 消息队列(FIFO) next() Looper 不断循环取消息 Handler handleMessage() 分发到主线程 主线程 更新UI 流程:子线程 → 发送消息 → MessageQueue排队 → Looper循环取出 → Handler分发 → 主线程处理

你看,流程其实很清晰:

  • 子线程:干活的地方,比如下载图片
  • 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