深度解析 Android 音频焦点处理与实战开发

文章目录

    • 一、引言:音频焦点的本质
    • 二、整体架构概览
      • [2.1 各层职责](#2.1 各层职责)
    • 三、核心数据结构与焦点栈
      • [3.1 FocusRequester](#3.1 FocusRequester)
      • [3.2 焦点栈(FocusStack)](#3.2 焦点栈(FocusStack))
    • 四、音频焦点类型深度解析
      • [4.1 焦点请求类型(Gain Types)](#4.1 焦点请求类型(Gain Types))
      • [4.2 焦点丢失类型(Loss Types)](#4.2 焦点丢失类型(Loss Types))
      • [4.3 焦点交互的三种模式](#4.3 焦点交互的三种模式)
    • 五、源码深度分析
      • [5.1 焦点请求的完整调用链](#5.1 焦点请求的完整调用链)
      • [5.2 关键源码解析](#5.2 关键源码解析)
      • [5.3 MediaFocusControl 的仲裁逻辑](#5.3 MediaFocusControl 的仲裁逻辑)
    • [六、不同 Android 版本的行为差异](#六、不同 Android 版本的行为差异)
    • 七、闪避(Ducking)机制详解
      • [7.1 闪避的实现条件](#7.1 闪避的实现条件)
      • [7.2 闪避的强制执行](#7.2 闪避的强制执行)
    • 八、实战开发指南
      • [8.1 基本使用流程](#8.1 基本使用流程)
      • [8.2 代码示例(Android 8.0+)](#8.2 代码示例(Android 8.0+))
      • [8.3 最佳实践](#8.3 最佳实践)
      • [8.4 常见陷阱](#8.4 常见陷阱)
    • 九、车载场景(AAOS)的特殊考量
      • [9.1 CarAudioContext](#9.1 CarAudioContext)
      • [9.2 多区音频](#9.2 多区音频)
      • [9.3 并发交互的硬件考量](#9.3 并发交互的硬件考量)
    • 十、总结

一、引言:音频焦点的本质

在 Android 生态中,多个应用可能同时尝试向同一输出设备播放音频。音频焦点(Audio Focus)正是为解决这一冲突而生的系统级协调机制------它并非简单的"播放/暂停"开关,而是一套管理多个音频源对唯一物理音频输出设备并发访问权的系统级契约。

音频焦点机制自 Android 2.3(API 级别 8)引入,其核心目标是确保在任何时刻,用户听到的都是最符合其意图和上下文优先级的声音。理解这一本质,是构建高质量音频应用的起点。

二、整体架构概览

Android 音频焦点架构自上而下可分为应用层、系统服务层和硬件抽象层三个层次:
#mermaid-svg-zO8h2RRM9UTeZFxw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zO8h2RRM9UTeZFxw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zO8h2RRM9UTeZFxw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zO8h2RRM9UTeZFxw .error-icon{fill:#552222;}#mermaid-svg-zO8h2RRM9UTeZFxw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zO8h2RRM9UTeZFxw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zO8h2RRM9UTeZFxw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zO8h2RRM9UTeZFxw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zO8h2RRM9UTeZFxw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zO8h2RRM9UTeZFxw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zO8h2RRM9UTeZFxw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zO8h2RRM9UTeZFxw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zO8h2RRM9UTeZFxw .marker.cross{stroke:#333333;}#mermaid-svg-zO8h2RRM9UTeZFxw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zO8h2RRM9UTeZFxw p{margin:0;}#mermaid-svg-zO8h2RRM9UTeZFxw .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zO8h2RRM9UTeZFxw .cluster-label text{fill:#333;}#mermaid-svg-zO8h2RRM9UTeZFxw .cluster-label span{color:#333;}#mermaid-svg-zO8h2RRM9UTeZFxw .cluster-label span p{background-color:transparent;}#mermaid-svg-zO8h2RRM9UTeZFxw .label text,#mermaid-svg-zO8h2RRM9UTeZFxw span{fill:#333;color:#333;}#mermaid-svg-zO8h2RRM9UTeZFxw .node rect,#mermaid-svg-zO8h2RRM9UTeZFxw .node circle,#mermaid-svg-zO8h2RRM9UTeZFxw .node ellipse,#mermaid-svg-zO8h2RRM9UTeZFxw .node polygon,#mermaid-svg-zO8h2RRM9UTeZFxw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zO8h2RRM9UTeZFxw .rough-node .label text,#mermaid-svg-zO8h2RRM9UTeZFxw .node .label text,#mermaid-svg-zO8h2RRM9UTeZFxw .image-shape .label,#mermaid-svg-zO8h2RRM9UTeZFxw .icon-shape .label{text-anchor:middle;}#mermaid-svg-zO8h2RRM9UTeZFxw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zO8h2RRM9UTeZFxw .rough-node .label,#mermaid-svg-zO8h2RRM9UTeZFxw .node .label,#mermaid-svg-zO8h2RRM9UTeZFxw .image-shape .label,#mermaid-svg-zO8h2RRM9UTeZFxw .icon-shape .label{text-align:center;}#mermaid-svg-zO8h2RRM9UTeZFxw .node.clickable{cursor:pointer;}#mermaid-svg-zO8h2RRM9UTeZFxw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zO8h2RRM9UTeZFxw .arrowheadPath{fill:#333333;}#mermaid-svg-zO8h2RRM9UTeZFxw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zO8h2RRM9UTeZFxw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zO8h2RRM9UTeZFxw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zO8h2RRM9UTeZFxw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zO8h2RRM9UTeZFxw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zO8h2RRM9UTeZFxw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zO8h2RRM9UTeZFxw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zO8h2RRM9UTeZFxw .cluster text{fill:#333;}#mermaid-svg-zO8h2RRM9UTeZFxw .cluster span{color:#333;}#mermaid-svg-zO8h2RRM9UTeZFxw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zO8h2RRM9UTeZFxw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zO8h2RRM9UTeZFxw rect.text{fill:none;stroke-width:0;}#mermaid-svg-zO8h2RRM9UTeZFxw .icon-shape,#mermaid-svg-zO8h2RRM9UTeZFxw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zO8h2RRM9UTeZFxw .icon-shape p,#mermaid-svg-zO8h2RRM9UTeZFxw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zO8h2RRM9UTeZFxw .icon-shape .label rect,#mermaid-svg-zO8h2RRM9UTeZFxw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zO8h2RRM9UTeZFxw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zO8h2RRM9UTeZFxw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zO8h2RRM9UTeZFxw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 硬件抽象层
系统服务层
API 层
应用层
Binder IPC
焦点仲裁
音乐应用
导航应用
通话应用
AudioManager
AudioFocusRequest
OnAudioFocusChangeListener
AudioService
MediaFocusControl
FocusStack

焦点栈
FocusRequester

焦点请求者
AudioFlinger
混音引擎
输出设备

2.1 各层职责

  • 应用层:各类音频应用通过 AudioManager API 发起焦点请求和监听焦点变化。
  • API 层 :提供 requestAudioFocus()abandonAudioFocus() 等核心接口,以及 AudioFocusRequest 封装请求参数。
  • 系统服务层AudioService 作为系统服务接收 IPC 请求,MediaFocusControl 维护焦点栈并执行仲裁逻辑。
  • 硬件抽象层AudioFlinger 负责音频数据的混音和输出,接收来自焦点管理的闪避(ducking)指令。

三、核心数据结构与焦点栈

3.1 FocusRequester

FocusRequester 是系统中表示一个音频焦点使用者的核心数据结构。每个焦点请求都会在 MediaFocusControl 中创建一个 FocusRequester 实例,包含以下关键信息:

  • 客户端标识(clientId)
  • 请求的焦点类型(focusGain)
  • 音频属性(AudioAttributes)
  • 回调接口(IBinder 形式的 listener)
  • 调度器(AudioFocusDispatcher)

3.2 焦点栈(FocusStack)

音频焦点采用**栈(Stack)**数据结构进行管理。MediaFocusControl 维护一个 mFocusStack,所有持有或请求焦点的应用按顺序入栈,栈顶元素持有当前音频焦点
#mermaid-svg-h3lKW9nqtoi2DaBZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-h3lKW9nqtoi2DaBZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-h3lKW9nqtoi2DaBZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-h3lKW9nqtoi2DaBZ .error-icon{fill:#552222;}#mermaid-svg-h3lKW9nqtoi2DaBZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-h3lKW9nqtoi2DaBZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-h3lKW9nqtoi2DaBZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-h3lKW9nqtoi2DaBZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-h3lKW9nqtoi2DaBZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-h3lKW9nqtoi2DaBZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-h3lKW9nqtoi2DaBZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-h3lKW9nqtoi2DaBZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-h3lKW9nqtoi2DaBZ .marker.cross{stroke:#333333;}#mermaid-svg-h3lKW9nqtoi2DaBZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-h3lKW9nqtoi2DaBZ p{margin:0;}#mermaid-svg-h3lKW9nqtoi2DaBZ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-h3lKW9nqtoi2DaBZ .cluster-label text{fill:#333;}#mermaid-svg-h3lKW9nqtoi2DaBZ .cluster-label span{color:#333;}#mermaid-svg-h3lKW9nqtoi2DaBZ .cluster-label span p{background-color:transparent;}#mermaid-svg-h3lKW9nqtoi2DaBZ .label text,#mermaid-svg-h3lKW9nqtoi2DaBZ span{fill:#333;color:#333;}#mermaid-svg-h3lKW9nqtoi2DaBZ .node rect,#mermaid-svg-h3lKW9nqtoi2DaBZ .node circle,#mermaid-svg-h3lKW9nqtoi2DaBZ .node ellipse,#mermaid-svg-h3lKW9nqtoi2DaBZ .node polygon,#mermaid-svg-h3lKW9nqtoi2DaBZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-h3lKW9nqtoi2DaBZ .rough-node .label text,#mermaid-svg-h3lKW9nqtoi2DaBZ .node .label text,#mermaid-svg-h3lKW9nqtoi2DaBZ .image-shape .label,#mermaid-svg-h3lKW9nqtoi2DaBZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-h3lKW9nqtoi2DaBZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-h3lKW9nqtoi2DaBZ .rough-node .label,#mermaid-svg-h3lKW9nqtoi2DaBZ .node .label,#mermaid-svg-h3lKW9nqtoi2DaBZ .image-shape .label,#mermaid-svg-h3lKW9nqtoi2DaBZ .icon-shape .label{text-align:center;}#mermaid-svg-h3lKW9nqtoi2DaBZ .node.clickable{cursor:pointer;}#mermaid-svg-h3lKW9nqtoi2DaBZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-h3lKW9nqtoi2DaBZ .arrowheadPath{fill:#333333;}#mermaid-svg-h3lKW9nqtoi2DaBZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-h3lKW9nqtoi2DaBZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-h3lKW9nqtoi2DaBZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-h3lKW9nqtoi2DaBZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-h3lKW9nqtoi2DaBZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-h3lKW9nqtoi2DaBZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-h3lKW9nqtoi2DaBZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-h3lKW9nqtoi2DaBZ .cluster text{fill:#333;}#mermaid-svg-h3lKW9nqtoi2DaBZ .cluster span{color:#333;}#mermaid-svg-h3lKW9nqtoi2DaBZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-h3lKW9nqtoi2DaBZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-h3lKW9nqtoi2DaBZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-h3lKW9nqtoi2DaBZ .icon-shape,#mermaid-svg-h3lKW9nqtoi2DaBZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-h3lKW9nqtoi2DaBZ .icon-shape p,#mermaid-svg-h3lKW9nqtoi2DaBZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-h3lKW9nqtoi2DaBZ .icon-shape .label rect,#mermaid-svg-h3lKW9nqtoi2DaBZ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-h3lKW9nqtoi2DaBZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-h3lKW9nqtoi2DaBZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-h3lKW9nqtoi2DaBZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 焦点栈 (mFocusStack)
持有焦点
失去焦点
失去焦点
栈顶 (当前焦点持有者)

音乐应用 A
栈中

导航应用 B

(暂时失去焦点)
栈底

语音助手 C

(暂时失去焦点)
可以播放音频
暂停/闪避
暂停/闪避

栈管理的关键行为

  1. 新应用请求焦点成功 → 入栈成为新栈顶
  2. 原栈顶应用收到焦点丢失通知 → 根据丢失类型采取相应行动
  3. 栈顶应用主动放弃焦点(abandonAudioFocus())→ 出栈,原栈中下一个应用重新获得焦点
  4. 电话等系统级焦点具有特殊优先级,会插入到栈中特定位置

焦点栈 MediaFocusControl 应用 B (导航) 应用 A (音乐) 焦点栈 MediaFocusControl 应用 B (导航) 应用 A (音乐) #mermaid-svg-QV7tRPocX75M2xPu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QV7tRPocX75M2xPu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QV7tRPocX75M2xPu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QV7tRPocX75M2xPu .error-icon{fill:#552222;}#mermaid-svg-QV7tRPocX75M2xPu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QV7tRPocX75M2xPu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QV7tRPocX75M2xPu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QV7tRPocX75M2xPu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QV7tRPocX75M2xPu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QV7tRPocX75M2xPu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QV7tRPocX75M2xPu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QV7tRPocX75M2xPu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QV7tRPocX75M2xPu .marker.cross{stroke:#333333;}#mermaid-svg-QV7tRPocX75M2xPu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QV7tRPocX75M2xPu p{margin:0;}#mermaid-svg-QV7tRPocX75M2xPu .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QV7tRPocX75M2xPu text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-QV7tRPocX75M2xPu .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-QV7tRPocX75M2xPu .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-QV7tRPocX75M2xPu .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-QV7tRPocX75M2xPu .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-QV7tRPocX75M2xPu #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-QV7tRPocX75M2xPu .sequenceNumber{fill:white;}#mermaid-svg-QV7tRPocX75M2xPu #sequencenumber{fill:#333;}#mermaid-svg-QV7tRPocX75M2xPu #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-QV7tRPocX75M2xPu .messageText{fill:#333;stroke:none;}#mermaid-svg-QV7tRPocX75M2xPu .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QV7tRPocX75M2xPu .labelText,#mermaid-svg-QV7tRPocX75M2xPu .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-QV7tRPocX75M2xPu .loopText,#mermaid-svg-QV7tRPocX75M2xPu .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-QV7tRPocX75M2xPu .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-QV7tRPocX75M2xPu .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-QV7tRPocX75M2xPu .noteText,#mermaid-svg-QV7tRPocX75M2xPu .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-QV7tRPocX75M2xPu .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QV7tRPocX75M2xPu .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QV7tRPocX75M2xPu .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QV7tRPocX75M2xPu .actorPopupMenu{position:absolute;}#mermaid-svg-QV7tRPocX75M2xPu .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-QV7tRPocX75M2xPu .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QV7tRPocX75M2xPu .actor-man circle,#mermaid-svg-QV7tRPocX75M2xPu line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-QV7tRPocX75M2xPu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 初始状态:A 持有焦点 B 播放导航语音 A requestAudioFocus(TRANSIENT_MAY_DUCK) 将 B 入栈 A, B (B 为栈顶) onAudioFocusChange(LOSS_TRANSIENT_CAN_DUCK) AUDIOFOCUS_REQUEST_GRANTED abandonAudioFocus() B 出栈 A (A 重新成为栈顶) onAudioFocusChange(GAIN)

四、音频焦点类型深度解析

4.1 焦点请求类型(Gain Types)

音频焦点类型的设计体现了对不同使用场景的精细区分:

请求类型 常量值 设计意图 典型场景 对其他应用的影响
AUDIOFOCUS_GAIN 1 长期、独占的焦点 用户主动播放音乐/视频 其他应用应停止播放并释放资源
AUDIOFOCUS_GAIN_TRANSIENT 2 短暂独占焦点 语音提醒、闹钟、通知音 其他应用应暂停播放,准备快速恢复
AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 3 短暂焦点,允许闪避 GPS 导航提示、IM 消息提示 其他应用降低音量(Ducking)
AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE 4 短暂、完全独占 录音场景 其他应用完全静音

4.2 焦点丢失类型(Loss Types)

焦点丢失类型与请求类型严格对应------后一个应用以何种方式获得焦点,前一个应用就会收到相应类型的丢失通知:

  • AUDIOFOCUS_LOSS:对应 GAIN,长期丢失,应停止播放并释放资源
  • AUDIOFOCUS_LOSS_TRANSIENT:对应 GAIN_TRANSIENT,短暂丢失,应暂停播放
  • AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:对应 GAIN_TRANSIENT_MAY_DUCK,短暂丢失但可闪避

4.3 焦点交互的三种模式

Android 系统根据请求方与当前焦点持有者的上下文,决定三种交互模式:
#mermaid-svg-ONoYcrid478CsAJl{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ONoYcrid478CsAJl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ONoYcrid478CsAJl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ONoYcrid478CsAJl .error-icon{fill:#552222;}#mermaid-svg-ONoYcrid478CsAJl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ONoYcrid478CsAJl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ONoYcrid478CsAJl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ONoYcrid478CsAJl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ONoYcrid478CsAJl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ONoYcrid478CsAJl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ONoYcrid478CsAJl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ONoYcrid478CsAJl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ONoYcrid478CsAJl .marker.cross{stroke:#333333;}#mermaid-svg-ONoYcrid478CsAJl svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ONoYcrid478CsAJl p{margin:0;}#mermaid-svg-ONoYcrid478CsAJl .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ONoYcrid478CsAJl .cluster-label text{fill:#333;}#mermaid-svg-ONoYcrid478CsAJl .cluster-label span{color:#333;}#mermaid-svg-ONoYcrid478CsAJl .cluster-label span p{background-color:transparent;}#mermaid-svg-ONoYcrid478CsAJl .label text,#mermaid-svg-ONoYcrid478CsAJl span{fill:#333;color:#333;}#mermaid-svg-ONoYcrid478CsAJl .node rect,#mermaid-svg-ONoYcrid478CsAJl .node circle,#mermaid-svg-ONoYcrid478CsAJl .node ellipse,#mermaid-svg-ONoYcrid478CsAJl .node polygon,#mermaid-svg-ONoYcrid478CsAJl .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ONoYcrid478CsAJl .rough-node .label text,#mermaid-svg-ONoYcrid478CsAJl .node .label text,#mermaid-svg-ONoYcrid478CsAJl .image-shape .label,#mermaid-svg-ONoYcrid478CsAJl .icon-shape .label{text-anchor:middle;}#mermaid-svg-ONoYcrid478CsAJl .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ONoYcrid478CsAJl .rough-node .label,#mermaid-svg-ONoYcrid478CsAJl .node .label,#mermaid-svg-ONoYcrid478CsAJl .image-shape .label,#mermaid-svg-ONoYcrid478CsAJl .icon-shape .label{text-align:center;}#mermaid-svg-ONoYcrid478CsAJl .node.clickable{cursor:pointer;}#mermaid-svg-ONoYcrid478CsAJl .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ONoYcrid478CsAJl .arrowheadPath{fill:#333333;}#mermaid-svg-ONoYcrid478CsAJl .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ONoYcrid478CsAJl .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ONoYcrid478CsAJl .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ONoYcrid478CsAJl .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ONoYcrid478CsAJl .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ONoYcrid478CsAJl .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ONoYcrid478CsAJl .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ONoYcrid478CsAJl .cluster text{fill:#333;}#mermaid-svg-ONoYcrid478CsAJl .cluster span{color:#333;}#mermaid-svg-ONoYcrid478CsAJl div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ONoYcrid478CsAJl .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ONoYcrid478CsAJl rect.text{fill:none;stroke-width:0;}#mermaid-svg-ONoYcrid478CsAJl .icon-shape,#mermaid-svg-ONoYcrid478CsAJl .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ONoYcrid478CsAJl .icon-shape p,#mermaid-svg-ONoYcrid478CsAJl .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ONoYcrid478CsAJl .icon-shape .label rect,#mermaid-svg-ONoYcrid478CsAJl .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ONoYcrid478CsAJl .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ONoYcrid478CsAJl .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ONoYcrid478CsAJl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 独占
拒绝
并发
焦点请求
交互判定
独占交互

新请求获得焦点

原持有者失去焦点
拒绝交互

请求被拒绝

原持有者不变
并发交互

同时持有焦点

可能触发闪避
🎵 音乐 → 🎵 音乐
📞 通话 → 🎵 音乐
🗺️ 导航 → 🎵 音乐

(满足特定条件)

独占交互(Exclusive):一次只允许一个应用持有焦点。新请求被授予焦点的同时,现有焦点持有者失去焦点。这是 Android 中最常见的交互模型。

拒绝交互(Reject) :传入请求一律被拒绝。典型场景是通话过程中音乐应用请求焦点------拨号器持有通话焦点时,音乐应用的请求会收到 AUDIOFOCUS_REQUEST_FAILED

并发交互(Concurrent):AAOS(Android Automotive OS)特有的交互模式。允许车载应用与其他应用同时持有焦点,但需满足以下条件:

  • 传入请求必须为 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
  • 当前焦点持有者未设置 setPauseWhenDucked(true)
  • 当前焦点持有者选择不接收闪避事件

五、源码深度分析

5.1 焦点请求的完整调用链

#mermaid-svg-djfHHCoCZlCls309{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-djfHHCoCZlCls309 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-djfHHCoCZlCls309 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-djfHHCoCZlCls309 .error-icon{fill:#552222;}#mermaid-svg-djfHHCoCZlCls309 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-djfHHCoCZlCls309 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-djfHHCoCZlCls309 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-djfHHCoCZlCls309 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-djfHHCoCZlCls309 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-djfHHCoCZlCls309 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-djfHHCoCZlCls309 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-djfHHCoCZlCls309 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-djfHHCoCZlCls309 .marker.cross{stroke:#333333;}#mermaid-svg-djfHHCoCZlCls309 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-djfHHCoCZlCls309 p{margin:0;}#mermaid-svg-djfHHCoCZlCls309 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-djfHHCoCZlCls309 .cluster-label text{fill:#333;}#mermaid-svg-djfHHCoCZlCls309 .cluster-label span{color:#333;}#mermaid-svg-djfHHCoCZlCls309 .cluster-label span p{background-color:transparent;}#mermaid-svg-djfHHCoCZlCls309 .label text,#mermaid-svg-djfHHCoCZlCls309 span{fill:#333;color:#333;}#mermaid-svg-djfHHCoCZlCls309 .node rect,#mermaid-svg-djfHHCoCZlCls309 .node circle,#mermaid-svg-djfHHCoCZlCls309 .node ellipse,#mermaid-svg-djfHHCoCZlCls309 .node polygon,#mermaid-svg-djfHHCoCZlCls309 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-djfHHCoCZlCls309 .rough-node .label text,#mermaid-svg-djfHHCoCZlCls309 .node .label text,#mermaid-svg-djfHHCoCZlCls309 .image-shape .label,#mermaid-svg-djfHHCoCZlCls309 .icon-shape .label{text-anchor:middle;}#mermaid-svg-djfHHCoCZlCls309 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-djfHHCoCZlCls309 .rough-node .label,#mermaid-svg-djfHHCoCZlCls309 .node .label,#mermaid-svg-djfHHCoCZlCls309 .image-shape .label,#mermaid-svg-djfHHCoCZlCls309 .icon-shape .label{text-align:center;}#mermaid-svg-djfHHCoCZlCls309 .node.clickable{cursor:pointer;}#mermaid-svg-djfHHCoCZlCls309 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-djfHHCoCZlCls309 .arrowheadPath{fill:#333333;}#mermaid-svg-djfHHCoCZlCls309 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-djfHHCoCZlCls309 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-djfHHCoCZlCls309 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-djfHHCoCZlCls309 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-djfHHCoCZlCls309 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-djfHHCoCZlCls309 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-djfHHCoCZlCls309 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-djfHHCoCZlCls309 .cluster text{fill:#333;}#mermaid-svg-djfHHCoCZlCls309 .cluster span{color:#333;}#mermaid-svg-djfHHCoCZlCls309 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-djfHHCoCZlCls309 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-djfHHCoCZlCls309 rect.text{fill:none;stroke-width:0;}#mermaid-svg-djfHHCoCZlCls309 .icon-shape,#mermaid-svg-djfHHCoCZlCls309 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-djfHHCoCZlCls309 .icon-shape p,#mermaid-svg-djfHHCoCZlCls309 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-djfHHCoCZlCls309 .icon-shape .label rect,#mermaid-svg-djfHHCoCZlCls309 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-djfHHCoCZlCls309 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-djfHHCoCZlCls309 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-djfHHCoCZlCls309 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} MediaFocusControl
AudioService (系统服务)
AudioManager
应用层
Binder IPC
app.requestAudioFocus()
requestAudioFocus(AudioFocusRequest)
requestAudioFocus(AFR, AudioPolicy)
registerAudioFocusRequest()
getService()
service.requestAudioFocus()
requestAudioFocus()
焦点栈操作
通知焦点变化

5.2 关键源码解析

入口:AudioManager.requestAudioFocus()

从 Android 8.0(API 级别 26)开始,必须使用 AudioFocusRequest 参数:

java 复制代码
public int requestAudioFocus(@NonNull AudioFocusRequest focusRequest) {
    return requestAudioFocus(focusRequest, null /* no AudioPolicy */);
}

第二个参数 AudioPolicy@SystemApi,允许将焦点优先级处理逻辑外置,而非使用系统默认逻辑。

注册与校验

java 复制代码
public int requestAudioFocus(@NonNull AudioFocusRequest afr, @Nullable AudioPolicy ap) {
    // 如果设置 locksFocus 为 true,必须指定 AudioPolicy
    if (afr.locksFocus() && ap == null) {
        throw new IllegalArgumentException("Illegal null audio policy when locking audio focus");
    }
    // 注册焦点请求信息
    registerAudioFocusRequest(afr);
    // 获取 AudioService 的 Binder 代理
    final IAudioService service = getService();
    // 将 listener 转换为 clientId
    final String clientId = getIdForAudioFocusListener(afr.getOnAudioFocusChangeListener());
    // 调用 AudioService
    status = service.requestAudioFocus(afr.getAudioAttributes(), afr.getFocusGain(),
            mICallBack, mAudioFocusDispatcher, clientId, ...);
}

核心要点

  1. AudioFocusRequest 封装 :从 8.0 开始,谷歌将申请音源的信息封装为 AudioFocusRequest 类,随着功能迭代日益丰富。
  2. Binder 跨进程调用getService() 获取 AudioService 的 Binder 代理,通过 IPC 将请求传递到系统服务进程。
  3. 外部策略支持AudioPolicy 参数允许将焦点处理逻辑外置,AUDIOFOCUS_REQUEST_WAITING_FOR_EXT_POLICY 状态表示等待外部策略决策。

5.3 MediaFocusControl 的仲裁逻辑

MediaFocusControl 是焦点管理的核心类,位于 frameworks/base/services/core/java/com/android/server/audio/MediaFocusControl.java

其主要职责包括:

  • 维护 mFocusStack 焦点栈
  • 执行焦点请求的仲裁(grant/reject/duck)
  • 分发焦点变化通知给相关应用
  • 管理闪避(ducking)的强制执行

六、不同 Android 版本的行为差异

音频焦点的管理方式随 Android 版本演进发生了显著变化:

Android 版本 焦点管理方式 关键特性
≤ 7.1 (API 25) 应用自主管理 系统不强制,依赖应用自觉遵守
8.0--11 (API 26--30) 应用自主管理 引入 AudioFocusRequest,提供更丰富的 API
≥ 12 (API 31+) 系统强制管理 系统自动淡出(fade out)失去焦点的应用

Android 12 及更高版本的关键变化

当一个应用在另一个应用拥有焦点且正在播放时请求音频焦点,系统会强制正在播放的应用淡出(fade out)。系统还会在收到来电时将音频播放设为静音。

这一变化意味着音频焦点从"建议性契约"升级为"强制性规则",应用开发者必须更加重视焦点管理。

七、闪避(Ducking)机制详解

闪避是音频焦点机制中提升用户体验的重要设计------当导航提示或通知音短暂出现时,背景音乐暂时降低音量而非完全停止。

7.1 闪避的实现条件

闪避的触发需要满足以下条件:

  • 新请求使用 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
  • 当前焦点持有者未设置 setPauseWhenDucked(true)
  • 当前焦点持有者选择接收闪避事件

7.2 闪避的强制执行

从 Android 某些版本开始,框架自身可以强制执行闪避(ducking)。系统使用 VolumeShaper 来实现音量的平滑降低和恢复。需要注意的是:

  • 如果播放内容是语音(speech)或使用 SoundPool,系统不会强制执行闪避,而是留给应用自行处理
  • 当应用重新获得焦点时,系统会确保其从闪避状态中恢复(unduck)

#mermaid-svg-FojZNRdGxKnwGgiG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FojZNRdGxKnwGgiG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FojZNRdGxKnwGgiG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FojZNRdGxKnwGgiG .error-icon{fill:#552222;}#mermaid-svg-FojZNRdGxKnwGgiG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FojZNRdGxKnwGgiG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FojZNRdGxKnwGgiG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FojZNRdGxKnwGgiG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FojZNRdGxKnwGgiG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FojZNRdGxKnwGgiG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FojZNRdGxKnwGgiG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FojZNRdGxKnwGgiG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FojZNRdGxKnwGgiG .marker.cross{stroke:#333333;}#mermaid-svg-FojZNRdGxKnwGgiG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FojZNRdGxKnwGgiG p{margin:0;}#mermaid-svg-FojZNRdGxKnwGgiG .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-FojZNRdGxKnwGgiG .cluster-label text{fill:#333;}#mermaid-svg-FojZNRdGxKnwGgiG .cluster-label span{color:#333;}#mermaid-svg-FojZNRdGxKnwGgiG .cluster-label span p{background-color:transparent;}#mermaid-svg-FojZNRdGxKnwGgiG .label text,#mermaid-svg-FojZNRdGxKnwGgiG span{fill:#333;color:#333;}#mermaid-svg-FojZNRdGxKnwGgiG .node rect,#mermaid-svg-FojZNRdGxKnwGgiG .node circle,#mermaid-svg-FojZNRdGxKnwGgiG .node ellipse,#mermaid-svg-FojZNRdGxKnwGgiG .node polygon,#mermaid-svg-FojZNRdGxKnwGgiG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FojZNRdGxKnwGgiG .rough-node .label text,#mermaid-svg-FojZNRdGxKnwGgiG .node .label text,#mermaid-svg-FojZNRdGxKnwGgiG .image-shape .label,#mermaid-svg-FojZNRdGxKnwGgiG .icon-shape .label{text-anchor:middle;}#mermaid-svg-FojZNRdGxKnwGgiG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-FojZNRdGxKnwGgiG .rough-node .label,#mermaid-svg-FojZNRdGxKnwGgiG .node .label,#mermaid-svg-FojZNRdGxKnwGgiG .image-shape .label,#mermaid-svg-FojZNRdGxKnwGgiG .icon-shape .label{text-align:center;}#mermaid-svg-FojZNRdGxKnwGgiG .node.clickable{cursor:pointer;}#mermaid-svg-FojZNRdGxKnwGgiG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-FojZNRdGxKnwGgiG .arrowheadPath{fill:#333333;}#mermaid-svg-FojZNRdGxKnwGgiG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-FojZNRdGxKnwGgiG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-FojZNRdGxKnwGgiG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FojZNRdGxKnwGgiG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FojZNRdGxKnwGgiG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FojZNRdGxKnwGgiG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-FojZNRdGxKnwGgiG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-FojZNRdGxKnwGgiG .cluster text{fill:#333;}#mermaid-svg-FojZNRdGxKnwGgiG .cluster span{color:#333;}#mermaid-svg-FojZNRdGxKnwGgiG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-FojZNRdGxKnwGgiG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FojZNRdGxKnwGgiG rect.text{fill:none;stroke-width:0;}#mermaid-svg-FojZNRdGxKnwGgiG .icon-shape,#mermaid-svg-FojZNRdGxKnwGgiG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FojZNRdGxKnwGgiG .icon-shape p,#mermaid-svg-FojZNRdGxKnwGgiG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-FojZNRdGxKnwGgiG .icon-shape .label rect,#mermaid-svg-FojZNRdGxKnwGgiG .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FojZNRdGxKnwGgiG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-FojZNRdGxKnwGgiG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-FojZNRdGxKnwGgiG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 导航请求焦点
导航释放焦点
闪避后
🎵 音乐播放

音量 100%
闪避中
🗺️ 导航提示播放
🎵 音乐播放

音量 30%
闪避前
🎵 音乐播放

音量 100%

八、实战开发指南

8.1 基本使用流程

#mermaid-svg-NmHrmCL1Yj2Cz83W{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NmHrmCL1Yj2Cz83W .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NmHrmCL1Yj2Cz83W .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NmHrmCL1Yj2Cz83W .error-icon{fill:#552222;}#mermaid-svg-NmHrmCL1Yj2Cz83W .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NmHrmCL1Yj2Cz83W .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NmHrmCL1Yj2Cz83W .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NmHrmCL1Yj2Cz83W .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NmHrmCL1Yj2Cz83W .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NmHrmCL1Yj2Cz83W .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NmHrmCL1Yj2Cz83W .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NmHrmCL1Yj2Cz83W .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NmHrmCL1Yj2Cz83W .marker.cross{stroke:#333333;}#mermaid-svg-NmHrmCL1Yj2Cz83W svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NmHrmCL1Yj2Cz83W p{margin:0;}#mermaid-svg-NmHrmCL1Yj2Cz83W .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NmHrmCL1Yj2Cz83W .cluster-label text{fill:#333;}#mermaid-svg-NmHrmCL1Yj2Cz83W .cluster-label span{color:#333;}#mermaid-svg-NmHrmCL1Yj2Cz83W .cluster-label span p{background-color:transparent;}#mermaid-svg-NmHrmCL1Yj2Cz83W .label text,#mermaid-svg-NmHrmCL1Yj2Cz83W span{fill:#333;color:#333;}#mermaid-svg-NmHrmCL1Yj2Cz83W .node rect,#mermaid-svg-NmHrmCL1Yj2Cz83W .node circle,#mermaid-svg-NmHrmCL1Yj2Cz83W .node ellipse,#mermaid-svg-NmHrmCL1Yj2Cz83W .node polygon,#mermaid-svg-NmHrmCL1Yj2Cz83W .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NmHrmCL1Yj2Cz83W .rough-node .label text,#mermaid-svg-NmHrmCL1Yj2Cz83W .node .label text,#mermaid-svg-NmHrmCL1Yj2Cz83W .image-shape .label,#mermaid-svg-NmHrmCL1Yj2Cz83W .icon-shape .label{text-anchor:middle;}#mermaid-svg-NmHrmCL1Yj2Cz83W .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NmHrmCL1Yj2Cz83W .rough-node .label,#mermaid-svg-NmHrmCL1Yj2Cz83W .node .label,#mermaid-svg-NmHrmCL1Yj2Cz83W .image-shape .label,#mermaid-svg-NmHrmCL1Yj2Cz83W .icon-shape .label{text-align:center;}#mermaid-svg-NmHrmCL1Yj2Cz83W .node.clickable{cursor:pointer;}#mermaid-svg-NmHrmCL1Yj2Cz83W .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NmHrmCL1Yj2Cz83W .arrowheadPath{fill:#333333;}#mermaid-svg-NmHrmCL1Yj2Cz83W .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NmHrmCL1Yj2Cz83W .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NmHrmCL1Yj2Cz83W .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NmHrmCL1Yj2Cz83W .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NmHrmCL1Yj2Cz83W .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NmHrmCL1Yj2Cz83W .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NmHrmCL1Yj2Cz83W .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NmHrmCL1Yj2Cz83W .cluster text{fill:#333;}#mermaid-svg-NmHrmCL1Yj2Cz83W .cluster span{color:#333;}#mermaid-svg-NmHrmCL1Yj2Cz83W div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NmHrmCL1Yj2Cz83W .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NmHrmCL1Yj2Cz83W rect.text{fill:none;stroke-width:0;}#mermaid-svg-NmHrmCL1Yj2Cz83W .icon-shape,#mermaid-svg-NmHrmCL1Yj2Cz83W .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NmHrmCL1Yj2Cz83W .icon-shape p,#mermaid-svg-NmHrmCL1Yj2Cz83W .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NmHrmCL1Yj2Cz83W .icon-shape .label rect,#mermaid-svg-NmHrmCL1Yj2Cz83W .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NmHrmCL1Yj2Cz83W .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NmHrmCL1Yj2Cz83W .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NmHrmCL1Yj2Cz83W :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

LOSS
LOSS_TRANSIENT
LOSS_TRANSIENT_CAN_DUCK
GAIN
开始播放音频
requestAudioFocus
请求成功?
开始播放
等待或放弃
监听焦点变化
焦点变化事件
停止播放 + abandonAudioFocus
暂停播放
降低音量
恢复播放/音量
结束

8.2 代码示例(Android 8.0+)

步骤一:创建 AudioFocusRequest

java 复制代码
AudioAttributes audioAttributes = new AudioAttributes.Builder()
        .setUsage(AudioAttributes.USAGE_MEDIA)
        .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
        .build();

AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
        .setAudioAttributes(audioAttributes)
        .setOnAudioFocusChangeListener(focusChangeListener)
        .setWillPauseWhenDucked(false)  // 闪避时是否暂停而非降低音量
        .build();

步骤二:请求焦点

java 复制代码
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
int result = audioManager.requestAudioFocus(focusRequest);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    // 获得焦点,开始播放
    startPlayback();
}

步骤三:实现焦点变化监听

java 复制代码
private final 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();
                abandonAudioFocus();
                break;
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                // 短暂丢失,暂停播放
                pausePlayback();
                break;
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                // 短暂丢失但可闪避,降低音量
                duckPlayback();
                break;
        }
    }
};

步骤四:放弃焦点

java 复制代码
public void onStop() {
    audioManager.abandonAudioFocusRequest(focusRequest);
}

8.3 最佳实践

  1. 在播放前请求焦点 :在 onPlay() 回调中调用 requestAudioFocus(),验证返回 AUDIOFOCUS_REQUEST_GRANTED 后才开始播放。

  2. 正确响应焦点变化 :根据丢失类型采取精确行动------LOSS 应停止并释放,LOSS_TRANSIENT 应暂停,LOSS_TRANSIENT_CAN_DUCK 应降低音量。

  3. 及时放弃焦点 :当播放停止且没有后续内容时,调用 abandonAudioFocus() 释放焦点。用户暂停但可能恢复时,不必放弃焦点。

  4. 使用 AudioAttributes :准确描述音频类型(如 CONTENT_TYPE_SPEECH 用于语音),帮助系统做出更合理的焦点决策。

  5. 处理焦点请求被拒绝 :当 requestAudioFocus() 返回 AUDIOFOCUS_REQUEST_FAILED 时,应用应保持静默,可延迟到获得焦点后再播放。

8.4 常见陷阱

陷阱一:在子线程中处理焦点回调

OnAudioFocusChangeListener 的回调可能在不同线程中执行。如果回调中直接操作 UI 或播放器,需要确保线程安全。

陷阱二:忽略焦点丢失后继续播放

在 Android 11 及以下版本,系统不会强制停止失去焦点的应用。但如果应用忽略焦点丢失继续大声播放,会导致糟糕的用户体验,用户很可能卸载该应用。

陷阱三:焦点类型选择不当

  • 音乐播放应使用 AUDIOFOCUS_GAIN,而非 TRANSIENT
  • 导航提示应使用 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK,使背景音乐能闪避
  • 录音应使用 AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE,确保完全独占

九、车载场景(AAOS)的特殊考量

Android Automotive OS(AAOS)在标准音频焦点机制之上进行了扩展:

9.1 CarAudioContext

AAOS 将音频用途归类为 CarAudioContext,用于定义路由、音量组、音频焦点和闪避管理。系统根据请求方和当前焦点持有者的 CarAudioContext 之间的预定义交互矩阵来处理焦点请求。

9.2 多区音频

AAOS 支持多区音频(Multi-zone Audio),不同区域(如驾驶区、乘客区)可独立管理音频焦点和音量。乘客可以聆听自己的音频,同时驾驶员收听主音频区的另一个音源。

9.3 并发交互的硬件考量

并发交互虽然允许多个应用同时持有焦点,但 OEM 必须在硬件层面实现混音和闪避。建议将可并发播放的 CarAudioContext 路由到不同的输出设备,使 HAL 能够在混音前对某一声音流进行闪避,或将物理声音流路由到车辆中不同的音响设备。

十、总结

Android 音频焦点机制从 Android 2.3 的"建议性契约"发展到 Android 12 的"强制性规则",其演进反映了移动操作系统对用户体验日益增长的重视。理解这一机制的核心------焦点栈管理、四种焦点类型的语义差异、三种交互模式以及闪避机制------是构建高质量音频应用的基础。

对于开发者而言,正确的焦点管理不仅是技术实现,更是对用户听觉体验的尊重。在复杂的多应用音频生态中,遵循焦点契约的应用将成为秩序的维护者,而非混乱的制造者。

相关推荐
程序员老陆6 小时前
使用Qt+大华设备SDK预览大华摄像机视频
qt·算法·音视频·视频监控
byte轻骑兵6 小时前
【LE Audio】CSIS精讲[4]: 从加密原语到RSI解析全链路拆解
音视频·人机交互·蓝牙耳机·le audio·低功耗蓝牙音频
祉猷并茂,雯华若锦7 小时前
Appium 3.x安卓APP企业级安装实战
android·appium
EasyDSS7 小时前
视频直播点播/音视频点播/云点播/云直播EasyDSS一站式音视频平台赋能社交娱乐场景创新
音视频·娱乐
微露清风8 小时前
浅析 MySQL 索引
android·数据库·mysql
91刘仁德8 小时前
MySQL 数据类型详解
android·笔记·mysql·adb
编程圈子9 小时前
操作系统学习14 IDT + PIC + 键盘中断
android·学习·计算机外设
齊家治國平天下16 小时前
Android 14 Vehicle HAL (VHAL) 设计与实现深度解析
android·aaos·carservice·车载开发·android 14·vhal·vehicle hal
CYB353218 小时前
R&S 罗德与施瓦茨 R&S VTC-VTE-VTS 视频测试仪
音视频