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

0 阅读14分钟

一、引言:多 App 同时发声的困境

想象这样一个场景:你正用耳机听音乐,突然导航 App 播报了一段路况语音。没有音频焦点机制时,音乐和导航语音会同时播放,两者互相叠加,用户既听不清导航,音乐也被噪音污染。

更糟糕的是:打来一个电话,电话、音乐、导航三路声音同时涌来——这大概是最混乱的用户体验了。

Android 的音频焦点(Audio Focus)机制就是为了解决这个问题而生的。它本质上是一套礼貌协议:应用在播放音频前先"举手申请",系统统一仲裁,通知其他应用"请暂停"或"请降低音量"。

重要的是,这套机制完全是协作性的——Android 并不会强制暂停应用,而是通过回调通知应用。应用响不响应、怎么响应,完全靠开发者自觉。这也是为什么市面上有那么多"氓流"App 根本不理焦点变化,依然强行播放自己的声音……好的开发者应该认真对待焦点机制。

二、音频焦点的基本概念

2.1 焦点 vs 音量

很多开发者会混淆音频焦点和音量控制,它们是两个独立维度:

维度音频焦点音量控制
控制对象是否应该播放播放多响
生效范围逻辑层(应用自愿遵守)硬件/软件实际音量值
APIrequestAudioFocus()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

06-01-audio-focus-architecture.png

整个仲裁流程:

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

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(后进先出)原则:

06-02-audio-focus-stack.png

焦点栈的三个关键操作:

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 加强了 MediaSessionAudioFocus 的联动。当 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 会:

  1. 播放前自动请求 AUDIOFOCUS_GAIN
  2. 接收到 LOSS 通知时自动暂停
  3. 接收到 LOSS_TRANSIENT_CAN_DUCK 时自动降音量
  4. 接收到 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:检查两点:

  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 调试工具

# 查看当前焦点栈状态
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 使用了 MediaBrowserServiceMediaSession,系统的 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 操作。细节决定体验。