如何创建一个具有独立 Looper 的子线程?它的典型应用场景是什么?
解读
国内面试中,这道题既考察“会不会写”,也考察“为什么用”。
“会写”指能正确给出 Looper.prepare()→Looper.loop() 的模板代码,并说明退出方式(quit()/quitSafely())。
“为什么用”要能说出“子线程需要长期存活、持续处理串行任务”的场景,比如网络长连接、蓝牙命令队列、音视频解码命令泵等,并对比与普通 Thread、线程池、HandlerThread 的差异。
如果只贴模板而说不出场景,会被追问“既然有 HandlerThread 为什么还要自己写”;如果只说 HandlerThread 而说不出原理,会被认为“只会用不会调”。
因此答案要“代码+原理+场景+边界”四位一体,体现“深度用过”。
知识点
- Java 层 Thread 默认没有 Looper,必须手动 Looper.prepare() 创建 MessageQueue 并绑定到当前线程。
- Looper.loop() 是阻塞方法,内部 for(;;) 从 MessageQueue 取消息,next() 无消息时 epoll 阻塞;有消息时 dispatch 到 Handler.target。
- 退出循环必须调用 Looper.quit()(直接清空)或 quitSafely()(处理完延迟消息后退出),否则线程永久阻塞,造成内存泄漏。
- 与 HandlerThread 关系:HandlerThread 是官方封装好的“带 Looper 的 Thread”,内部已处理 prepare/loop/quit,适合快速使用;但继承 Thread 可自定义优先级、命名、异常捕获、线程局部存储,更灵活。
- 典型场景特征:
- 任务必须串行、有序、可延迟
- 线程需长期存活而非用完即退
- 需要与主线程或其他线程通过 Message 通信
具体如:
a. 蓝牙/Gatt 命令泵:协议栈要求命令顺序发送且 ACK 回来前不能发下一条;
b. 音频解码控制线程:MediaCodec 需要单独线程按 pts 顺序喂数据;
c. 长连接心跳线程:WebSocket 读写分离,写线程用 Looper 做心跳队列;
d. 离线日志压缩任务队列:避免线程池并发写 SD 卡导致文件损坏。
- 性能注意:独立 Looper 线程常驻内存,必须管理生命周期,在 Activity/Service 销毁时同步 quit,防止 Activity 被 Message 隐式持有导致无法回收。
- 调试技巧:给线程命名,并在 ANR 日志、Systrace、Profiler 中快速定位;Looper 日志可打开 DEBUG 级别观察 dispatch 耗时。
答案
- 创建方式(模板代码,可直接手写)
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();
- 典型应用场景(答出任意两个并解释原因即可)
- 音视频渲染命令泵:GLSurfaceView 的渲染线程需要按帧序接收解码后的纹理,串行送入 GPU,防止帧乱序;
- 蓝牙低功耗命令队列:Android BLE 要求写特征必须等 onCharacteristicWrite 回调才能继续,用 Looper 线程保证 FIFO;
- 离线任务调度器:WorkManager 底层在部分 ROM 中需自定义调度线程,避免线程池并发写数据库导致冲突;
- 车载 CAN 总线收发:车机 MCU 通信需保持 50 ms 心跳,独立线程通过 Looper 管理超时与重发。
拓展思考
-
为什么不用普通线程池?
线程池适合大量并发、相互独立的任务;若任务必须严格串行且可延迟,线程池的“并发+队列”模型反而要用单线程池+阻塞队列,复杂度与 Looper 相当,但无法天然支持 Message 的 when 字段与 IdleHandler。 -
与 Kotlin Coroutine 的关系?
在 Kotlin 层可用Dispatchers.newSingleThreadContext("IoTWorker")创建单线程协程,其底层仍依赖一个带 Looper 的线程;理解 Looper 原理有助于调试协程阻塞、线程泄漏问题。 -
性能边界测试思路:
在低端机上连续 post 1w 条空消息,观察 Systrace 中 doFrame 是否抖动;若 loop() 单次 dispatch 超过 5 ms,需检查消息体内是否做 IO;另外用adb shell ps -t | grep 包名观察线程是否退出,验证 quitSafely 可靠性。