Skip to content

Service Handler 组合

从 Service 生命周期与 TextToSpeechService 的 SynthThread/SynthHandler 追踪后台串行队列、flush、停止和线程销毁边界。

基于android-17.0.0_r1
AndroidServiceHandlerThreadTextToSpeechService源码阅读

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 时,只要任一条件仍成立就不会销毁。

java
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。

java
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 保留这个观察者。

java
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。

java
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()。

java
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 的待处理消息。

java
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 过滤。

java
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 线程和播放线程是两个独立清理对象。

java
@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。每个阶段的错误回调/停止 动作不同。

java
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 会走停止分支:

java
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 还可能产生锁竞争和优先级反转。

java
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.java
  • frameworks/base/core/java/android/app/Service.java
  • frameworks/base/core/java/android/os/HandlerThread.java
  • blog/android/03-handler-looper/03-advanced-message/HL027-IntentService.md
bash
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 退出后的新提交,以及谁定义进程被杀后的恢复。