如何创建一个具有独立 Looper 的子线程?它的典型应用场景是什么?

解读

国内面试中,这道题既考察“会不会写”,也考察“为什么用”。
“会写”指能正确给出 Looper.prepare()→Looper.loop() 的模板代码,并说明退出方式(quit()/quitSafely())。
“为什么用”要能说出“子线程需要长期存活、持续处理串行任务”的场景,比如网络长连接、蓝牙命令队列、音视频解码命令泵等,并对比与普通 Thread、线程池、HandlerThread 的差异。
如果只贴模板而说不出场景,会被追问“既然有 HandlerThread 为什么还要自己写”;如果只说 HandlerThread 而说不出原理,会被认为“只会用不会调”。
因此答案要“代码+原理+场景+边界”四位一体,体现“深度用过”。

知识点

  1. Java 层 Thread 默认没有 Looper,必须手动 Looper.prepare() 创建 MessageQueue 并绑定到当前线程。
  2. Looper.loop() 是阻塞方法,内部 for(;;) 从 MessageQueue 取消息,next() 无消息时 epoll 阻塞;有消息时 dispatch 到 Handler.target。
  3. 退出循环必须调用 Looper.quit()(直接清空)或 quitSafely()(处理完延迟消息后退出),否则线程永久阻塞,造成内存泄漏。
  4. 与 HandlerThread 关系:HandlerThread 是官方封装好的“带 Looper 的 Thread”,内部已处理 prepare/loop/quit,适合快速使用;但继承 Thread 可自定义优先级、命名、异常捕获、线程局部存储,更灵活。
  5. 典型场景特征:
    • 任务必须串行、有序、可延迟
    • 线程需长期存活而非用完即退
    • 需要与主线程或其他线程通过 Message 通信
      具体如:
      a. 蓝牙/Gatt 命令泵:协议栈要求命令顺序发送且 ACK 回来前不能发下一条;
      b. 音频解码控制线程:MediaCodec 需要单独线程按 pts 顺序喂数据;
      c. 长连接心跳线程:WebSocket 读写分离,写线程用 Looper 做心跳队列;
      d. 离线日志压缩任务队列:避免线程池并发写 SD 卡导致文件损坏。
  6. 性能注意:独立 Looper 线程常驻内存,必须管理生命周期,在 Activity/Service 销毁时同步 quit,防止 Activity 被 Message 隐式持有导致无法回收。
  7. 调试技巧:给线程命名,并在 ANR 日志、Systrace、Profiler 中快速定位;Looper 日志可打开 DEBUG 级别观察 dispatch 耗时。

答案

  1. 创建方式(模板代码,可直接手写)
public class LooperThread extends Thread {
    private Handler mHandler;
    private volatile Looper mLooper;

    @Override
    public void run() {
        Looper.prepare();          // 1. 创建 MessageQueue 并绑定到当前线程
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();           // 通知等待线程 Looper 已就绪
        }
        // 可在此设置线程优先级、异常处理器等
        android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
        Looper.loop();             // 2. 开始循环,阻塞
    }

    public Handler getHandler() {
        if (!isAlive()) return null;
        synchronized (this) {
            while (mLooper == null) {
                try { wait(); } catch (InterruptedException e) { /* ignore */ }
            }
        }
        return new Handler(mLooper);
    }

    public void quitSafely() {
        Looper looper = mLooper;
        if (looper != null) looper.quitSafely();
    }
}

使用侧:

LooperThread renderThread = new LooperThread();
renderThread.start();
Handler rh = renderThread.getHandler();
rh.sendMessage(rh.obtainMessage(MSG_RENDER, data));
// 生命周期结束时
renderThread.quitSafely();
  1. 典型应用场景(答出任意两个并解释原因即可)
  • 音视频渲染命令泵:GLSurfaceView 的渲染线程需要按帧序接收解码后的纹理,串行送入 GPU,防止帧乱序;
  • 蓝牙低功耗命令队列:Android BLE 要求写特征必须等 onCharacteristicWrite 回调才能继续,用 Looper 线程保证 FIFO;
  • 离线任务调度器:WorkManager 底层在部分 ROM 中需自定义调度线程,避免线程池并发写数据库导致冲突;
  • 车载 CAN 总线收发:车机 MCU 通信需保持 50 ms 心跳,独立线程通过 Looper 管理超时与重发。

拓展思考

  1. 为什么不用普通线程池?
    线程池适合大量并发、相互独立的任务;若任务必须严格串行且可延迟,线程池的“并发+队列”模型反而要用单线程池+阻塞队列,复杂度与 Looper 相当,但无法天然支持 Message 的 when 字段与 IdleHandler。

  2. 与 Kotlin Coroutine 的关系?
    在 Kotlin 层可用 Dispatchers.newSingleThreadContext("IoTWorker") 创建单线程协程,其底层仍依赖一个带 Looper 的线程;理解 Looper 原理有助于调试协程阻塞、线程泄漏问题。

  3. 性能边界测试思路:
    在低端机上连续 post 1w 条空消息,观察 Systrace 中 doFrame 是否抖动;若 loop() 单次 dispatch 超过 5 ms,需检查消息体内是否做 IO;另外用 adb shell ps -t | grep 包名 观察线程是否退出,验证 quitSafely 可靠性。