2、线程模型:主线程(UI线程)与工作线程的职责划分
聊线程模型前,我先问一个问题:你写UI程序时,有没有遇到过界面卡死几秒甚至十几秒?
我遇到过。印象很深的一次,在做一个图片处理工具,直接在点击按钮的回调里跑了个循环处理大图。结果界面整个冻住,用户疯狂点按钮,事件全堆积,最后App直接崩溃。说白了,就是没搞懂谁该干什么活。
这一节我们就把主线程(UI线程)和工作线程的职责画清楚。记住一条铁律:UI操作永远只能在主线程执行,耗时任务永远别碰主线程。
主线程 vs 工作线程:一张表看清楚
| 对比维度 | 主线程(UI线程) | 工作线程 |
|---|---|---|
| 核心职责 | 处理用户输入、更新界面、事件循环 | 执行计算/IO/网络请求等耗时操作 |
| 可访问UI | 是 | 否(直接操作会崩溃或异常) |
| 典型耗时 | 应 < 16ms(60fps),否则掉帧 | 几毫秒到几十分钟都可以 |
| 消息机制 | 从消息队列取出任务执行 | 通过队列/回调向主线程提交结果 |
| 常见例子 | onClick、onDraw、布局计算 | 图像处理、数据库查询、文件读写 |
我习惯把主线程比作前台接待,工作线程比作后台工程师。前台只负责快速响应客户,一旦需要查数据库、算报表,就递给后台去搞,搞完再拿结果给前台。
主线程到底在干什么?
主线程本质上是一个无限循环,不断从消息队列里取任务来执行。这些任务包括:
- 事件处理(点击、触摸、按键)
- 视图布局与绘制
- 动画回调与帧同步
- 系统回调(如Activity/Fragment生命周期)
我个人习惯把主线程比喻成单车道——每次只能跑一辆车。如果一辆车(任务)跑得太慢,后面所有车都得堵着。这就是掉帧、卡顿、ANR(Application Not Responding)的根源。
我的经验:曾经优化一个列表滑动卡顿问题,发现是列表项的onBindViewHolder里做了图片缩放。缩放本身耗时20ms左右,但一帧只有16ms。解决办法是改成加载时缩放好再缓存,或者扔到工作线程去做。那一次让我深刻体会到:任何超过1ms的UI无关操作,都应该考虑移出主线程。
工作线程该怎么做?
工作线程就是用来扛大活的。比如:
- 网络请求(HttpURLConnection、OkHttp)
- 密集型计算(图像滤镜、音视频转码)
- 数据库大批量读写(Room/Realm)
- 文件解析(JSON/XML反序列化)
工作线程做完事后,不能直接甩结果给UI。它必须通过某种线程间通信方式把结果送回主线程。
// 伪代码示例:工作线程提交结果到主线程
// Android 的 Handler 机制
new Thread(new Runnable() {
@Override
public void run() {
// 耗时操作
final Bitmap result = processImage(inputPath);
// 必须切回主线程更新UI
runOnUiThread(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(result);
}
});
}
}).start();
Python里也一样,比如Tkinter或PyQt,都会提供类似的线程安全回调机制。说白了,工作线程负责生产数据,主线程负责消费数据并展示。
线程间通信的常见方式
不同平台有不同的方案,但核心思想一致:
| 平台 / 技术栈 | 通信方式 |
|---|---|
| Android | Handler + Looper / LiveData / Coroutine |
| iOS | DispatchQueue.main.async / performSelector |
| WinForms / WPF | Control.Invoke / Dispatcher.Invoke |
| Qt (C++/Python) | QMetaObject::invokeMethod / signal-slot |
| 通用 Python (Tkinter) | after() 定时调用 + queue.Queue |
我曾经踩过的坑:在iOS里用GCD把UI更新直接放到工作线程的block里,结果发现界面一直没变化。查了半天,发现工作线程的RunLoop根本不会自动刷新UI。后来加上dispatch_async(dispatch_get_main_queue(), ^{...})才解决。记住:UI更新必须在主线程的RunLoop里执行。
什么时候该用多线程?
并不是所有任务都值得开线程。我的经验法则:
- 必须开线程:网络请求、文件读写、图像处理、数据库批量操作(耗时 > 50ms)
- 可以考虑不开:简单状态检查、轻量缓存读取(耗时 < 1ms)
- 坚决不能开:UI绘制、直接修改视图属性
还有一个容易忽略的点:线程数不是越多越好。我曾经在一个项目里一口气开了50个线程去下载文件,结果CPU没跑满,内存先爆了。后来改用线程池(ThreadPoolExecutor),核心线程数设为CPU核心数+1,队列用LinkedBlockingQueue,才稳定下来。