如何处理 AUDIOFOCUS_GAIN_TRANSIENT 和 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK?
解读
国内面试官问这道题,并不是想听你背官方文档,而是考察三件事:
- 是否真正在播放器、语音助手、车载导航等场景里踩过“抢焦点”的坑;
- 能否把“暂停/降低音量/恢复”做成可灰度、可配置、可监控的闭环;
- 是否了解 Android 10 以后“强制静音”与“隐私权限”对音频焦点的新限制。
一句话:要证明你既懂“规矩”(系统流程),也懂“变通”(业务体验)。
知识点
- 音频焦点三件套:请求(requestAudioFocus)、监听(AudioFocusCallback/Listener)、释放(abandonAudioFocus)。
- 8.0 以后必须用 AudioFocusRequest.Builder,显式传入 AudioAttributes 与 WillPauseWhenDucked 标志,否则系统直接拒掉。
- 两种瞬时焦点差异:
- TRANSIENT:对方要求“独占”,你必须立即暂停,并在失焦结束(GAIN)后主动恢复;
- TRANSIENT_MAY_DUCK:系统只让你“降低音量”,不应暂停,但国内 ROM(华米 OV)可能直接给你强制静音,需要兼容。
- duck 策略:推荐把音量压到 20 %~30 % 并关掉重低音,避免用户听不清导航提示;恢复时做 200 ms 淡入动画,防止“啪”一声。
- 生命周期联动:要在 onPause/onStop 里 abandon,防止应用退后台仍占焦点被系统拉黑名单;车载场景下还要监听 UXRestrictions(驾驶模式),禁止用户手动恢复。
- 灰度与监控:用 APM 埋点记录“请求→失焦→恢复”整条链路的耗时与失败码,国内厂商 ROM 经常返回 AUDIOFOCUS_REQUEST_FAILED(-1),需要降级到静音而不是崩溃。
- Android 12+ 新增“App 录音指示器”与“隐私看板”,若你的 duck 音量过低被用户投诉“偷录”,需要在设置里提供“忽略焦点”开关,否则应用商店审核会被卡。
答案
以在线音乐播放器为例,给出可直接落地的代码骨架与注意事项:
class PlayerRepository(
private val ctx: Context,
private val exoPlayer: ExoPlayer
) : AudioManager.OnAudioFocusChangeListener {
private val audioManager = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
private val focusLock = Any()
private var playbackDelayed = false
private var resumeOnFocusGain = false
private val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
)
.setWillPauseWhenDucked(true) // 8.0+ 必须显式声明
.setOnAudioFocusChangeListener(this)
.build()
fun play() {
synchronized(focusLock) {
val ret = audioManager.requestAudioFocus(focusRequest)
when (ret) {
AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> {
resumeOnFocusGain = false
exoPlayer.playWhenReady = true
}
AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> {
playbackDelayed = true
resumeOnFocusGain = false
}
else -> {
// 国内 ROM 常见 -1,直接静音提示用户
exoPlayer.volume = 0f
Toast.makeText(ctx, "与其他应用冲突,已静音", Toast.LENGTH_SHORT).show()
}
}
}
}
override fun onAudioFocusChange(focusChange: Int) {
synchronized(focusLock) {
when (focusChange) {
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
exoPlayer.playWhenReady = false
resumeOnFocusGain = true
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
// 降低音量,不暂停
exoPlayer.volume = 0.2f
}
AudioManager.AUDIOFOCUS_GAIN -> {
if (resumeOnFocusGain) {
exoPlayer.playWhenReady = true
resumeOnFocusGain = false
}
// 淡入恢复音量
ValueAnimator.ofFloat(exoPlayer.volume, 1f).apply {
duration = 200
addUpdateListener { exoPlayer.volume = it.animatedValue as Float }
start()
}
}
AudioManager.AUDIOFOCUS_LOSS -> {
abandonFocus()
exoPlayer.playWhenReady = false
}
}
}
}
fun abandonFocus() {
audioManager.abandonAudioFocusRequest(focusRequest)
}
}
使用要点:
- 在 Activity/Service onPause() 里一定调用 abandonFocus(),否则后台被系统拉黑,下次请求直接失败。
- 车载、手表等低内存设备上,把“音量降低”阈值做成云端配置,防止 ROM 差异导致体验不一致。
- 若业务允许后台播放,需启动前台服务并挂“媒体样式”通知,否则 Android 12 会抛出 ForegroundServiceStartNotAllowedException。
拓展思考
- 多音频会话场景:如果 App 内同时存在音乐、音效、TTS 三条播放轨道,建议为每条轨道独立申请 AudioFocusRequest,并设置不同的 AudioAttributes.usage,让系统帮你做“分层 duck”。例如 TTS 用 USAGE_ASSISTANCE_NAVIGATION_GUIDANCE,系统会强制压低音乐而不压低通知铃音。
- 与通话状态耦合:国内运营商定制机常把电话状态广播延迟 500 ms,导致焦点回调与 TelephonyCallback 不同步。稳妥做法是同时监听 AudioManager.MODE_IN_CALL,一旦进入通话模式立即 abandon,防止“电话挂断后音乐外放”社死现场。
- 折叠屏/多窗口:在分屏或浮窗模式下,系统只对“可见窗口”授予焦点。若用户把视频播一半拖到副屏,主屏打开抖音,你的焦点会被抢走;此时应在 onWindowFocusChanged 里重新请求,否则用户回切后无声。
- 隐私合规:工信部 164 号文要求“录音前需弹窗明示”。虽然焦点本身不录音,但部分安全键盘、语音助手在拿到 TRANSIENT 焦点后会立刻开始录音。若你的应用被投诉“诱导授权”,需要在上报日志里区分“因焦点而录音”与“用户主动点击录音”,否则应用市场会被下架。
把以上细节讲透,面试官基本会认定你“真的在一线项目里修过音频焦点的坑”,而不是背八股文。