线程调度基础:Android主线程与子线程、Handler/Looper原理、TalkBack线程模型

Android 应用的线程模型是构建稳定无障碍服务的基石。TalkBack 作为系统级无障碍服务,其事件响应、语音合成与手势反馈均依赖正确的线程调度。本章从 Android 线程模型出发,逐步深入到 Handler/Looper 机制,最后解析 TalkBack 自身的线程架构,为后续 RTL 布局下的线程优化打下理论基础。

3.1 Android 主线程与子线程

Android 应用进程启动时,系统会创建一个主线程(Main Thread),也称为 UI 线程。主线程负责三大职责:

职责说明风险
UI 渲染执行 View 的 measure、layout、draw 流程阻塞导致掉帧、ANR
事件分发处理触摸、按键、无障碍事件事件积压,响应延迟
广播 / Service 回调onCreate、onResume、onAccessibilityEvent 等超时触发 ANR (5s)

子线程(Worker Thread)用于执行耗时操作:文件 I/O、网络请求、TTS 语音合成、数据计算。子线程严禁直接更新 UI,必须通过 Handler 切换到主线程执行。

关键指标:主线程单次任务不应超过 16ms(60fps 帧率),无障碍事件处理建议控制在 8ms 以内以保证 TalkBack 跟手性。

3.2 Handler / Looper 原理

Handler、Looper、MessageQueue 三者构成了 Android 的线程消息驱动模型:

  • Looper:为线程创建消息循环,通过 loop() 不断从 MessageQueue 取出消息并分发。
  • MessageQueue:消息队列,以链表结构存储 Message,支持延时和同步屏障。
  • Handler:发送 Message 到 MessageQueue,并绑定目标 Looper;在 handleMessage() 中执行回调。

主线程的 Looper 由 ActivityThread 自动创建。子线程需手动调用 Looper.prepare() 和 Looper.loop():

// 子线程手动创建 Looper
class TalkBackWorkerThread : Thread() {
    private var handler: Handler? = null

    override fun run() {
        // 1. 初始化 Looper 和 MessageQueue
        Looper.prepare()

        // 2. 创建 Handler 并关联当前线程 Looper
        handler = object : Handler(Looper.myLooper()!!) {
            override fun handleMessage(msg: Message) {
                when (msg.what) {
                    MSG_TTS_SPEAK -> synthesizeAndSpeak(msg.obj as String)
                    MSG_FETCH_NODE -> fetchAccessibilityNodeInfo(msg.arg1)
                }
            }
        }

        // 3. 启动消息循环(阻塞式)
        Looper.loop()
    }

    fun postTts(text: String) {
        handler?.obtainMessage(MSG_TTS_SPEAK, text)?.sendToTarget()
    }
}
常见错误:子线程中忘记调用 Looper.prepare() 直接 new Handler() 会抛出 Can't create handler inside thread that has not called Looper.prepare() 异常。TalkBack 子线程初始化时务必检查 Looper 状态。

3.3 TalkBack 线程模型

TalkBack 作为 AccessibilityService,运行在 com.android.talkback 进程。其线程架构分为三层:

线程层角色对应 Looper典型任务
主线程事件分发 & UI 反馈系统 Main LooperonAccessibilityEvent、手势识别、焦点导航
TTS 线程语音合成 & 播放独立子线程 LooperTextToSpeech.speak、音频文件生成
后台 Worker 线程节点解析 & 缓存线程池(无 Looper)节点树遍历、RTL 布局转换计算

TalkBack 主线程处理 onAccessibilityEvent() 回调时,必须快速生成反馈策略(朗读内容、执行动作)。任何超过 200ms 的处理都会导致用户感知到“语音滞后于焦点”。因此,耗时操作(如节点文本解析、TTS 合成)必须委托给子线程。

