Service Handler 组合
Service + HandlerThread 的关键不是“把 Service 变成后台线程”,而是把生命周期入口和后台 工作队列分成两个 owner:Service 的 onCreate、onStartCommand、onDestroy 仍由系统安排, HandlerThread 的 Looper 只消费显式投递的工作。Android 17 的 TextToSpeechService 提供了 一个真实 framework 例子:SynthThread 拥有 worker Looper,SynthHandler 串行处理语音项, 通过 caller identity 实现 flush/stop,并在销毁时先 quit、再停止当前项。
本文面向已经读过 IntentService 请求生命周期、 HandlerThread 生命周期、HandlerExecutor 提交语义 和 Service 生命周期 的读者。本文不提供一个 脱离系统服务契约的“通用模板”,也不声称所有 Service 都应创建 HandlerThread;只沿 TextToSpeechService.onCreate()、SynthHandler.enqueueSpeechItem()、flush/stop 和 onDestroy() 的真实调用链说明所有权、串行性和清理顺序。
读完后,读者应能解释 Service 主线程如何把请求转到 worker,判断 sendMessage() 成功后工作 是否已完成,区分队列中的 speech item 与当前正在播放的 item,并理解 quit()、stop()、 finishInput 等清理动作的先后关系。
1. 生命周期线程
源码文件:frameworks/base/core/java/android/app/Service.java
Service 的类注释明确说明,系统会在 Service 首次创建时调用 onCreate(),启动请求再调用 onStartCommand();Service 同时被 start 和 bind 时,只要任一条件仍成立就不会销毁。
public void onCreate() {
}
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY_COMPATIBILITY;
}
public void onDestroy() {
}这些生命周期方法不是 HandlerThread 的 Looper.loop()。Service 若要后台串行工作,必须在 onCreate() 自己创建 worker,再把生命周期入口拿到的参数转换成 Message/Runnable;系统不会 自动把 onStartCommand() 切换到 worker。
2. Worker 建立
源码文件:frameworks/base/core/java/android/speech/tts/TextToSpeechService.java
TTS Service 的 onCreate() 创建并启动 SynthThread,再用该线程的 Looper 构造 SynthHandler。音频播放另有 AudioPlaybackHandler,所以语音合成和音频播放不是同一个 HandlerThread。
SynthThread synthThread = new SynthThread();
synthThread.start();
mSynthHandler = new SynthHandler(synthThread.getLooper());
mAudioPlaybackHandler = new AudioPlaybackHandler();
mAudioPlaybackHandler.start();SynthThread 继承 HandlerThread,其 run() 创建 Looper、发布 mLooper、调用 onLooperPrepared(),然后进入循环。mSynthHandler 的线程归属来自 synthThread.getLooper(),不是来自 Binder 调用方。
3. 空闲观察
源码文件:frameworks/base/core/java/android/speech/tts/TextToSpeechService.java
SynthThread.onLooperPrepared() 把线程自身注册为 IdleHandler。第一次空闲只翻转 mFirstIdle,后续空闲才广播 TTS 队列完成;返回 true 保留这个观察者。
private class SynthThread extends HandlerThread implements MessageQueue.IdleHandler {
private boolean mFirstIdle = true;
@Override
protected void onLooperPrepared() {
getLooper().getQueue().addIdleHandler(this);
}
@Override
public boolean queueIdle() {
if (mFirstIdle) {
mFirstIdle = false;
} else {
broadcastTtsQueueProcessingCompleted();
}
return true;
}
}这里的“处理完成”只来自 worker MessageQueue 的空闲窗口,不是 Service 生命周期完成,也不 表示音频播放线程已经没有工作。一个 Service 若要提供强完成语义,必须额外定义任务状态。
4. 请求封装
源码文件:frameworks/base/core/java/android/speech/tts/TextToSpeechService.java
enqueueSpeechItem() 由 Service 的 Binder 线程调用。它先验证 SpeechItem,根据 queueMode 执行 flush/destroy,再创建 Runnable 包装真正的 speechItem.play(),最后用 Message.obtain(this, runnable) 把 Runnable 交给 SynthHandler。
public int enqueueSpeechItem(int queueMode, final SpeechItem speechItem) {
UtteranceProgressDispatcher progress =
speechItem instanceof UtteranceProgressDispatcher
? (UtteranceProgressDispatcher) speechItem : null;
if (!speechItem.isValid()) {
if (progress != null) {
progress.dispatchOnError(TextToSpeech.ERROR_INVALID_REQUEST);
}
return TextToSpeech.ERROR;
}
if (queueMode == TextToSpeech.QUEUE_FLUSH) {
stopForApp(speechItem.getCallerIdentity());
} else if (queueMode == TextToSpeech.QUEUE_DESTROY) {
stopAll();
}
Runnable runnable = () -> {
if (setCurrentSpeechItem(speechItem)) {
speechItem.play();
removeCurrentSpeechItem();
} else {
speechItem.stop();
}
};
Message msg = Message.obtain(this, runnable);
msg.obj = speechItem.getCallerIdentity();
return sendMessage(msg) ? TextToSpeech.SUCCESS : TextToSpeech.ERROR;
}请求的 owner 在这里分成三层:Binder 调用方拥有提交时机;SynthHandler 拥有排队顺序; mCurrentSpeechItem 拥有当前正在播放的 item。Message.obj 的 caller identity 不是播放 内容,而是后续按应用 flush 的匹配键。
5. 串行消费
源码文件:frameworks/base/core/java/android/speech/tts/TextToSpeechService.java
SynthHandler 绑定一个 Looper,因此多个 sendMessage() 进入同一队列并逐个执行。Runnable 开始时调用 setCurrentSpeechItem(),播放结束后清除当前项;如果 item 在排队期间已被 flush, 设置当前项会失败,Runnable 转而调用 speechItem.stop()。
private class SynthHandler extends Handler {
private SpeechItem mCurrentSpeechItem;
private synchronized boolean setCurrentSpeechItem(SpeechItem item) {
if (mCurrentSpeechItem != null) {
return false;
}
mCurrentSpeechItem = item;
return true;
}
private synchronized SpeechItem removeCurrentSpeechItem() {
SpeechItem current = mCurrentSpeechItem;
mCurrentSpeechItem = null;
return current;
}
}串行并不等价于“每个请求一定播放”:请求可能在队列中被 flush,worker 也可能在消息取出前 退出。真正的播放结果由 setCurrentSpeechItem() 和 item 的 stop/play 协议决定。
6. 按应用清理
源码文件:frameworks/base/core/java/android/speech/tts/TextToSpeechService.java
stopForApp(callerIdentity) 先把该 caller 加入 flush 列表,再检查当前播放项是否属于同一个 caller,必要时停止当前项,并移除队列中 Message.obj == callerIdentity 的待处理消息。
public int stopForApp(final Object callerIdentity) {
if (callerIdentity == null) {
return TextToSpeech.ERROR;
}
startFlushingSpeechItems(callerIdentity);
SpeechItem current = maybeRemoveCurrentSpeechItem(callerIdentity);
if (current != null) {
current.stop();
}
removeCallbacksAndMessages(callerIdentity);
finishFlushingSpeechItems(callerIdentity);
return TextToSpeech.SUCCESS;
}这里不能只调用 removeCallbacksAndMessages():已被 Looper 取出并正在播放的 item 已不在 MessageQueue 中,必须通过 mCurrentSpeechItem 和 flush 状态协作停止。源码注释还解释了为何 flush 列表使用 List 而不是 Set:多个 QUEUE_FLUSH/QUEUE_DESTROY 请求需要按次数维持 flush 窗口。
7. 全量清理
源码文件:frameworks/base/core/java/android/speech/tts/TextToSpeechService.java
QUEUE_DESTROY 调用 stopAll(),目标是停止当前项并移除所有待处理 speech item。它与按 caller 清理不同,不以 Message.obj 过滤。
public void stop() {
getLooper().quit();
SpeechItem current = removeCurrentSpeechItem();
if (current != null) {
current.stop();
}
}SynthHandler.quit() 先让 Looper 不再处理后续消息,再停止当前 speech item。由于这是立即 退出语义,尚未取出的队列消息不会继续执行;调用方若需要安全完成,应在更高层显式等待并 设计持久化/恢复协议。
8. Service 销毁
源码文件:frameworks/base/core/java/android/speech/tts/TextToSpeechService.java
TTS Service 的 onDestroy() 先让 SynthHandler quit,再停止音频播放线程、清理 callback, 最后返回 Service 生命周期。worker 线程和播放线程是两个独立清理对象。
@Override
public void onDestroy() {
mSynthHandler.quit();
mAudioPlaybackHandler.quit();
mCallbacks.kill();
super.onDestroy();
}这条顺序没有 join();onDestroy() 返回并不表示每个已经排队的 Runnable 都完成,只表示 Service 发出了停止和资源清理动作。若业务必须等待线程结束,需要自行定义不会阻塞主线程的 等待策略。
9. Binder 入口
源码文件:frameworks/base/core/java/android/speech/tts/TextToSpeechService.java
enqueueSpeechItem() 的注释标明调用来自 Service Binder 线程。Binder 线程只负责校验、flush 和向 SynthHandler 发消息;SpeechItem.play() 在 worker Looper 执行。
这张图只表示请求从 Binder 到 worker 的线程转移,不代表 TTS 音频播放完成或客户端 callback 已经收到结果。
10. 失败路径
源码文件:frameworks/base/core/java/android/speech/tts/TextToSpeechService.java
请求可能在三个阶段失败:SpeechItem.isValid() 失败、sendMessage() 因 worker 已退出返回 失败、或 item 已被 flush 导致 setCurrentSpeechItem() 返回 false。每个阶段的错误回调/停止 动作不同。
if (!speechItem.isValid()) {
progress.dispatchOnError(TextToSpeech.ERROR_INVALID_REQUEST);
return TextToSpeech.ERROR;
}
if (!sendMessage(msg)) {
Log.w(TAG, "SynthThread has quit");
if (progress != null) {
progress.dispatchOnError(TextToSpeech.ERROR_SERVICE);
}
return TextToSpeech.ERROR;
}如果消息已经进入 worker 队列,但 item 在处理前被 flush,Runnable 会走停止分支:
if (setCurrentSpeechItem(speechItem)) {
speechItem.play();
removeCurrentSpeechItem();
} else {
speechItem.stop();
}这不是 exactly-once:一个 item 可能被拒绝、停止或播放后异常结束。客户端如果要求重试或 幂等,必须在自己的 utterance identity 和持久化协议中实现。
11. 线程边界
源码文件:frameworks/base/core/java/android/os/HandlerThread.java
HandlerThread 的类注释提醒:它适合必须使用 Handler API 且需要一条新的 Looper 线程的场景, 否则优先考虑 Executor/ExecutorService。每个 HandlerThread 都消耗线程资源,MessageQueue 还可能产生锁竞争和优先级反转。
public class HandlerThread extends Thread {
@Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}
}TTS 之所以使用它,是因为 SynthHandler 需要 Handler 消息、IdleHandler 和串行队列;这不等于 所有 Service 的后台工作都应复制相同设计。
12. 验证输入
源码文件:frameworks/base/core/java/android/speech/tts/TextToSpeechService.java
第一组验证是线程归属:在 Binder/主线程调用 enqueueSpeechItem(),在 SpeechItem.play() 记录 Thread.currentThread(),断言它等于 SynthThread 的 Looper 线程;同时验证多个请求按 Message 入队顺序串行进入 setCurrentSpeechItem()。
第二组验证是按 caller flush:先排入 caller A、caller B 两个 item,再调用 stopForApp(A),断言 A 的 pending Message 被移除、A 的 current item 被 stop,而 B 仍可处理。 这个断言需要同时检查 Message.obj 和 mCurrentSpeechItem,只看队列长度不够。
第三组验证是销毁:调用 onDestroy() 后,再提交一个 item,断言 sendMessage() 返回失败并 得到 ERROR_SERVICE;已取出的 current item 则必须观察到 stop() 调用。该验证不证明音频 播放线程已经完成 shutdown。
13. 复查命令
源码文件:
frameworks/base/core/java/android/speech/tts/TextToSpeechService.javaframeworks/base/core/java/android/app/Service.javaframeworks/base/core/java/android/os/HandlerThread.javablog/android/03-handler-looper/03-advanced-message/HL027-IntentService.md
rg -n "class SynthThread|class SynthHandler|onLooperPrepared|enqueueSpeechItem|stopForApp|stopAll|quit\(" \
frameworks/base/core/java/android/speech/tts/TextToSpeechService.java
rg -n "Message.obtain\(this|sendMessage\(|removeCallbacksAndMessages|mCurrentSpeechItem" \
frameworks/base/core/java/android/speech/tts/TextToSpeechService.java
rg -n "onCreate|onStartCommand|onDestroy|stopSelf" \
frameworks/base/core/java/android/app/Service.java阅读 Service + HandlerThread 时,应明确主线程生命周期、Binder 提交线程、worker Looper、 Message payload、current item 和销毁 owner。只写“onStartCommand 发给后台线程”无法判断 flush、停止和异常时到底谁拥有任务。
14. 适用边界
ServiceHandler 组合适合一个 Service 需要 Handler API、单线程串行工作和显式队列控制的场景。 它不自动提供并行、结果 Future、进程死亡恢复、幂等或安全退出等待。Service 的主线程生命周期 仍由系统持有,worker 线程必须在 onDestroy() 或等价 owner 路径中主动停止。
当任务涉及长时间 I/O、大量并行工作或结构化取消时,应重新评估 Executor/ExecutorService 等模型;当继续使用 HandlerThread 时,至少要能从源码回答:谁启动 Looper,谁投递消息,谁 取消 pending/current 工作,谁处理 worker 退出后的新提交,以及谁定义进程被杀后的恢复。
