Android 的 AudioManager 音频焦点竞争,多应用同时播放的处理
「Android 的 AudioManager 音频焦点竞争,多应用同时播放的处理」
开车导航时最怕什么?不是路线绕远,是扬声器里的混战。高德地图刚喊完“前方三百米掉头”,抖音突然开始“注意看这个男人叫小帅”,你手忙脚乱切回后台的网易云想暂停,结果发现微信语音消息的提示音还在从另一个通道往外蹦。四个 App 各唱各的,你的手机仿佛一个菜市场。这背后就是 Android AudioManager 的音频焦点机制在——或者说不在——起作用。
音频焦点不是锁,是一张没有强制力的建议书
Android 的音频焦点从设计根子上就带着一种天真的“绅士协定”气质。你调用 AudioManager.requestAudioFocus(),传进去一个 OnAudioFocusChangeListener,系统告诉你“好的,你现在拿到了焦点”,然后别的应用会收到 AUDIOFOCUS_LOSS 回调,理论上它们应该乖乖静音或者降低音量。问题在于,这套机制从头到尾没有任何强制力。AOSP 的 AudioService.java 里,焦点管理本质上就是一个栈结构加上回调分发,系统层面不会调用你的 MediaPlayer.pause(),也不会把某个进程的音频流掐断。它只会友善地通知你:“嘿,别人要用喇叭了,你自己看着办。”
这种设计的后果是什么?后果就是音频焦点成了一个可选配件。早期的 Android 开发者文档甚至把请求焦点描述为“最佳实践”而不是硬性要求。很多应用根本不调用 requestAudioFocus(),照样能放出声音。就算你调用了,返回值也只有三种:AUDIOFOCUS_REQUEST_GRANTED、AUDIOFOCUS_REQUEST_FAILED、AUDIOFOCUS_REQUEST_DELAYED。即便你拿到了 GRANTED,隔壁一个不守规矩的 App 照样可以不请求焦点直接播放,而系统对此毫无惩罚。这跟相机完全不同——相机硬件是互斥的,你打开了,别人就一定打不开。但扬声器不是,它是一个所有人都能往里灌数据的公用混音器,Google 选择了在软件层靠“自觉”来解决资源竞争,这从一开始就是个馊主意。
更讽刺的是,焦点丢失的类型分得特别细:AUDIOFOCUS_LOSS 是永久丢失,你应该彻底停止;AUDIOFOCUS_LOSS_TRANSIENT 是暂时丢失,你应该暂停但准备好恢复;AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK 是暂时丢失但可以 ducking(压低音量继续播放)。理想很丰满,现实是大部分应用开发者连 OnAudioFocusChangeListener 都懒得注册。抖音这类短视频应用为了用户体验,经常在收到 LOSS 后直接选择 abandonAudioFocus() 然后静音,但当你划到下一条视频时,它未必会重新请求焦点,而是直接开始播放——因为重新请求可能会被拒绝,导致视频没声音,用户会认为是 App 的 bug,然后给一星差评。于是最理性的商业选择就是:忽略焦点,或者假装配合,实则硬放。
Android 8.0 的 AudioFocusRequest:亡羊补牢,但羊已经跑了
Google 不是没意识到这个问题。Android 8.0(API 26)引入了 AudioFocusRequest 和 AudioFocusRequest.Builder,试图把这套绅士协定包装得更正式一点。你可以通过 setUsage(AudioAttributes.USAGE_MEDIA) 来声明你的音频用途,通过 setWillPauseWhenDucked(true) 告诉系统在被打断时自动帮你暂停,而不是压低音量。还有 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 这种更加语义化的焦点类型,看起来像是给混乱的江湖立了一套规矩。
可核心矛盾一点都没解决。焦点请求依然是个建议,AudioFocusRequest 只是让“建议书”的格式更规范了。Android 8.0 还引入了自动 ducking 的概念:如果焦点获得者允许,系统会自动帮失焦者降低音量,而不需要失焦应用自己处理。但问题在于,这个行为依赖于 AudioAttributes 的正确设置,而国内相当一部分应用还在用远古时代的 AudioManager.STREAM_MUSIC 硬编码,根本不理会 AudioAttributes 这套新语义。更有甚者,某些国产 ROM 在系统层自己改写了 AudioService 的焦点策略,把 Google 好不容易加进来的 AudioPolicy 和 FocusRequest 逻辑绕了过去,导致同样一套代码在 Pixel 上表现得规规矩矩,到了某品牌手机上就成了废纸。
从 AOSP 源码来看,MediaFocusControl 里的焦点请求处理逻辑并不复杂,它维护着一个焦点栈,新来的请求根据 AudioAttributes 的优先级和 focusGain 类型决定是否抢占。但这个优先级判断是静态的,它无法感知业务场景。比如导航的 TTS 和电话铃声,哪个该赢?理论上电话该赢,但如果用户正在听着导航找路口呢?系统不知道,它只能按预设的优先级表走。更糟的是,很多国内应用为了在后台保活,会长时间持有音频焦点不释放,比如某些网络电台 App,它们拿到 AUDIOFOCUS_GAIN 后就永远不再 abandon,直到被系统杀死。而 Android 的 Low Memory Killer 对音频进程的优先级保护,反而让这类流氓行为更加肆无忌惮。
国内 ROM 的魔幻现实:系统层也在添乱
如果你以为只有应用开发者不守规矩,那还是太年轻了。国内各大 ROM 厂商在音频策略上的各自为政,把原本就脆弱的焦点机制撕得更碎。华为 EMUI 某些版本里,系统自带的音乐应用拥有隐藏的音频焦点特权,第三方播放器在收到来电后经常无法正确恢复播放,因为华为的音频策略服务在系统层重新分配了焦点,但恢复时漏掉了非白名单应用。小米的 MIUI 早年为了支持“应用双开”和“分屏小窗”,在 AudioManager 里做了大量定制化修改,导致小窗模式下的视频应用和全屏游戏同时出声成为常态——用户可能觉得这是“人性化”,但做音频开发的都知道,这是焦点栈被多实例逻辑搞崩了的表现。
OPPO 的 ColorOS 和 vivo 的 OriginOS 也没好到哪去。它们普遍在系统里内置了一个“音频场景识别”服务,试图通过 AI 判断你现在该听什么。比如检测到游戏在前台,就自动压低音乐;检测到相机打开,就直接 mute 掉所有后台音频。这听起来很智能,但对开发者来说是噩梦。因为你的 `OnAudio