5、音频流管理:PlaybackThread与RecordThread、音频流状态机、流生命周期管理、流参数配置

好,我们继续往下走。这一章要聊的是音频流管理,说白了就是音频数据在 AudioFlinger 里怎么跑、怎么被管起来的。你想想看,一个手机同时要放音乐、响铃、录语音,这些音频流怎么共存?谁先谁后?谁该被掐掉?这就是我们要解决的问题。

我个人习惯把音频流管理比作一个交通调度系统。PlaybackThread 和 RecordThread 就是两条主干道,音频流就是路上的车,状态机就是红绿灯。嗯,这样理解起来会直观很多。

5.1 PlaybackThread 与 RecordThread:两条核心线程

AudioFlinger 里最核心的两个线程,一个是 PlaybackThread,负责播放;一个是 RecordThread,负责录音。它们俩的架构其实是对称的,但干的活完全相反。

我记得刚接触 AudioFlinger 源码时,第一反应是:为什么不用一个线程搞定所有事?后来踩了坑才明白——播放和录音的实时性要求不同,混在一起会出大问题。播放可以容忍几十毫秒的延迟,但录音如果延迟大了,回声消除就废了。

5.1.1 PlaybackThread 的工作模式

PlaybackThread 其实是个抽象基类,它有好几个子类,分别应对不同的场景:

子类 用途 特点
MixerThread 混音播放 支持多路音频流混音,最常见
DirectOutputThread 直通输出 绕过混音器,低延迟,用于导航音等
DuplicatingThread 多设备输出 一份音频同时送到多个输出设备
OffloadThread 硬件卸载播放 把解码和播放交给 DSP,省电

这里有个关键点:每个音频输出设备(比如扬声器、耳机、蓝牙)都对应一个独立的 PlaybackThread。你插上耳机,系统会创建一个新的线程来管理耳机输出。拔掉耳机,这个线程就被销毁了。

核心逻辑:一个线程管一个设备,设备变了线程就重建。这是 AudioFlinger 的设计哲学。

5.1.2 RecordThread 的录音路径

RecordThread 相对简单一些,它没有那么多子类。录音路径通常是:

  1. 音频数据从麦克风硬件进来
  2. 经过 HAL 层到达 RecordThread
  3. RecordThread 把数据分发给所有注册的录音客户端

我曾经在调试一个通话录音功能时发现,如果同时有两个 App 在录音,第二个 App 会拿不到数据。查了半天,原来是 RecordThread 的 buffer 分配策略有问题——它默认只给第一个客户端分配了足够的 buffer,第二个客户端只能读到空数据。后来我改成了动态分配,问题就解决了。

小技巧:调试录音问题时,先检查 RecordThread 的 buffer 状态。用 dumpsys media.audio_flinger 可以看到每个线程的 buffer 使用情况。

5.2 音频流状态机:从生到死的状态变迁

每个音频流都有自己的生命周期,AudioFlinger 用状态机来管理它。状态机其实不复杂,就几个状态:

状态定义(在 AudioFlinger.h 中):
enum track_state {
    IDLE,       // 空闲,刚创建
    ACTIVE,     // 活跃,正在播放或录音
    STOPPED,    // 停止,但资源还在
    PAUSED,     // 暂停,可以恢复
    FLUSHED,    // 已刷新,数据清空
    TERMINATED  // 终止,即将销毁
};

状态转换的路径是这样的:

  • IDLE → ACTIVE:调用 start(),开始传输数据
  • ACTIVE → STOPPED:调用 stop(),停止传输但保留配置
  • ACTIVE → PAUSED:调用 pause(),临时停止,可以 resume() 恢复
  • STOPPED → ACTIVE:再次 start(),重新开始
  • 任何状态 → TERMINATED:调用 destroy(),彻底销毁

嗯,这里要注意一个坑:PAUSED 状态只能从 ACTIVE 进入,不能从 STOPPED 进入。我见过有开发者想当然地认为 stop 之后再 pause 也能恢复,结果代码直接崩溃了。

警告:千万不要在 STOPPED 状态下调用 resume()。正确的做法是先 start(),再 pause(),再 resume()。这个顺序搞错了,AudioFlinger 会直接返回错误码 -22(EINVAL)。

为什么会这样设计?我个人理解是:PAUSED 意味着音频流还在活跃状态,只是暂时不传输数据;而 STOPPED 意味着音频流已经退出了活跃状态,需要重新 start 才能激活。这两个语义是不同的。

5.3 流生命周期管理:创建、使用、销毁

一个音频流的完整生命周期,我把它分成三个阶段:

5.3.1 创建阶段

当 App 调用 AudioTrack 或 AudioRecord 的构造函数时,会通过 Binder 调用到 AudioFlinger 的 createTrack() 或 createRecord() 方法。这个过程大概是这样:

// 简化后的 createTrack 流程
status_t AudioFlinger::createTrack(...) {
    // 1. 找到合适的 PlaybackThread
    sp<PlaybackThread> thread = findSuitableThread(...);
    
    // 2. 在 thread 上创建 Track
    sp<Track> track = thread->createTrack_l(...);
    
    // 3. 分配 buffer
    track->allocateBuffer();
    
    // 4. 返回给客户端
    return track;
}

