Android 15音频子系统(六):音频焦点管理机制深度解析

一、引言:多 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

kotlin 复制代码
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 焦点变化监听器

kotlin 复制代码
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 焦点的释放

一定要在不需要时释放焦点,否则会影响其他应用:

kotlin 复制代码
// 正确的释放时机
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

整个仲裁流程:

  1. App 通过 AudioManager.requestAudioFocus() 发起请求
  2. AudioManager 调用 Binder IPC → AudioService.requestAudioFocus()
  3. MediaFocusControl 接收请求,评估优先级
  4. 通知当前焦点持有者(如有),发送焦点变化回调
  5. 将新请求者入栈,返回请求结果

4.2 MediaFocusControl 源码解析

java 复制代码
// 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() 是发送焦点变化通知的核心:

java 复制代码
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() 根据请求类型决定通知类型:

java 复制代码
// 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(入栈):新请求者获得焦点时,入栈成为栈顶

java 复制代码
mFocusStack.push(nfr);  // 新请求者成为栈顶(当前焦点持有者)

Pop(出栈):请求者释放焦点时,出栈并触发下一个持有者恢复

java 复制代码
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() 中有这样的逻辑:

java 复制代码
// 通话状态下,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 延迟焦点的正确处理

kotlin 复制代码
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

java 复制代码
// 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(事件通知)的专项处理:

java 复制代码
// 语音助手可以抢占几乎所有焦点(除了通话)
// USAGE_ASSISTANT 的优先级介于 IN_CALL 和 MEDIA 之间
if (attributes.getUsage() == AudioAttributes.USAGE_ASSISTANT) {
    // 允许 duck 媒体播放,但不打断电话
    gainRequest = AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK;
}

7.3 MediaSession 与焦点联动

Android 15 加强了 MediaSessionAudioFocus 的联动。当 App 使用 MediaSessionCompat 时,MediaSessionService 会自动管理焦点请求:

kotlin 复制代码
// 使用 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 会:

  1. 播放前自动请求 AUDIOFOCUS_GAIN
  2. 接收到 LOSS 通知时自动暂停
  3. 接收到 LOSS_TRANSIENT_CAN_DUCK 时自动降音量
  4. 接收到 GAIN 时自动恢复

省去了大量模板代码,强烈推荐使用。

八、实战最佳实践

8.1 完整的焦点管理封装

将焦点管理逻辑封装成独立类,方便复用:

kotlin 复制代码
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 结合

kotlin 复制代码
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,对焦点的处理策略与普通媒体不同:

kotlin 复制代码
// 游戏应用:来电时暂停,导航语音时可继续(不 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:检查两点:

  1. 是否在 AUDIOFOCUS_LOSS_TRANSIENT 时将 shouldAutoResume = true
  2. 是否在 AUDIOFOCUS_GAIN 回调中判断了 shouldAutoResume 再恢复播放
  3. 检查是否在用户暂停时错误地设置了 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 调试工具

bash 复制代码
# 查看当前焦点栈状态
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"

关键日志字段解读

ini 复制代码
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 使用了 MediaBrowserServiceMediaSession,系统的 MediaSessionService 会代理焦点管理。此时不要自行请求焦点,避免双重管理:

kotlin 复制代码
// 使用 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 操作。细节决定体验。

相关推荐
千里马学框架1 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台1 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone1 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc1 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo1 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077001 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼1 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone1 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen1 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone1 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui