Android 吐槽大会:音频焦点反人类

如果你是一名 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 申请焦点总共就四大门派,搞懂了它们,你就出师了:

  1. AUDIOFOCUS_GAIN(长住霸占型):

    • 特点: 申请长期的、未知时长的焦点。
    • 场景: 音乐播放器、长视频 App、播客。当你准备长时间霸占扬声器,并且希望其他背景音乐"彻底死心"并释放资源时用它。
  2. AUDIOFOCUS_GAIN_TRANSIENT(短暂借用型):

    • 特点: 申请短暂的焦点,用完马上还。系统允许其他应用的提示音与你混音。
    • 场景: 导航播报、语音助手的回答(TTS播报)。比如导航说"前方路口左转"时,让背景音乐暂停一下,播完让音乐继续。
  3. AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK(短租温柔型):

    • 特点: 申请短暂焦点,而且态度极其温柔。它告诉被抢的人:"你不用暂停,只需要把音量降低(Duck)一点点就行,我插句话"。
    • 场景: 微信提示音、导航播报(允许混音时)。比如你听歌时导航说话,音乐声音变小,导航说完音乐声音恢复正常。这种体验比直接暂停更连贯。
  4. AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE(短租且绝对霸道型):

    • 特点: 申请短暂焦点,且要求"绝对排他"。占用期间系统会严格屏蔽其他一切提示音和按键音。
    • 场景: 语音唤醒/识别(ASR)、警告提示、录音。当你录音时,绝对不允许系统发出"叮"的一声混进录音里导致识别失败或者其他杂音问题。

第三回合:GAIN 和 EXCLUSIVE,傻傻分不清楚

暴躁老哥: 行吧,大师。那我问你个具体的,AUDIOFOCUS_GAINAUDIOFOCUS_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_TRANSIENTAUDIOFOCUS_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 在新系统和苛刻的车机环境下能够正常存活!

暴躁老哥: 大师!(感激涕零)

相关推荐
吃不胖爹2 小时前
手机连接 Android Studio 调试完整步骤
android·智能手机·android studio
蜡台2 小时前
Android Studio 高版本兼容低版本项目配置
android·ide·jdk·gradle·android studio
Android系统攻城狮2 小时前
Android tinyalsa深度解析之pcm_params_set_min调用流程与实战(一百六十九)
android·pcm·tinyalsa·音频进阶
高梦轩2 小时前
MySQL 主从复制 + 读写分离
android·数据库
cch89183 小时前
PHP vs C++:10倍性能差距的编程语言对决
android·java·开发语言
cnnews3 小时前
Termux中安装python包
android·linux·开发语言·python·安卓·termux
00后程序员张9 小时前
从审核被拒到稳定过审,iOS 上架技术优化
android·ios·小程序·https·uni-app·iphone·webview
不会写DN12 小时前
PHP 中的文件读写与上传
android·开发语言·php
冬奇Lab14 小时前
Android 15音频子系统(七):音量控制系统深度解析
android·音视频开发