6、音频焦点机制:焦点请求与释放、焦点策略与冲突处理

音频焦点,说白了就是系统里一把「谁说话」的钥匙。

我刚开始做Android音频时,觉得这玩意儿不就是个开关嘛。后来踩了几个坑才明白——焦点机制是整个音频策略的基石。你想想看,如果两个App同时播放音乐,用户听到的就是一团浆糊。焦点机制就是为了解决这个问题的。

6.1 焦点是什么?为什么需要它?

音频焦点是一个全局的、独占的资源。系统同一时间只允许一个App持有焦点。持有焦点的App可以正常播放,其他App要么暂停,要么降低音量,要么干脆闭嘴。

我个人习惯把焦点理解成「麦克风前的发言权」。谁拿到焦点,谁的声音就是主角。

核心原则:焦点不是强制抢占,而是协商机制。App可以请求焦点,也可以放弃焦点。系统会根据策略决定是否授予。

6.2 焦点请求与释放

请求焦点很简单,调用 AudioManager.requestAudioFocus() 就行。但这里有几个关键参数,我当年第一次用的时候就栽了跟头。

// 请求焦点示例
AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
AudioAttributes attrs = new AudioAttributes.Builder()
        .setUsage(AudioAttributes.USAGE_MEDIA)
        .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
        .build();
int result = am.requestAudioFocus(
        afChangeListener,          // 焦点变化监听器
        attrs,                     // 音频属性
        AudioManager.AUDIOFOCUS_GAIN,  // 焦点类型
        AudioManager.AUDIOFOCUS_FLAGS); // 可选标志
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    // 焦点获取成功,开始播放
}

释放焦点更简单,调用 abandonAudioFocus() 就行。但我建议你在 onPause() 或播放停止时主动释放,别等系统来回收。

小技巧:我曾经在某个项目中,App退到后台后还占着焦点不放,导致其他App无法播放。后来加了个生命周期监听,在onStop()里释放焦点,问题就解决了。

6.3 焦点策略:AUDIOFOCUS_GAIN 家族

焦点策略有四种,我列个表方便你对比:

策略常量 含义 对当前焦点持有者的影响 典型场景
AUDIOFOCUS_GAIN 完全获取焦点 原持有者失去焦点,应停止播放 音乐播放器开始播放
AUDIOFOCUS_GAIN_TRANSIENT 短暂获取焦点 原持有者应暂停播放 语音消息、导航提示
AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 短暂获取焦点,允许闪避 原持有者降低音量(闪避) 通知提示音、短信铃声
AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE 短暂获取焦点,独占模式 原持有者应停止播放,且不允许闪避 语音唤醒、紧急警报

嗯,这里要注意:MAY_DUCK 里的「闪避」不是让App完全静音,而是把音量降到原来的30%左右。我见过不少开发者直接静音了,用户反而觉得奇怪——背景音乐突然消失,体验很突兀。

6.4 焦点冲突处理

冲突处理是焦点机制里最绕的部分。说白了就是:当两个App都想发声时,系统怎么裁决?

系统会维护一个焦点栈。新请求的焦点会压栈,栈顶的App拥有焦点。当栈顶App释放焦点后,系统会把焦点还给栈中的下一个App。

但这里有个坑:焦点不是自动恢复的。系统只会通知你「焦点被抢了」,至于你要不要恢复播放,那是你自己的事。

注意:我曾经遇到过一个Bug:App A播放音乐时,App B发起语音通话。A收到焦点丢失通知后暂停了播放。但B通话结束后,A没有自动恢复播放。用户以为App坏了。解决方案是在焦点变化监听器里处理 AUDIOFOCUS_GAIN 事件,主动恢复播放。

// 焦点变化监听器
private AudioManager.OnAudioFocusChangeListener afChangeListener =
        new AudioManager.OnAudioFocusChangeListener() {
    @Override
    public void onAudioFocusChange(int focusChange) {
        switch (focusChange) {
            case AudioManager.AUDIOFOCUS_GAIN:
                // 重新获得焦点,恢复播放
                resumePlayback();
                break;
            case AudioManager.AUDIOFOCUS_LOSS:
                // 永久失去焦点,停止播放
                stopPlayback();
                break;
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                // 短暂失去焦点,暂停播放
                pausePlayback();
                break;
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                // 短暂失去焦点,降低音量
                duckPlayback();
                break;
        }
    }
};

6.5 焦点机制的核心流程图

下面这张图是我自己整理的,把焦点请求、冲突、释放的完整流程画出来了。你仔细看看,基本就能理解整个机制了。

音频焦点请求与冲突处理流程 App 请求焦点 当前有焦点 持有者吗? 授予焦点 冲突处理 根据请求策略 通知原持有者 原持有者响应 授予新焦点 App 播放完毕释放焦点 焦点归还系统 焦点栈

6.6 实战中的避坑指南

我做了这么多年音频,总结了几条血泪教训:

  • 不要假设焦点一定会被授予。 请求焦点后一定要检查返回值。我曾经见过一个App,不管结果直接播放,结果和导航语音打架。
  • 释放焦点要彻底。 调用 abandonAudioFocus() 时,传入的监听器要和请求时一致。否则系统可能找不到对应的监听器,焦点释放失败。
  • 处理焦点丢失要分场景。 永久丢失(LOSS)和短暂丢失(LOSS_TRANSIENT)的处理逻辑完全不同。前者要彻底停止,后者只需要暂停。
  • 闪避(DUCK)不是静音。 把音量降到30%左右就行,别直接 mute。用户能听到背景音乐若隐若现,体验反而更好。

我的习惯:在开发音频App时,我会写一个焦点管理类,统一处理请求、释放、监听。这样代码清晰,也不容易漏掉某个分支。你想想看,如果每个Activity都自己写一套焦点逻辑,后期维护起来得多痛苦。

6.7 小结

音频焦点机制,说白了就是一套「谁先说话谁有理」的规则。但规则背后,是系统对用户体验的精细考量。你掌握了焦点请求的四种策略,理解了冲突处理的流程,再配合正确的监听器实现,基本就能应对大部分场景了。

嗯,焦点这块内容就讲到这里。记住一句话:焦点不是抢来的,是协商来的。 尊重用户的听觉体验,你的App才能走得更远。

公众号:蓝海资料掘金营,微信deep3321