13、音频焦点管理:焦点机制设计、焦点请求与释放、焦点变化通知、焦点与音量管理联动

音频焦点,说白了就是Android系统里音频资源的“通行证”。

我刚开始接触AudioFlinger时,觉得焦点管理就是个简单的“谁要谁拿”的逻辑。后来在做一个多音源播放器项目时,才发现这里面的门道深着呢。你想想看,如果两个App同时播放音乐,系统该听谁的?如果导航语音播报时,音乐要不要自动降低音量?这些场景,全靠焦点机制来协调。

13.1 焦点机制设计:谁说了算?

音频焦点的设计思路,其实很像现实中的“话筒使用权”。

系统里只有一个“焦点持有者”。其他App想发声,必须先申请焦点。焦点申请成功后,系统会通知之前的持有者:“嘿,你的焦点被抢了”。

焦点类型分为几种:

  • AudioManager.AUDIOFOCUS_GAIN:长期焦点,比如音乐播放器。拿到后可以一直用,直到主动释放或被抢走。
  • AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:短暂焦点,比如短信提示音。用完就释放,不会长期霸占。
  • AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:短暂焦点,但允许之前的播放器“闪避”(降低音量),而不是完全暂停。

我个人习惯把焦点机制理解成一个“栈”。每次申请焦点,就像往栈顶压入一个元素。释放焦点时,就从栈顶弹出。栈顶的App拥有实际播放权。

核心设计原则:

  • 同一时间只有一个App持有焦点
  • 焦点可以“被抢占”,但必须通知原持有者
  • 焦点释放后,自动恢复上一个持有者的焦点

嗯,这里要注意:焦点机制是应用层的约定,不是内核强制。也就是说,App如果不遵守焦点规则,照样可以播放声音。但系统会通过AudioManager的API来引导开发者正确使用。

13.2 焦点请求与释放:代码里怎么玩?

焦点请求的入口在AudioManager类里。我直接上代码,你们感受一下:

// 申请焦点
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(
        focusChangeListener,   // 焦点变化监听器
        attrs,                 // 音频属性
        AudioManager.AUDIOFOCUS_GAIN,  // 焦点类型
        0                      // 标志位,一般传0
);

if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    // 焦点申请成功,开始播放
    startPlayback();
}

释放焦点就更简单了:

am.abandonAudioFocus(focusChangeListener);

我在项目中遇到过一个问题:某个App在onPause()里释放了焦点,但onResume()时没有重新申请。结果音乐播放到一半,突然被另一个App抢走了焦点,用户体验极差。

所以我的建议是:焦点申请和释放一定要成对出现。不要只在播放开始时申请,结束时才释放。中间如果暂停了,也应该释放焦点,让其他App有机会发声。

13.3 焦点变化通知:谁抢了我的焦点?

焦点变化通知是通过OnAudioFocusChangeListener回调来实现的。当焦点被抢占、释放或丢失时,系统会回调这个接口。

private AudioManager.OnAudioFocusChangeListener focusChangeListener =
        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;
        }
    }
};

你想想看,为什么要有这么多状态?

因为不同的场景需要不同的处理方式。比如导航语音播报时,音乐应该暂停(LOSS_TRANSIENT)。而微信消息提示音响起时,音乐降低音量就行(CAN_DUCK)。

避坑指南:

我曾经在AUDIOFOCUS_LOSS回调里直接调用了release()释放播放器资源。结果用户切回App时,播放器已经销毁了,需要重新创建。后来我改成只暂停播放,不释放资源,用户体验好了很多。

13.4 焦点与音量管理联动:闪避(Ducking)机制

焦点和音量管理是怎么联动的?最典型的例子就是闪避(Ducking)

当App申请AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK焦点时,系统会通知当前焦点持有者:“你可以降低音量继续播放,不用完全暂停”。

这个降低音量的幅度,系统默认是降低到原音量的30%左右。但具体数值因厂商而异。我见过有些手机厂商把这个值改成了50%,结果导航语音和音乐混在一起,根本听不清。

闪避的实现逻辑在AudioService里:

// AudioService.java 简化版
private void duckPlayers(IAudioFocus focusWinner) {
    // 遍历所有播放器
    for (PlayerRecord player : mPlayers) {
        if (player != focusWinner) {
            // 降低音量到0.3
            player.setVolume(0.3f);
        }
    }
}

嗯,这里要注意:闪避不是系统自动完成的。系统只负责通知,具体要不要闪避、闪避多少,由App自己决定。所以有些App不遵守规范,收到CAN_DUCK后根本不降低音量,导致用户体验很差。

警告:

不要依赖系统自动闪避。一定要在onAudioFocusChange回调里手动处理音量。我曾经见过一个App,收到CAN_DUCK后什么都不做,结果用户投诉“导航语音被音乐盖住了”。

13.5 焦点管理的整体流程

下面这张图展示了焦点管理的完整流程:

音频焦点管理流程 应用A(音乐) 应用B(导航) AudioService 焦点管理器 焦点栈 应用B(导航) 应用A(音乐) ① 申请焦点 ② 申请焦点 ③ 通知焦点丢失 ④ 降低音量(闪避) ⑤ 开始播放导航语音 申请焦点 通知/丢失

流程很简单:

  1. 应用A先申请焦点,成功持有
  2. 应用B申请焦点,系统检查焦点栈
  3. 系统通知应用A:你的焦点被抢了
  4. 应用A根据通知类型(这里是CAN_DUCK)降低音量
  5. 应用B获得焦点,开始播放

当应用B释放焦点后,系统会自动恢复应用A的焦点。应用A收到AUDIOFOCUS_GAIN回调,恢复音量继续播放。

个人经验:

我在做车载系统时,发现焦点管理特别重要。因为车载场景下,导航、音乐、电话、语音助手同时发声的情况很常见。我建议在申请焦点时,一定要明确指定AudioAttributesusage类型。比如导航用USAGE_ASSISTANCE_NAVIGATION_GUIDANCE,音乐用USAGE_MEDIA。这样系统才能做出正确的焦点决策。

13.6 总结

音频焦点管理,说白了就是一套“礼貌协议”。它让多个音频源能和谐共处,不会互相打架。

核心要点就三个:

  • 申请和释放要成对,别漏了
  • 回调处理要完整,别只处理GAIN不管LOSS
  • 闪避要主动做,别指望系统帮你降音量

嗯,这些经验都是我在实际项目中踩过坑才总结出来的。你们写代码时多留个心眼,就能少走很多弯路。


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