27、音频策略源码导读:AudioPolicyService关键类解析、策略决策核心函数分析、路由状态机源码走读

好,咱们今天来啃一块硬骨头——AudioPolicyService 的源码。说实话,这章内容我准备了很久。为什么?因为音频策略这块,光看文档是学不会的。你得一行一行读代码,跟着路由状态机走一遍,才能真正理解 Android 是怎么决定「声音从哪出」的。

我在做某个车载项目时,遇到过蓝牙电话切不回扬声器的诡异问题。查了两天,最后发现是策略决策函数里一个策略优先级判断写反了。从那以后,我就养成了定期通读 AudioPolicyService 核心代码的习惯。嗯,咱们今天就把这些关键类和方法捋一遍。

一、AudioPolicyService 关键类解析

先看看 AudioPolicyService 的类结构。说白了,它就是个策略大脑,负责管理音频输入输出设备的选择和切换。

类名 职责 我的理解
AudioPolicyService 对外服务接口,处理客户端请求 相当于前台接待,收需求但不做决策
AudioPolicyManager 核心策略引擎,决策路由 真正的幕后大脑,所有策略逻辑在这
AudioPolicyClient 与 HAL 层交互的桥梁 执行者,让硬件动起来
DeviceDescriptor 描述音频设备属性 每个设备的「身份证」
AudioPatch 表示音频路由路径 从源到宿的完整链路

我个人习惯把 AudioPolicyManager 看作整个系统的「交通指挥中心」。它手里拿着所有设备的清单,知道每个音频流该走哪条路。

核心要点:AudioPolicyService 本身不做决策,它把请求转发给 AudioPolicyManager。真正的策略逻辑全在 AudioPolicyManager 里。

二、策略决策核心函数分析

咱们直接看代码。AudioPolicyManager 里最核心的函数是 getDeviceForStrategy。这个函数决定了某个音频策略应该使用哪个设备。

// frameworks/av/services/audiopolicy/manager/AudioPolicyManager.cpp

audio_devices_t AudioPolicyManager::getDeviceForStrategy(
        audio_strategy_t strategy, 
        const audio_attributes_t *attributes,
        const sp<AudioOutputDescriptor> &outputDesc) {
    
    // 1. 检查是否有强制设备
    audio_devices_t forcedDevice = getForcedDeviceForStrategy(strategy);
    if (forcedDevice != AUDIO_DEVICE_NONE) {
        return forcedDevice;
    }
    
    // 2. 根据策略类型选择设备
    switch (strategy) {
        case STRATEGY_MEDIA:
            // 媒体策略:优先选择 A2DP 耳机,其次扬声器
            if (isA2dpAvailable()) {
                return AUDIO_DEVICE_OUT_BLUETOOTH_A2DP;
            }
            return AUDIO_DEVICE_OUT_SPEAKER;
            
        case STRATEGY_PHONE:
            // 电话策略:优先选择有线耳机,其次蓝牙 SCO
            if (isWiredHeadsetAvailable()) {
                return AUDIO_DEVICE_OUT_WIRED_HEADSET;
            }
            if (isBluetoothScoAvailable()) {
                return AUDIO_DEVICE_OUT_BLUETOOTH_SCO;
            }
            return AUDIO_DEVICE_OUT_EARPIECE;
            
        // ... 其他策略
    }
}

你看,这个函数的逻辑其实很直白。它根据不同的策略类型,按照优先级去检查设备是否可用。我曾经在调试一个通话场景时发现,明明插着有线耳机,电话声音却从听筒出来。查了半天,原来是 isWiredHeadsetAvailable() 里检测的是耳机插入状态,但那个状态在某个场景下没更新。嗯,这种坑很常见。

另一个关键函数是 checkAndSetDevice。它负责在设备切换时做校验和状态更新。

status_t AudioPolicyManager::checkAndSetDevice(
        audio_io_handle_t output,
        audio_devices_t device,
        const sp<AudioOutputDescriptor> &outputDesc) {
    
    // 检查设备是否有效
    if (!isValidDevice(device)) {
        ALOGE("Invalid device 0x%x", device);
        return BAD_VALUE;
    }
    
    // 检查设备是否被策略允许
    if (!isDeviceAllowedByStrategy(device, outputDesc->getStrategy())) {
        ALOGW("Device 0x%x not allowed for strategy %d", 
              device, outputDesc->getStrategy());
        return PERMISSION_DENIED;
    }
    
    // 更新输出描述符的设备信息
    outputDesc->setDevice(device);
    
    // 通知 HAL 层设备变更
    mpClientInterface->setOutputDevice(output, device);
    
    return OK;
}

避坑指南:我曾经在 checkAndSetDevice 里漏掉了 isDeviceAllowedByStrategy 的检查,结果导致某个定制设备在通话时被错误地路由到了扬声器。记住:策略检查不能跳过,哪怕你觉得某个设备「肯定能用」。

三、路由状态机源码走读

路由状态机是 AudioPolicyService 里最精妙的部分。它管理着音频路由的生命周期:从创建、激活、切换到销毁。