实践要点:TalkBack 的 TTS 线程使用 HandlerThread 封装,内部自带 Looper。建议在 onServiceConnected() 中初始化,并在 onDestroy() 中调用 quitSafely() 避免内存泄漏。

3.4 TalkBack 线程调度架构图

下图展示了 TalkBack 事件从主线程分发到子线程处理,再通过 Handler 回传结果至主线程的完整调度链路:

TalkBack 线程调度架构图 AccessibilityEvent 系统 → TalkBack 主线程 (Main Looper) onAccessibilityEvent 手势识别 / 焦点导航 dispatch TTS 子线程 HandlerThread + Looper TextToSpeech.speak() obtainMessage(TTS_SPEAK) 后台 Worker 线程池 节点树解析 / RTL 转换 缓存更新 / 配置加载 execute(Runnable) 反馈聚合器 主线程 Handler 回调 更新 TalkBack 反馈层 handleMessage post 解析结果 用户反馈输出 语音 / 振动 / 字幕 说明: 虚线箭头表示异步任务分发,实线箭头表示消息投递。所有用户反馈最终必须在主线程上更新,子线程仅执行计算和 I/O。

3.5 调试与验证

使用以下方法验证 TalkBack 线程调度是否符合预期:

  1. StrictMode 策略:在主线程检测磁盘读/写和网络调用,捕获 TalkBack 中非预期的阻塞操作。
// Application.onCreate() 中启用 StrictMode
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
    .detectDiskReads()
    .detectDiskWrites()
    .detectNetwork()
    .penaltyLog()
    .build())
  1. Systrace / Perfetto 抓取:标记 TalkBack 主线程和子线程的 slice,分析事件处理耗时。
# 抓取 10 秒 TalkBack 进程 trace
adb shell perfetto -o /data/local/tmp/talkback_trace.perfetto \
    -t 10s \
    -b 64mb \
    sched freq idle am wm gfx view hal app

# 拉取到本地分析
adb pull /data/local/tmp/talkback_trace.perfetto .
  1. 线程名检查:TalkBack 子线程应具有可识别的名称,便于定位。
// 设置线程名便于调试
val ttsThread = HandlerThread("TalkBack-TTS-Thread")
ttsThread.start()

val workerExecutor = Executors.newFixedThreadPool(4) { runnable ->
    Thread(runnable, "TalkBack-Worker-${counter.incrementAndGet()}")
}

3.6 常见错误与稳定性优化

错误模式现象根因解决方案
主线程 TTS 合成TalkBack 语音卡顿、ANR直接在主线程调用 TextToSpeech.speak() 同步合成使用 HandlerThread 投递 TTS 任务
Looper 泄漏子线程无法退出,内存上涨Service 销毁后未调用 quitSafely()onDestroy() 中退出子线程 Looper
跨线程节点访问IllegalStateException子线程直接操作 AccessibilityNodeInfo节点信息序列化后传递给子线程
消息积压反馈延迟 > 500ms子线程任务队列过长,处理不及时设置优先级、丢弃过期消息
稳定性优化 Checklist:
✅ 主线程 Handler 处理反馈消息时执行耗时 < 5ms
✅ TTS 线程队列最大长度限制 128,超限则丢弃最旧消息
✅ 后台 Worker 使用线程池,核心线程数 = CPU 核数 + 1
✅ 所有跨线程数据传递使用不可变对象或深度拷贝
✅ 在 onAccessibilityEvent() 中启用 Trace.beginAsyncSection() 标记

3.7 与 RTL 布局的关联

当 TalkBack 在 RTL(Right-to-Left)布局下导航时,线程调度面临额外挑战:

  • RTL 节点树的遍历方向与 LTR 相反,解析逻辑需在 Worker 线程中重算。
  • RTL 布局下 getBoundsInScreen() 返回的坐标需镜像转换,该计算应异步完成。
  • 主线程仅保留焦点索引和反馈策略,具体的坐标映射和文本顺序重组交给子线程。