28、音频策略扩展接口:自定义策略插件机制、动态策略更新API、厂商策略扩展实践

音频策略这东西,说白了就是系统怎么决定“声音从哪出”。

但现实很骨感。每个厂商的硬件不一样,每个产品的场景也不一样。Google 给的那套默认策略,很多时候只是个“能跑”的起点。我这些年做过的项目里,几乎没有哪一个是直接用默认策略就能交付的。嗯,今天我们就聊聊怎么把策略这块“玩出花来”。

为什么需要策略扩展?

你想想看,一个手机厂商,可能同时做平板、电视、手表。每个设备的音频拓扑都不一样。有的用 SmartPA,有的用 Codec 直出,有的还有独立的 DSP。如果策略写死了,那换一个产品就得改框架层代码——这谁受得了?

所以,Android 从某个版本开始,就留了口子。允许你通过插件机制、动态 API 来定制策略。我个人的习惯是,先把默认策略跑一遍,看看哪些地方“不对劲”,然后再决定要不要上扩展。

核心思路: 策略框架提供“钩子”,厂商实现“逻辑”。框架不关心你怎么决策,只关心你返回什么结果。

自定义策略插件机制

插件机制,说白了就是让厂商能写自己的策略模块,然后塞进音频服务里。Android 的 AudioPolicy 本身是一个抽象类,厂商可以继承它,重写关键方法。

我记得第一次接触这个机制时,是在一个车载项目上。车机有十几个扬声器,还有独立的功放通道。默认的 AudioPolicy 根本处理不了这种拓扑。我们当时就是写了一个自定义的 AudioPolicy 插件,直接接管了路由决策。

插件机制的典型实现方式:

  • 继承 AudioPolicy 基类
  • 实现 getOutputForAttrgetInputForAttr 等核心方法
  • 通过 AudioPolicyConfig 加载自定义配置
  • 注册到 AudioFlinger 中
// 伪代码示例:自定义策略插件
class CustomAudioPolicy : public AudioPolicy {
public:
    audio_io_handle_t getOutputForAttr(
        const audio_attributes_t *attr,
        audio_io_handle_t output,
        audio_session_t session,
        audio_stream_type_t stream,
        uid_t uid
    ) override {
        // 这里你可以写自己的路由逻辑
        // 比如:根据 uid 判断应用,或者根据设备状态选择输出
        if (isCarMode()) {
            return getCarSpeakerOutput();
        }
        return AudioPolicy::getOutputForAttr(attr, output, session, stream, uid);
    }
};
避坑指南: 我曾经在插件里直接返回了一个不存在的 output handle,结果整个音频服务直接挂掉。所以,一定要确保你返回的 handle 是已经注册过的。建议在插件初始化时,先遍历所有可用设备,建立映射表。

动态策略更新 API

有些场景下,策略需要运行时改变。比如:插入耳机时,策略要切换路由;开启游戏模式时,策略要调整延迟。这些如果靠重启服务来实现,用户体验就太差了。

Android 提供了动态策略更新 API,允许你在不重启音频服务的情况下,修改策略行为。

常用的动态 API 包括:

  • AudioPolicyManager::setDeviceConnectionState:动态通知设备连接状态变化
  • AudioPolicyManager::setForceUse:强制指定某种场景下的使用策略
  • AudioPolicyManager::setParameters:传递自定义参数给策略层

我建议你在设计动态策略时,一定要考虑“回退机制”。比如,你强制把媒体声音路由到蓝牙音箱,但如果蓝牙突然断开,策略要能自动回退到扬声器。否则,用户会以为手机坏了。

// 动态更新示例:强制媒体输出到蓝牙
AudioSystem::setForceUse(
    FORCE_MEDIA,
    FORCE_BLUETOOTH_A2DP
);
注意: 动态更新 API 是线程安全的,但回调处理要小心。我遇到过一个问题:在设备连接回调里直接调用 setForceUse,导致死锁。后来改成 post 到主线程处理,才解决。

厂商策略扩展实践

厂商扩展策略,通常有几种模式:

  1. 配置驱动:通过 XML 或 JSON 配置文件,定义路由规则。框架解析配置,动态生成策略。
  2. 代码注入:直接修改 AudioPolicy 源码,加入厂商特有的逻辑。
  3. 混合模式:配置 + 代码,配置决定大部分行为,代码处理特殊场景。

我个人比较推荐配置驱动的方式。为什么呢?因为代码注入虽然灵活,但每次 Android 版本升级,你都得重新适配。配置驱动的话,只要接口不变,配置可以复用。我在一个项目里,用 XML 定义了 200 多条路由规则,覆盖了所有可能的设备组合。后来升级到 Android 13,只改了 3 行代码就适配完了。

下面是一个典型的厂商策略扩展流程图:

厂商策略扩展流程图 应用请求音频 AudioPolicy 决策(默认 + 插件) 加载厂商策略配置(XML/JSON) 动态策略更新 API 介入 最终路由决策 插件机制: 继承 AudioPolicy 重写路由方法 动态 API: setForceUse setParameters 配置驱动: XML 规则定义 运行时解析

实战中的几个关键点

说了这么多理论,来点实际的。我在几个项目里总结了一些经验:

场景 推荐方案 注意事项
多扬声器设备 自定义插件 + 配置驱动 注意扬声器分组和功率限制
车载多音区 代码注入 + 动态 API 不同音区之间要隔离,避免串扰
游戏低延迟模式 动态 API + 参数传递 切换时要做淡入淡出,避免爆音
会议场景 配置驱动 + 插件 回声消除和麦克风阵列要联动
我的习惯: 每次写策略扩展前,先画一张“设备-场景-路由”的矩阵图。把所有的输入输出列出来,然后看哪些是默认策略能覆盖的,哪些需要自定义。这样能避免后期反复改代码。

总结一下

音频策略扩展,本质上就是给框架装上一个“可编程的大脑”。插件机制让你能写自己的决策逻辑,动态 API 让你能在运行时调整行为,配置驱动则让策略变得可维护。

嗯,最后说一句:不要为了扩展而扩展。如果默认策略能满足需求,就别动它。我见过太多项目,明明一个 setForceUse 就能解决的事,非要写一个几百行的插件——结果 bug 比功能还多。

核心原则: 简单 > 灵活 > 复杂。能用配置解决的,不要写代码;能用 API 解决的,不要改框架。

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