咱们先看状态定义:

enum routing_state_t {
    ROUTING_STATE_IDLE = 0,       // 空闲状态
    ROUTING_STATE_PENDING,        // 等待路由变更
    ROUTING_STATE_ACTIVE,         // 路由已激活
    ROUTING_STATE_SWITCHING,      // 正在切换路由
    ROUTING_STATE_ERROR           // 路由错误
};

状态机的核心逻辑在 AudioPolicyManager::setOutputDevice 里。我简化一下关键流程:

void AudioPolicyManager::setOutputDevice(
        audio_io_handle_t output,
        audio_devices_t device,
        bool force) {
    
    sp<AudioOutputDescriptor> outputDesc = getOutputDesc(output);
    
    // 状态机:检查当前状态
    switch (outputDesc->getRoutingState()) {
        case ROUTING_STATE_IDLE:
            // 直接设置设备
            outputDesc->setDevice(device);
            outputDesc->setRoutingState(ROUTING_STATE_ACTIVE);
            break;
            
        case ROUTING_STATE_ACTIVE:
            // 需要切换设备
            outputDesc->setRoutingState(ROUTING_STATE_SWITCHING);
            // 执行切换逻辑
            performDeviceSwitch(output, device);
            outputDesc->setRoutingState(ROUTING_STATE_ACTIVE);
            break;
            
        case ROUTING_STATE_SWITCHING:
            // 正在切换中,新请求排队
            queuePendingDeviceChange(output, device);
            break;
            
        case ROUTING_STATE_ERROR:
            // 错误状态,需要重置
            resetRoutingState(output);
            break;
    }
}

你想想看,为什么需要状态机?因为音频路由切换不是瞬间完成的。比如从扬声器切换到蓝牙耳机,中间要经过蓝牙连接建立、SCO 链路建立、音频重定向等步骤。如果在这个过程中又来一个新请求,状态机就能优雅地处理——要么排队,要么丢弃。

音频路由状态机 ROUTING_IDLE ROUTING_PENDING ROUTING_ACTIVE ROUTING_SWITCHING ROUTING_ERROR 设置设备 设备切换请求 切换完成 新请求排队 处理排队请求 设备异常 重置路由 图例: 正常流转 排队/延迟 切换路径 异常路径

这张图我画了好几次才满意。你看,从 IDLE 到 ACTIVE 是最简单的路径——直接设置设备就行。但从 ACTIVE 到 SWITCHING 再到 ACTIVE,中间就复杂了。如果切换过程中有新请求进来,就会进入 PENDING 状态排队。

注意:状态机里最容易出问题的是 SWITCHING 状态。如果切换过程中设备断开或者超时,状态机会卡在 SWITCHING 里。我遇到过蓝牙耳机在切换过程中突然断开,结果状态机一直卡在 SWITCHING,后续所有路由请求都被排队了。解决方案是在切换函数里加超时检测,超时后强制回退到 IDLE 状态。

四、路由决策的完整流程

咱们把上面讲的串起来,看看一个完整的路由决策流程是什么样的:

  1. 应用发起请求:比如播放音乐,AudioTrack 创建时指定了使用 MUSIC 流类型。
  2. AudioPolicyService 接收:收到请求后,调用 getOutputForAttr 方法。
  3. 策略匹配:AudioPolicyManager 根据音频属性匹配到 STRATEGY_MEDIA 策略。
  4. 设备选择:调用 getDeviceForStrategy,按优先级选择可用设备。
  5. 状态机检查:检查当前输出设备的状态,决定是直接设置还是切换。
  6. 执行路由:通过 AudioPolicyClient 通知 HAL 层,创建或更新 AudioPatch。
  7. 状态更新:更新状态机状态,通知监听者路由变更。

这个流程看起来简单,但每个步骤都有很多细节。比如第 4 步的设备选择,要考虑设备优先级、可用性、策略限制、音频属性匹配等多个因素。

我的经验:调试路由问题时,我建议你在 getDeviceForStrategysetOutputDevice 里加日志。打印出当前策略、可用设备列表、选择的设备、状态机状态。这样你就能清楚地看到每一步发生了什么。我曾经靠这个方法,半天就定位了一个蓝牙耳机连上后声音还在扬声器出的问题——原来是 A2DP 设备注册时漏掉了策略更新。

五、总结

AudioPolicyService 的源码读起来确实有点绕,但抓住三个核心就好:关键类(知道谁负责什么)、决策函数(知道怎么选设备)、状态机(知道怎么切换设备)。

我个人建议你按照这个顺序去读源码:先看 AudioPolicyManager::getDeviceForStrategy,再看 setOutputDevice,最后看状态机的各个状态处理。这样由浅入深,不会一开始就被状态机的各种分支搞晕。

嗯,音频策略这块内容确实不少。但只要你把今天讲的这些关键类和方法吃透了,以后遇到路由相关的问题,你就能快速定位到问题所在。毕竟,源码面前,了无秘密。


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