一、引言:多 App 同时发声的困境
想象这样一个场景:你正用耳机听音乐,突然导航 App 播报了一段路况语音。没有音频焦点机制时,音乐和导航语音会同时播放,两者互相叠加,用户既听不清导航,音乐也被噪音污染。
更糟糕的是:打来一个电话,电话、音乐、导航三路声音同时涌来——这大概是最混乱的用户体验了。
Android 的音频焦点(Audio Focus)机制就是为了解决这个问题而生的。它本质上是一套礼貌协议:应用在播放音频前先"举手申请",系统统一仲裁,通知其他应用"请暂停"或"请降低音量"。
重要的是,这套机制完全是协作性的——Android 并不会强制暂停应用,而是通过回调通知应用。应用响不响应、怎么响应,完全靠开发者自觉。这也是为什么市面上有那么多"氓流"App 根本不理焦点变化,依然强行播放自己的声音……好的开发者应该认真对待焦点机制。
二、音频焦点的基本概念
2.1 焦点 vs 音量
很多开发者会混淆音频焦点和音量控制,它们是两个独立维度:
| 维度 | 音频焦点 | 音量控制 |
|---|---|---|
| 控制对象 | 是否应该播放 | 播放多响 |
| 生效范围 | 逻辑层(应用自愿遵守) | 硬件/软件实际音量值 |
| API | requestAudioFocus() | setStreamVolume() |
| 实施方 | 应用自行响应回调 | AudioFlinger/HAL 强制生效 |
音频焦点控制的是播放权限,音量控制的是信号大小。两者可以独立变化:没有焦点的应用可以继续播放(只是违背了礼貌协议),而获得焦点的应用也可以把音量调为零。
2.2 焦点的层次结构
AudioFocus 请求有两个维度:
请求类型(申请焦点时指定):
AUDIOFOCUS_GAIN:长期持有焦点,独占性使用AUDIOFOCUS_GAIN_TRANSIENT:短暂使用,结束后归还AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:短暂使用,允许其他人继续播放(但降音量)AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE:短暂独占,完全禁止其他人播放
通知类型(收到回调时携带):
AUDIOFOCUS_LOSS:永久失去焦点(另一个 App 请求了GAIN)AUDIOFOCUS_LOSS_TRANSIENT:暂时失去焦点(另一个 App 请求了GAIN_TRANSIENT)AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:可以 Duck(降低音量继续播放)
类比一下:
GAIN= 霸占了整个会议室,其他人得离开GAIN_TRANSIENT= 借用一下,用完就还GAIN_TRANSIENT_MAY_DUCK= 借用一下,但你们可以继续小声说话GAIN_TRANSIENT_EXCLUSIVE= 借用,且要求周围绝对安静(录音场景)
三、焦点请求 API
3.1 推荐方式:AudioFocusRequest(Android 8+)
旧的 requestAudioFocus() 重载方法在 Android 8.0 被标记为废弃。推荐使用 AudioFocusRequest.Builder:
val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
)
.setAcceptsDelayedFocusGain(true) // 支持延迟获取焦点
.setWillPauseWhenDucked(false) // Duck时不暂停,手动降低音量
.setOnAudioFocusChangeListener( // 焦点变化回调
focusChangeListener,
Handler(Looper.getMainLooper())
)
.build()
val result = audioManager.requestAudioFocus(focusRequest)
when (result) {
AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> { /* 立即获得焦点,开始播放 */ }
AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> { /* 延迟获取,等待回调 */ }
AudioManager.AUDIOFOCUS_REQUEST_FAILED -> { /* 焦点请求失败,不要播放 */ }
}
Builder 关键参数说明:
| 参数 | 含义 | 默认值 |
|---|---|---|
setAcceptsDelayedFocusGain(true) | 是否接受延迟获取焦点(通话中请求会延迟) | false |
setWillPauseWhenDucked(true) | Duck通知时直接暂停而非降音量 | false |
setFocusGain() | 焦点类型 | 必填 |
setAudioAttributes() | 描述音频用途(影响优先级) | 必填建议 |
3.2 焦点变化监听器
private val focusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
when (focusChange) {
AudioManager.AUDIOFOCUS_GAIN -> {
// 重新获得焦点(例如通话结束,导航语音播完)
if (wasPlayingBeforeLoss) {
mediaPlayer.start()
mediaPlayer.setVolume(1.0f, 1.0f) // 恢复正常音量
}
}
AudioManager.AUDIOFOCUS_LOSS -> {
// 永久失去焦点(另一个App长期占用,如另一个音乐App)
mediaPlayer.pause()
wasPlayingBeforeLoss = false // 不要自动恢复,等用户主动操作
// 可以选择 release 掉 MediaPlayer 以节省资源
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
// 短暂失去焦点(来电、语音助手等)
mediaPlayer.pause()
wasPlayingBeforeLoss = true // 记录状态,焦点恢复后自动继续
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
// 可以继续播放,但要降低音量(导航、通知等)
mediaPlayer.setVolume(0.3f, 0.3f) // 降到30%音量
// 或者选择完全暂停(尤其是语音内容)
}
}
}
最佳响应策略:
| 通知类型 | 音乐/视频 | 语音播客/有声书 |
|---|---|---|
LOSS | 暂停,不自动恢复 | 暂停,不自动恢复 |
LOSS_TRANSIENT | 暂停,GAIN时恢复 | 暂停,GAIN时恢复 |
LOSS_TRANSIENT_CAN_DUCK | 降音量至30% | 暂停(语音叠加影响理解) |
3.3 焦点的释放
一定要在不需要时释放焦点,否则会影响其他应用:
// 正确的释放时机
override fun onPause() {
super.onPause()
// 暂停播放时不一定要释放焦点(取决于场景)
}
override fun onStop() {
super.onStop()
// 停止播放时必须释放焦点
if (::focusRequest.isInitialized) {
audioManager.abandonAudioFocusRequest(focusRequest)
}
mediaPlayer.stop()
}
override fun onDestroy() {
super.onDestroy()
audioManager.abandonAudioFocusRequest(focusRequest)
mediaPlayer.release()
}
常见坑:在 onStop() 里只释放了 MediaPlayer,忘记释放 AudioFocus,导致其他 App 的焦点请求一直被压制。
四、焦点仲裁机制:系统内部的"会议主席"
4.1 架构概览
音频焦点的仲裁逻辑运行在 system_server 进程的 AudioService 中,核心类是 MediaFocusControl:
整个仲裁流程:
- App 通过
AudioManager.requestAudioFocus()发起请求 AudioManager调用 Binder IPC →AudioService.requestAudioFocus()MediaFocusControl接收请求,评估优先级- 通知当前焦点持有者(如有),发送焦点变化回调
- 将新请求者入栈,返回请求结果
4.2 MediaFocusControl 源码解析
// frameworks/base/services/core/java/com/android/server/audio/MediaFocusControl.java
int requestAudioFocus(AudioAttributes aa, int focusChangeHint, IBinder cb,
IAudioFocusDispatcher fd, String clientId, String callingPackageName,
String attributionTag, int flags, IAudioPolicyCallback apc,
int sdk) {
synchronized (mAudioFocusLock) {
// 1. 构造 FocusRequester 对象,封装请求者信息
FocusRequester nfr = new FocusRequester(aa, focusChangeHint, cb, fd,
clientId, callingPackageName, attributionTag, this, ...);
// 2. 如果请求者已在栈中(重复请求),先处理旧的
if (mFocusOwners.contains(clientId)) {
removeFocusStackEntry(clientId, false, false);
}
// 3. 评估是否可以立即获得焦点
// IN_CALL 或 IN_COMMUNICATION 模式下,媒体类请求可能被延迟
if (canReceiveFocusLocked(nfr)) {
// 4. 通知当前所有焦点持有者
notifyTopOfStackToGrantFocus(nfr);
// 5. 新请求者入栈(成为栈顶)
mFocusStack.push(nfr);
return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
} else if ((flags & AudioManager.AUDIOFOCUS_FLAG_DELAY_OK) != 0) {
// 6. 支持延迟:加入等待队列
mFocusStack.push(nfr);
return AudioManager.AUDIOFOCUS_REQUEST_DELAYED;
} else {
return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
}
}
}
notifyTopOfStackToGrantFocus() 是发送焦点变化通知的核心:
private void notifyTopOfStackToGrantFocus(FocusRequester newRequest) {
Iterator<FocusRequester> stackIterator = mFocusStack.iterator();
while (stackIterator.hasNext()) {
FocusRequester focusLoser = stackIterator.next();
// 判断当前持有者是否要收到 LOSS 通知
int lossType = focusLoser.getFocusLossForGainRequest(
newRequest.getGainRequest());
if (lossType != AudioManager.AUDIOFOCUS_NONE) {
focusLoser.handleFocusLoss(lossType, newRequest, ...);
}
}
}
getFocusLossForGainRequest() 根据请求类型决定通知类型:
// FocusRequester.java
int getFocusLossForGainRequest(int gainRequest) {
switch (gainRequest) {
case AudioManager.AUDIOFOCUS_GAIN:
// 新来者要 GAIN(长期)→ 现有者收 LOSS(永久丢失)
return AudioManager.AUDIOFOCUS_LOSS;
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:
// 新来者要 GAIN_TRANSIENT → 现有者收 LOSS_TRANSIENT
return AudioManager.AUDIOFOCUS_LOSS_TRANSIENT;
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:
// 新来者说"可以duck" → 现有者收 LOSS_TRANSIENT_CAN_DUCK
return AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK;
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE:
// 独占 → 现有者收 LOSS_TRANSIENT
return AudioManager.AUDIOFOCUS_LOSS_TRANSIENT;
default:
return AudioManager.AUDIOFOCUS_NONE;
}
}
规则非常直观:新来者请求什么,现有者就收到对应的 LOSS 通知。
五、焦点栈管理
5.1 Focus Stack 工作机制
焦点栈是理解整个焦点系统的核心数据结构。它是一个 LinkedList<FocusRequester>,遵循 LIFO(后进先出)原则:
焦点栈的三个关键操作:
Push(入栈):新请求者获得焦点时,入栈成为栈顶
mFocusStack.push(nfr); // 新请求者成为栈顶(当前焦点持有者)
Pop(出栈):请求者释放焦点时,出栈并触发下一个持有者恢复
private void removeFocusStackEntry(String clientId, boolean signal, ...) {
Iterator<FocusRequester> stackIterator = mFocusStack.iterator();
while (stackIterator.hasNext()) {
FocusRequester fr = stackIterator.next();
if (fr.hasSameClient(clientId)) {
stackIterator.remove();
if (signal) {
// 通知下一个 FocusRequester(现在的栈顶)恢复焦点
notifyStackTopToGrantFocus();
}
break;
}
}
}
恢复通知:调用 notifyStackTopToGrantFocus(),向新栈顶发送 AUDIOFOCUS_GAIN 回调,实现焦点的"自动归还"。
5.2 栈中的优先级判断
并不是所有请求都能入栈。canReceiveFocusLocked() 中有这样的逻辑:
// 通话状态下,USAGE_MEDIA 类音频请求会被延迟
private boolean canReceiveFocusLocked(FocusRequester nfr) {
// 规则1:通话进行中,非通话相关音频不得抢占焦点
if (isInVoiceCallLocked() &&
!nfr.hasSameAttributesAs(AudioAttributes.USAGE_VOICE_COMMUNICATION)) {
return false;
}
// 规则2:检查是否有优先级更高的持有者阻止获取
// (Android 15新增:基于AudioAttributes的更细粒度判断)
for (FocusRequester focusOwner : mFocusStack) {
if (focusOwner.isBlockedBy(nfr)) {
return false;
}
}
return true;
}
通话场景是最典型的"优先级压制"场景:打电话时,任何新的媒体播放请求都会被延迟(如果 App 设置了 setAcceptsDelayedFocusGain(true))或直接失败。
5.3 同一应用的多次请求
当同一个应用多次请求焦点时(例如切换歌曲时),MediaFocusControl 会先移除旧的请求,再将新请求入栈——不会造成同一 App 在栈中出现多次。这防止了"焦点栈膨胀"问题。
六、延迟焦点机制
6.1 什么情况下会延迟
当设备处于通话状态(AUDIO_MODE_IN_CALL)时,媒体类音频焦点请求不会立即获得许可。如果 App 在 AudioFocusRequest 中设置了 setAcceptsDelayedFocusGain(true),请求会以 AUDIOFOCUS_REQUEST_DELAYED 状态"挂起",等通话结束后自动回调。
6.2 延迟焦点的正确处理
private var isFocusDelayed = false
private var wasPlayingBeforeLoss = false
private val focusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
when (focusChange) {
AudioManager.AUDIOFOCUS_GAIN -> {
if (isFocusDelayed || wasPlayingBeforeLoss) {
// 延迟焦点最终到来 或 短暂失去后重新获得
isFocusDelayed = false
mediaPlayer.start()
mediaPlayer.setVolume(1.0f, 1.0f)
}
}
// ... 其他情况处理
}
}
fun playMusic() {
val result = audioManager.requestAudioFocus(focusRequest)
when (result) {
AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> {
mediaPlayer.start()
}
AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> {
// 通话中,焦点延迟到通话结束后发放
// 显示"通话结束后自动播放"的提示
isFocusDelayed = true
showDelayedPlaybackIndicator()
}
AudioManager.AUDIOFOCUS_REQUEST_FAILED -> {
// 无法获取焦点(例如来电铃声场景)
showPlaybackFailedMessage()
}
}
}
延迟焦点是一个被严重低估的功能。没有它,用户在打电话时点击播放,歌曲无声无息地"播放"了,通话结束后还要手动重新点一次——体验很差。
七、Android 15 的新变化
7.1 自动 Duck 增强(Auto Ducking Improvements)
Android 15 对自动 Duck 功能做了重要改进。在之前的版本中,Duck 是由应用自行处理的(系统通知 CAN_DUCK,应用自己降音量)。Android 15 引入了系统级自动 Duck:
// Android 15: AudioService 中的自动 Duck 逻辑
// frameworks/base/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
void duckPlayers(FocusRequester winner, FocusRequester loser, boolean forceDuck) {
for (AudioPlaybackConfiguration apc : mPlayers.values()) {
if (apc.getClientUid() == loser.getClientUid()) {
// 系统直接在 AudioFlinger 层面降低音量
// 不需要应用响应回调
VolumeShaper.Configuration duckConfig = new VolumeShaper.Configuration.Builder()
.setInterpolatorType(VolumeShaper.Configuration.INTERPOLATOR_TYPE_CUBIC)
.setOptionFlags(VolumeShaper.Configuration.OPTION_FLAG_DELAY_EFFECT |
VolumeShaper.Configuration.OPTION_FLAG_VOLUME_IN_DBFS)
.setDuration(DEFAULT_DUCK_DURATION_MS)
.setCurve(new float[] {0.f, 1.f}, new float[]{0.f, -12.f}) // 降低12dBFS
.build();
apc.getPlayerProxy().applyVolumeShaper(duckConfig, new VolumeShaper.Operation.Builder()
.setXOffset(0.f).build());
}
}
}
系统级 Duck 的优点:
- 应用即使忘记实现
CAN_DUCK响应,系统也会自动降音量 - 使用
VolumeShaper实现平滑的淡入淡出(而非突变) - Duck 幅度固定为 -12dBFS(约30%音量),统一用户体验
应用需要注意:如果不希望被系统 Duck(例如游戏音效,希望完全暂停),可以在 AudioFocusRequest 中设置 setWillPauseWhenDucked(true)。
7.2 焦点优先级体系更新
Android 15 细化了基于 AudioAttributes 的焦点优先级判断。新增了对 USAGE_ASSISTANT(语音助手)和 USAGE_NOTIFICATION_EVENT(事件通知)的专项处理:
// 语音助手可以抢占几乎所有焦点(除了通话)
// USAGE_ASSISTANT 的优先级介于 IN_CALL 和 MEDIA 之间
if (attributes.getUsage() == AudioAttributes.USAGE_ASSISTANT) {
// 允许 duck 媒体播放,但不打断电话
gainRequest = AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK;
}
7.3 MediaSession 与焦点联动
Android 15 加强了 MediaSession 与 AudioFocus 的联动。当 App 使用 MediaSessionCompat 时,MediaSessionService 会自动管理焦点请求:
// 使用 Media3/ExoPlayer 时,焦点管理完全自动化
val player = ExoPlayer.Builder(context)
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
.build(),
/* handleAudioFocus = */ true // ExoPlayer 自动处理焦点
)
.build()
开启 handleAudioFocus = true 后,ExoPlayer 会:
- 播放前自动请求
AUDIOFOCUS_GAIN - 接收到
LOSS通知时自动暂停 - 接收到
LOSS_TRANSIENT_CAN_DUCK时自动降音量 - 接收到
GAIN时自动恢复
省去了大量模板代码,强烈推荐使用。
八、实战最佳实践
8.1 完整的焦点管理封装
将焦点管理逻辑封装成独立类,方便复用:
class AudioFocusHelper(
private val context: Context,
private val onFocusGain: () -> Unit,
private val onFocusLoss: () -> Unit,
private val onFocusLossTransient: () -> Unit,
private val onDuck: () -> Unit,
private val onUnduck: () -> Unit
) {
private val audioManager = context.getSystemService(AudioManager::class.java)
private var focusRequest: AudioFocusRequest? = null
private var currentFocusState = AudioManager.AUDIOFOCUS_LOSS
private val focusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
val previousFocusState = currentFocusState
currentFocusState = focusChange
when (focusChange) {
AudioManager.AUDIOFOCUS_GAIN -> {
if (previousFocusState == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
onUnduck() // 从 duck 状态恢复
} else {
onFocusGain() // 普通获得焦点
}
}
AudioManager.AUDIOFOCUS_LOSS -> onFocusLoss()
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> onFocusLossTransient()
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> onDuck()
}
}
/**
* 请求音频焦点
* @param usage 音频用途 (USAGE_MEDIA, USAGE_GAME, etc.)
* @param acceptDelayed 是否接受延迟获取(通话中)
* @return true=立即获得或已延迟, false=获取失败
*/
fun requestFocus(
usage: Int = AudioAttributes.USAGE_MEDIA,
acceptDelayed: Boolean = true
): Boolean {
val audioAttrs = AudioAttributes.Builder()
.setUsage(usage)
.build()
val request = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(audioAttrs)
.setAcceptsDelayedFocusGain(acceptDelayed)
.setWillPauseWhenDucked(false)
.setOnAudioFocusChangeListener(focusChangeListener,
Handler(Looper.getMainLooper()))
.build()
focusRequest = request
return when (audioManager.requestAudioFocus(request)) {
AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> {
currentFocusState = AudioManager.AUDIOFOCUS_GAIN
true
}
AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> {
// 延迟获取,等回调
true
}
else -> false
}
}
/**
* 释放音频焦点(停止播放时必须调用)
*/
fun releaseFocus() {
focusRequest?.let {
audioManager.abandonAudioFocusRequest(it)
focusRequest = null
}
currentFocusState = AudioManager.AUDIOFOCUS_LOSS
}
fun hasFocus() = currentFocusState == AudioManager.AUDIOFOCUS_GAIN
}
8.2 与 Lifecycle 结合
class MusicPlayerViewModel(application: Application) :
AndroidViewModel(application), DefaultLifecycleObserver {
private val focusHelper = AudioFocusHelper(
context = application,
onFocusGain = {
if (shouldAutoResume) {
mediaPlayer.start()
mediaPlayer.setVolume(1.0f, 1.0f)
}
},
onFocusLoss = {
shouldAutoResume = false // 永久丢失:不自动恢复
mediaPlayer.pause()
},
onFocusLossTransient = {
shouldAutoResume = mediaPlayer.isPlaying // 临时丢失:记录状态
mediaPlayer.pause()
},
onDuck = {
shouldAutoResume = true
mediaPlayer.setVolume(0.3f, 0.3f) // Duck至30%
},
onUnduck = {
mediaPlayer.setVolume(1.0f, 1.0f) // 恢复正常音量
}
)
private var shouldAutoResume = false
fun play() {
if (focusHelper.requestFocus()) {
mediaPlayer.start()
}
}
fun pause() {
mediaPlayer.pause()
// 注意:暂停时通常不要释放焦点!
// 用户可能只是临时暂停,很快会继续
}
fun stop() {
mediaPlayer.stop()
focusHelper.releaseFocus() // 停止时才释放焦点
shouldAutoResume = false
}
override fun onCleared() {
focusHelper.releaseFocus() // ViewModel 销毁时一定要释放
mediaPlayer.release()
}
}
8.3 游戏应用的特殊处理
游戏通常使用 USAGE_GAME,对焦点的处理策略与普通媒体不同:
// 游戏应用:来电时暂停,导航语音时可继续(不 duck 音乐,duck 游戏效果音)
val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build())
.setWillPauseWhenDucked(true) // 游戏音效宁可暂停也不降音量(听起来更好)
.setOnAudioFocusChangeListener(
{ focusChange ->
when (focusChange) {
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK,
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
// 游戏暂停时弹出暂停菜单,比悄悄降音量体验更好
gameEngine.pause()
showPauseOverlay()
}
AudioManager.AUDIOFOCUS_GAIN -> {
// 可以选择自动恢复,也可以等用户点击继续
hidePauseOverlay()
// gameEngine.resume() // 根据游戏类型决定是否自动恢复
}
}
},
Handler(Looper.getMainLooper())
)
.build()
九、常见问题与调试
9.1 常见问题
Q:我的应用响应了焦点变化,但通话结束后音乐没有自动恢复?
A:检查两点:
- 是否在
AUDIOFOCUS_LOSS_TRANSIENT时将shouldAutoResume = true - 是否在
AUDIOFOCUS_GAIN回调中判断了shouldAutoResume再恢复播放 - 检查是否在用户暂停时错误地设置了
shouldAutoResume = true(用户手动暂停不应自动恢复)
Q:两个音乐 App 同时请求 AUDIOFOCUS_GAIN,最后谁赢了?
A:后请求的胜出。Focus Stack 是栈结构,后入栈的在栈顶,系统通知之前的持有者收到 AUDIOFOCUS_LOSS。所以用户如果同时打开两个音乐 App,应该只有最后打开的在播放——前提是两个 App 都正确实现了焦点机制。
Q:我的 App 没有请求焦点也能播放,这正常吗?
A:是的,音频焦点是合作性协议,不是强制执行的。Android 不会强制停止没有焦点的播放。但这是不良实践,会影响用户体验,也可能违反 Play Store 政策。
Q:录音时,其他 App 的音频应该完全静音吗?
A:使用 AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE(独占焦点),表明"我需要绝对安静"。收到这个通知的应用应该立刻停止播放(不是 duck,而是完全停止)。录音 App 通常会用这种类型。
9.2 调试工具
# 查看当前焦点栈状态
adb shell dumpsys audio | grep -A 20 "Audio Focus"
# 典型输出:
# Audio Focus stack entries (from foreground to background) :
# FocusRequester[uid=10145, client=..., attr=AudioAttributes:
# usage=MEDIA content=MUSIC, focusGain=AUDIOFOCUS_GAIN, ...]
# FocusRequester[uid=10089, client=..., attr=AudioAttributes:
# usage=MEDIA content=MUSIC, focusGain=AUDIOFOCUS_GAIN, ...]
# 追踪焦点事件日志
adb shell setprop log.tag.AudioService V
adb logcat -s AudioService:V | grep -E "requestAudioFocus|abandonAudioFocus|dispatchFocusChange"
# 查看音频播放状态(Android 9+)
adb shell dumpsys audio | grep -A 5 "PlaybackActivityMonitor"
关键日志字段解读:
requestAudioFocus() from uid=10145, clientId=com.example.music|...
focusChangeType=AUDIOFOCUS_GAIN attrs=AudioAttributes: usage=MEDIA
result=AUDIOFOCUS_REQUEST_GRANTED
Focus loss dispatched to uid=10089: AUDIOFOCUS_LOSS
-> App 收到永久焦点丢失通知
9.3 与 MediaSession 协调
如果 App 使用了 MediaBrowserService 和 MediaSession,系统的 MediaSessionService 会代理焦点管理。此时不要自行请求焦点,避免双重管理:
// 使用 Media3/ExoPlayer + MediaSession 时
// 焦点由 MediaSessionService 统一处理,不要在外部手动请求
val mediaSession = MediaSession.Builder(context, player)
.build()
// 系统会在 player.play() 时自动请求焦点
十、总结
| 知识点 | 要点 |
|---|---|
| 焦点类型 | GAIN(长期)/ GAIN_TRANSIENT(短暂)/ GAIN_TRANSIENT_MAY_DUCK(可duck)/ GAIN_TRANSIENT_EXCLUSIVE(独占录音) |
| 焦点丢失 | LOSS(永久,不自动恢复)/ LOSS_TRANSIENT(短暂,可自动恢复)/ LOSS_TRANSIENT_CAN_DUCK(降音量继续) |
| 仲裁规则 | 后请求者胜出;通话场景优先级最高;优先级由 AudioAttributes 决定 |
| 焦点栈 | LIFO栈;释放焦点后自动通知下一持有者;同一App不会入栈两次 |
| Android 15 | 系统级自动Duck(-12dBFS);语音助手优先级增强;ExoPlayer自动焦点管理 |
| 必须做的事 | 播放前请求焦点;停止时释放焦点;正确响应回调;区分永久/临时丢失 |
音频焦点是 Android 音频开发中最"软性"但最影响用户体验的机制。实现它不复杂,但实现正确却需要细心——尤其是处理边界情况(通话打断、多次暂停恢复、App 意外退出等)。下一篇我们将深入音量控制系统,看看从用户按下音量键到硬件实际调音,中间经历了多少层处理。
踩坑回忆:曾经有个产品需求"用户在后台播放时,来了导航语音应该降低音量"。正常实现就是响应
CAN_DUCK降音量。但测试发现,如果导航语音很短(小于1秒),会出现"音量降低→快速恢复→再降低"的抖动感。解决方案:在 duck 时加一个 300ms 的延迟检测,若 300ms 内又收到GAIN,则不执行 duck 操作。细节决定体验。