2、线程模型:主线程(UI线程)与工作线程的职责划分

聊线程模型前,我先问一个问题:你写UI程序时,有没有遇到过界面卡死几秒甚至十几秒?

我遇到过。印象很深的一次,在做一个图片处理工具,直接在点击按钮的回调里跑了个循环处理大图。结果界面整个冻住,用户疯狂点按钮,事件全堆积,最后App直接崩溃。说白了,就是没搞懂谁该干什么活。

这一节我们就把主线程(UI线程)和工作线程的职责画清楚。记住一条铁律:UI操作永远只能在主线程执行,耗时任务永远别碰主线程。

主线程 vs 工作线程:一张表看清楚

对比维度主线程(UI线程)工作线程
核心职责处理用户输入、更新界面、事件循环执行计算/IO/网络请求等耗时操作
可访问UI是否(直接操作会崩溃或异常)
典型耗时应 < 16ms(60fps),否则掉帧几毫秒到几十分钟都可以
消息机制从消息队列取出任务执行通过队列/回调向主线程提交结果
常见例子onClick、onDraw、布局计算图像处理、数据库查询、文件读写

我习惯把主线程比作前台接待,工作线程比作后台工程师。前台只负责快速响应客户,一旦需要查数据库、算报表,就递给后台去搞,搞完再拿结果给前台。

线程模型:UI线程与工作线程的职责划分 主线程(UI线程) 事件循环 UI更新 布局测量 触摸事件 消息队列 (FIFO 存储任务) 工作线程 后台计算 网络请求 文件读写 数据库查询 取出任务 提交结果 / 任务 ⚠ 工作线程绝不允许直接操作UI,必须通过消息队列委托给主线程 线程池 复用线程,减少开销

主线程到底在干什么?

主线程本质上是一个无限循环,不断从消息队列里取任务来执行。这些任务包括:

  • 事件处理(点击、触摸、按键)
  • 视图布局与绘制
  • 动画回调与帧同步
  • 系统回调(如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,都会提供类似的线程安全回调机制。说白了,工作线程负责生产数据,主线程负责消费数据并展示。

线程间通信的常见方式

不同平台有不同的方案,但核心思想一致:

平台 / 技术栈通信方式
AndroidHandler + Looper / LiveData / Coroutine
iOSDispatchQueue.main.async / performSelector
WinForms / WPFControl.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,才稳定下来。