如果你是一名 Android 开发者,尤其是做过多媒体、语音助手或者车载开发的工程师,你一定在某个深夜对着 AudioManager 破口大骂过。
小弟不才,我刚骂完。
起因是我正在做一个 AOSP 项目,开发一个非常复杂的 App - Launcher。
这个 App 支持语音的各种操作,目前就在改一个 Bug —— 焦点抢占导致其他 App 的音乐播放有问题。
今天,我们就以一场“抬杠”式的对话,彻底扒下 Android 音频焦点(Audio Focus)那层令人费解的外衣。
出场人物:
- 暴躁老哥(我): 饱受 Audio Focus 折磨的 Android 开发,今天专程来找茬。
- Android 田文镜: 见多识广的老油条,专注于解答(并忍受)各种刁钻问题。
第一回合:这到底是个什么反人类设计
暴躁老哥: 田文镜!我就问你,Android 的音频焦点申请到底是怎么设计的?我觉得好难用啊!为了放个声音,我还要写一堆错综复杂的状态机,别人抢了还要管,这是什么反人类的设计?
内心OS: 无数个日夜,我都在与第三方扯皮音频焦点的 Bug ,这个 Bug 最糟糕的河西在于——最终谁去改!有些 App 根本不遵循音频焦点的设计原则,根本无法让他暂停播放!
Android 田文镜: (喝了口茶)年轻人,火气不要这么大。你觉得难用,是因为你没有理解它的核心设计理念。
Android 是一个多任务系统,但人类的耳朵在同一时间只能接受单一(或极少数)的主音频流。想象一下:你正在听歌,这时候导航突然大声播报,微信又弹出一个超大声的提示音,后台还有一个带声音的广告——这绝对是一场听觉灾难。
为了解决冲突,Google 设计了 Audio Focus。但它早期的核心思想是一个“君子协议”,而不是底层强制锁:
- 系统是一个大喇叭: 当你申请焦点时,系统会通知之前占用焦点的 App:“老哥,焦点被别人抢了。”
- 如何响应全靠自觉: 至于被抢的 App 是暂停、降低音量,还是假装没听见继续“头铁”播放,系统早年是完全不管的。它把处理复杂冲突的责任推给了开发者。
特别是如果你在做车机系统,这套规则就变成了“独裁统治”。
车机底层的 CarAudioService 接管了焦点,维护了严密的并发矩阵。导航发声时,根本不需要你同意,底层会直接对音乐进行硬件级压音(Ducking)甚至静音(Muting)。这也是为什么车机的焦点管理让人觉得更加受限。
第二回合:有本事你一口气背出所有的焦点类型
暴躁老哥: 行吧,就算它是君子协议。但你别以为我好糊弄,API 文档里那一大堆参数我看都看晕了。你要是能一口气把获取焦点的方式全列出来,我当场......叫你大师!
内心OS: 我真的不相信这世界上有人能一口气背出音频焦点所有的获取方式!
Android 田文镜: (冷笑)这有何难?听好了,Android 申请焦点总共就四大门派,搞懂了它们,你就出师了:
-
AUDIOFOCUS_GAIN(长住霸占型):
- 特点: 申请长期的、未知时长的焦点。
- 场景: 音乐播放器、长视频 App、播客。当你准备长时间霸占扬声器,并且希望其他背景音乐“彻底死心”并释放资源时用它。
-
AUDIOFOCUS_GAIN_TRANSIENT(短暂借用型):
- 特点: 申请短暂的焦点,用完马上还。系统允许其他应用的提示音与你混音。
- 场景: 导航播报、语音助手的回答(TTS播报)。比如导航说“前方路口左转”时,让背景音乐暂停一下,播完让音乐继续。
-
AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK(短租温柔型):
- 特点: 申请短暂焦点,而且态度极其温柔。它告诉被抢的人:“你不用暂停,只需要把音量降低(Duck)一点点就行,我插句话”。
- 场景: 微信提示音、导航播报(允许混音时)。比如你听歌时导航说话,音乐声音变小,导航说完音乐声音恢复正常。这种体验比直接暂停更连贯。
-
AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE(短租且绝对霸道型):
- 特点: 申请短暂焦点,且要求“绝对排他”。占用期间系统会严格屏蔽其他一切提示音和按键音。
- 场景: 语音唤醒/识别(ASR)、警告提示、录音。当你录音时,绝对不允许系统发出“叮”的一声混进录音里导致识别失败或者其他杂音问题。
第三回合:GAIN 和 EXCLUSIVE,傻傻分不清楚
暴躁老哥: 行吧,大师。那我问你个具体的,AUDIOFOCUS_GAIN 和 AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE 这两个参数有什么区别,不都是“我要抢占焦点”吗?
内心OS: 既然都是抢占焦点,能有什么区别?
Android 田文镜: 这两者在使用场景和系统底层处理方式上有着天壤之别!用一句话概括:
GAIN 是“我要长住,请大家让座”,而 EXCLUSIVE 是“我短住,但是要求绝对安静,谁也别出声”。
-
AUDIOFOCUS_GAIN(长期的常规独占):
- 含义: 你不知道要播多久,通常是很长时间。
- 别人怎么想: 正在放歌的 App 会收到
LOSS(永久失去)。它应该彻底停止播放并释放资源。 - 包容度: 它允许系统的其他提示音混进来。听歌时来微信,你会听到“叮”的一声,音乐不会断。
- 场景: 音乐播放器、长视频、播客。
-
AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE(短暂的排他独占):
- 含义: 占用时间很短,马上就还。
- 别人怎么想: 放歌的 App 会收到
LOSS_TRANSIENT(短暂失去),乖乖暂停等待。 - 霸道程度(核心区别): 极其霸道!系统会尽可能保证在你占用期间,没有任何声音干扰你。系统的按键音、通知音统统被静音或阻止。
- 场景: 语音唤醒/识别(ASR)、录像机。你总不希望用户喊“你好语音助手”时,录进去一声微信的“叮”导致识别失败吧?
排他——意味着在这个时间内,别的声音是不允许说话的!
你仔细想想这种设计理念,AUDIOFOCUS_GAIN 这种长期的占用如果还是排他的,是不是特别不合理;但是如果你真的希望排他独占,那么就只能短暂的,不能长时间的!
第四回合:那 TRANSIENT 和 EXCLUSIVE 又有啥区别
暴躁老哥: 等等,那 AUDIOFOCUS_GAIN_TRANSIENT 和 AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE 呢?既然都是带 TRANSIENT,说明都是短时间占用,这俩又有什么区别?
内心OS: 我不信短暂占用还能有区别?
Android 田文镜: 你的盲点又来了!既然都是 TRANSIENT,那对于被抢焦点的音乐 App 来说,完全没有区别,它收到的都是 LOSS_TRANSIENT,都会乖乖暂停。
它们的区别在于系统对“第三方干扰音”的态度。
打个通俗的比方:
-
TRANSIENT(短暂发言):
你在会议室放着背景音乐,突然站起来说:“音乐停一下,我插两句话。”(音乐暂停)。这时候如果旁边同事的手机来微信了,“叮”的一声你是能听到的。场景: 导航播报、语音助手回答(TTS播报)。
-
TRANSIENT_EXCLUSIVE(机密录音):
你站起来说:“音乐停一下,现在我要录音了,所有人手机静音,任何人不许咳嗽!”(音乐暂停,且系统强制屏蔽所有杂音)。场景: 麦克风开始收音时。
第五回合:终极刁难 —— 快进快出的 GAIN
暴躁老哥: (摸了摸下巴)我好像懂了点。那我给你出一个绝的:我申请 AUDIOFOCUS_GAIN,但是过个 5 秒钟我就立刻调用 abandonAudioFocus 释放掉!反正都是用一下就还回去,这不就和 AUDIOFOCUS_GAIN_TRANSIENT 一模一样了吗?我还背那么多参数干嘛!
内心OS: 系统怎么知道我是快进快出?
Android 田文镜: (猛拍大腿),嘿,你TND,还真是个天才!
绝对不行,这是极其糟糕的做法!你只站在了你自己的视角,完全没有考虑被你抢走焦点的“受害者”的感受!
区别不在于你实际占用了多久,而在于系统发给别人的“契约”是不一样的。
-
如果你用 TRANSIENT 然后释放: 系统告诉音乐 App
LOSS_TRANSIENT。音乐 App 想:“他马上回来”,于是它仅仅暂停(Pause),原地待命。5秒后你释放,音乐 App 收到GAIN,自动无缝恢复播放,体验丝滑。 -
如果你用 GAIN 然后立刻释放: 系统告诉音乐 App
LOSS。音乐 App 心灰意冷:“主人不需要我了”,于是它不仅暂停,还彻底销毁了播放器实例,甚至清理了通知栏。5秒后你释放,焦点空闲了,但音乐 App 绝对不会自动恢复播放。用户会骂街:“你的破软件把我的后台音乐彻底杀掉了?!”
暴躁老哥: 卧槽?!那我 abandon 之后,那个音乐 App 难道收不到 GAIN 的通知重新活过来吗?
Android 田文镜: 绝大多数情况下,收不到!
因为底层的 AudioManager 维护了一个焦点栈(Audio Focus Stack)。
当音乐 App 收到 LOSS 时,正常的逻辑是它不仅停止播放,还会主动调用 abandonAudioFocus,将自己从焦点栈中彻底移除!。
等你 5 秒后释放焦点时,系统往下看,发现栈已经空了。没有任何人排队,自然也不会发通知。
从设计的逻辑上讲,AUDIOFOCUS_GAIN 表示你的占用时间是无限的,无法预知结果的,就像如果你是播放音乐,你不知道用户何时关闭。此时当其他应用收到 LOSS 通知的时候,自然不会一直等待下去,所以就会做音频相关的清理工作了。
第六回合:一图胜千言
暴躁老哥: 这个焦点栈退栈的逻辑有点绕,大师你能画个图吗?
Android 田文镜: 没问题,两张时序图,让你彻底看透。
场景一:申请 GAIN
场景二:申请 TRANSIENT
醍醐灌顶
暴躁老哥: 不愧是田文镜大师,我真是醍醐灌顶了!原来之前用户的投诉都是因为我用错了参数!
Android 田文镜: 孺子可教也。既然你这么好学,最后,我再给你一套决策宝典,下次碰到音频焦点申请的难题的时候,直接看宝典:
暴躁老哥: 跪谢大师!
Android 田文镜: 顺便提一句,从 Android 12 (API 31) 开始,如果别的 App 不守规矩(比如收到 LOSS 还不暂停),系统底层的 AudioFlinger 会直接把它强制淡出(Fade-out)甚至物理级静音。
所以,这套“君子协议”正在逐渐变成“系统强制的法律”。写好焦点状态机,不仅是为了优雅,更是为了你的 App 在新系统和苛刻的车机环境下能够正常存活!
暴躁老哥: 大师!(感激涕零)