这里有个关键决策:findSuitableThread 怎么选线程?规则是这样的:

  • 如果 App 指定了设备,就用该设备对应的线程
  • 如果没有指定,就用默认输出设备的线程
  • 如果 App 要求低延迟,优先用 DirectOutputThread

我记得有一次,一个游戏 App 抱怨声音延迟大。查了发现它没有指定低延迟策略,结果被分配到了 MixerThread,延迟自然就大了。后来让它在创建 AudioTrack 时加上 PERFORMANCE_LOW_LATENCY 标志,问题就解决了。

5.3.2 使用阶段

创建完成后,音频流就进入了使用阶段。这个阶段主要做三件事:

  1. 数据写入/读取:App 通过 write() 或 read() 方法传输数据
  2. 状态管理:start/stop/pause 等操作
  3. 参数调整:音量、声道、采样率等

使用阶段最核心的是数据流。对于 PlaybackThread,它会周期性地从所有活跃的 Track 中读取数据,混音后写入 HAL。对于 RecordThread,它会从 HAL 读取数据,分发给所有活跃的 RecordTrack。

5.3.3 销毁阶段

当 App 不再需要音频流时,会调用 release() 或析构函数。AudioFlinger 会做以下清理:

  • 从线程的 Track 列表中移除
  • 释放分配的 buffer
  • 如果这是最后一个 Track,线程可能会进入 idle 状态

这里有个容易忽略的点:如果 App 进程异常退出,AudioFlinger 怎么清理?答案是靠 Binder 的死亡通知。每个 Track 在创建时都会注册一个死亡通知回调,当客户端进程挂了,AudioFlinger 会自动清理该进程创建的所有 Track。

避坑指南:我曾经遇到过一个 bug,App 闪退后音频设备一直被占用。查了发现是死亡通知回调里有个死锁——清理 Track 时需要拿线程锁,但死亡通知本身也在一个锁的上下文中。后来我把清理操作放到了工作线程里异步执行,才解决了这个问题。

5.4 流参数配置:音量、声道、采样率

音频流的参数配置,说白了就是告诉 AudioFlinger 你想怎么播放或录音。这些参数在创建 Track 时指定,也可以在运行中动态修改。

5.4.1 核心参数一览

参数 说明 动态修改
采样率 每秒采样点数,如 44100、48000 部分支持
声道数 单声道、立体声、5.1 等 不支持
格式 PCM 16bit、PCM 24bit、Float 等 不支持
音量 0.0 ~ 1.0 浮点数 支持
路由 输出到哪个设备 支持

你可能会问:为什么声道数和格式不支持动态修改?嗯,这其实是个设计取舍。声道数和格式决定了 buffer 的大小和布局,如果允许动态修改,buffer 管理会变得极其复杂。Android 团队选择了简单可靠的方式——创建时定死,想改就重建 Track。

5.4.2 音量配置的细节

音量控制是参数配置里最常用的。AudioFlinger 支持两种音量控制方式:

  • App 端音量:每个 Track 有自己的音量,由 App 控制
  • 系统端音量:由 AudioService 管理,对应不同的音频流类型(音乐、铃声、通话等)

最终输出到硬件的音量 = App 端音量 × 系统端音量。这个乘法是在混音器里做的。

我记得有一次,一个 App 把音量设成了 0.5,但用户觉得声音太小。查了发现系统音量也设成了 0.5,最终音量只有 0.25。用户把系统音量调到最大也没用,因为 App 端限制了。这就是典型的「双重音量控制」问题。

最佳实践:App 端音量建议只用来做静音/非静音切换,不要做精细调节。精细调节交给系统音量,这样用户体验更一致。

5.4.3 路由配置

路由配置决定了音频数据最终从哪个设备出去。这个配置通常由 AudioService 根据策略自动决定,但 App 也可以通过 setPreferredDevice() 来指定。

路由变更时,AudioFlinger 会做一件重要的事:重建 PlaybackThread。因为不同的设备对应不同的 HAL 接口,不能简单地在同一个线程里切换。重建过程中,所有活跃的 Track 会被迁移到新的线程上。

这个迁移过程我踩过坑。有一次,蓝牙耳机断开时,音乐播放卡顿了一下。查了发现是 Track 迁移时 buffer 没有正确同步,导致数据丢失。后来我在迁移逻辑里加了一个 buffer 重填的步骤,问题就解决了。

5.5 核心流程图:音频流生命周期

下面我用一张 SVG 图来总结音频流的完整生命周期,从创建到销毁,以及状态机的转换关系。

音频流生命周期与状态机 创建 Track IDLE ACTIVE PAUSED STOPPED TERMINATED start() pause() resume() stop() start() destroy() 图例 start/resume stop pause destroy

这张图把音频流的整个生命周期串起来了。从创建开始,经过 IDLE、ACTIVE、PAUSED、STOPPED 这几个状态,最终到达 TERMINATED。你注意看,从任何状态都可以直接跳到 TERMINATED,这就是销毁操作——简单粗暴,但有效。

好了,这一章的内容就到这里。音频流管理是 AudioFlinger 的核心,理解了它,你就掌握了音频框架的骨架。下一章我们会深入混音器,看看多路音频是怎么合成一路的。


公众号:蓝海资料掘金营,微信deep3321