1. 先搞清楚AudioAttributes到底是个啥
1.1 为什么Android要规定音频属性
做Android音频开发的人,不管你是做播放器、录音、语音通话还是铃声定制,迟早都会跟AudioAttributes打交道。这个类从API 21开始引入,Android 5.0之后系统音频架构大规模重构,它成了几乎所有音频链路入口的统一配置项。哪怕是API 36也就是Android 16,这套体系依然在沿用,只是底层策略更加细化。
很多初学者容易把它当成一个可有可无的配置,觉得“不设置也能播放啊”。没错,你不设置系统会用默认值,但后果就是:音量调节走的是媒体流、焦点申请用的是默认类型、铃声播放可能被其他媒体应用抢占。最直观的例子,你做一个语音聊天App,如果不明确指定通话场景,系统可能把音频焦点跟音乐播放混为一谈,对方一放歌你的通话音量就被压低了。
AudioAttributes本质上就是把你这段音频的“身份标签”告诉系统:这段声音是什么类型的、用途是什么、优先级如何。系统再根据这些标签决定音量曲线、路由策略、音频焦点交互、音效处理等行为。
1.2 音频属性里的几个关键维度
AudioAttributes里面有三个字段最核心:contentType(内容类型)、usage(使用场景)、flags(标志位)。这三个字段各管各的,但组合起来才是一个完整的属性描述。
- contentType:说明这段音频的内容本质是什么,是人声、音乐、音效还是未知。它影响的是音频处理策略,比如是否要经过回声消除、是否走语音处理链路。
- usage:说明用户用这段音频做什么,是媒体播放、通话、闹钟还是导航。它直接决定音量流(STREAM)的归属。
- flags:一些特殊标志,比如音频需要独占硬件、需要低延迟等,一般用得比较少。
说得直白一点,contentType是你告诉系统“这段音频长什么样”,usage是你告诉系统“这段音频拿来干嘛”。两者分工不同,但又互相配合。setContentType只是设置其中之一,你还需要配合setUsage一起用才能达到预期的系统行为。
有一点必须强调:如果你的targetSdkVersion比较高,Android对音频属性校验会更严格。Android 16上,AudioAttributes的属性组合如果自相矛盾,系统会直接抛IllegalArgumentException。这个问题我后面会详细讲。
2. setContentType到底在设置什么
2.1 contentType的可选值及各自语义
看源码就知道,AudioAttributes类里预定义了一组contentType常量,咱们一个一个过:
- CONTENT_TYPE_UNKNOWN(值0):未知类型。系统会按照通用处理,音量、路由都比较保守,适合“还没想好是什么音频”或者混合类型的场景。
- CONTENT_TYPE_SPEECH(值1):语音,典型的人声内容。像通话、录音、语音消息、有声书朗读都适合用这个。系统很可能启用语音处理链路,包括降噪、回声消除等。但注意,不是所有设备都启用了这些处理,具体还要看设备实现。
- CONTENT_TYPE_MUSIC(值2):音乐。最常见的类型,绝大多数媒体播放器都用这个。系统一般不会做额外处理,保留原始波形,适合听歌看视频。
- CONTENT_TYPE_MOVIE(值3):电影伴音。这种场景通常包含对白、音效、背景音乐混合,系统可能倾向更宽的频响和动态范围,部分设备上会启用环绕声或虚拟化处理。
- CONTENT_TYPE_SONIFICATION(值4):提示音,比如按键音、通知音、闹钟提示。这类声音通常较短、需要清晰可辨,系统可能会走独立的提示音管理链路。
除了这四个常规值,还有一个已废弃的CONTENT_TYPE_EMERGENCY(值5)和CONTENT_TYPE_DSD(值6),以及API 34引入的CONTENT_TYPE_ULTRASOUND(值7)。后两者基本属于特定硬件场景,我们可以暂时只关注前五个。
这个表收藏好,省得每次都翻文档:
| contentType常量 | 值 | 典型场景 | 系统行为倾向 |
|---|---|---|---|
| CONTENT_TYPE_UNKNOWN | 0 | 混合内容、不确定场景 | 默认处理,无特殊优化 |
| CONTENT_TYPE_SPEECH | 1 | 通话、录音、语音消息 | 语音链路,可能降噪/AEC |
| CONTENT_TYPE_MUSIC | 2 | 音乐播放、视频播放 | 高保真优先,少处理 |
| CONTENT_TYPE_MOVIE | 3 | 电影伴音、流媒体影片 | 宽频响、可能空间音频 |
| CONTENT_TYPE_SONIFICATION | 4 | 通知音、闹钟、按键音 | 提示音链路,清晰优先 |
2.2 不同的contentType对系统行为的影响
很多人以为contentType只影响音质或者声音处理,其实不止。它对系统行为的改变覆盖了好几个层面。
第一个层面是声音处理链路。拿SPEECH来说,如果你的设备开启了语音通话降噪,系统在检测到SPEECH类型时会自动把降噪算法挂到音频链路上。如果你设置的是MUSIC,那就算硬件有降噪能力,系统通常也不会启用,因为音乐信号不适合做语音的降噪处理,容易把音乐细节削掉。
第二个层面是焦点交互。当音频焦点发生冲突时,系统会根据双方的contentType和usage来决定谁可以继续播放、谁需要暂停或者闪避(duck)。比如一个MUSIC类型的应用在播放,这时一个SONIFICATION类型的通知音响起,系统大概率会让通知音直接播放,同时把音乐闪避到较低音量。这个逻辑跟contentType有直接关系。
第三个层面是音量曲线。不同类型的声音在同一个音量流上的衰减曲线可能不一样,尤其SPEECH类型在低音量时还容易触发设备的声音清晰化增强。有些手机在低音量播放语音消息时,系统会自动压低环境感知,突出人声,这就是contentType参与决策的结果。
第四个层面是路由策略。蓝牙耳机、外放、USB声卡之间切换时,系统对不同contentType的处理策略有差异。MUSIC和MOVIE通常会跟随媒体流的默认路由,而SPEECH会更倾向于通信路由,有些设备甚至会把SPEECH从蓝牙的A2DP切换到HFP模式,这是很多开发者忽略的坑。
2.3 为什么设置contentType必须配合usage
前面说过,contentType和usage是两个维度,但在系统策略中它们是组合计算的。最常见的搭配规则:
- 播放音乐 → usage=USAGE_MEDIA,contentType=CONTENT_TYPE_MUSIC
- 语音通话 → usage=USAGE_VOICE_COMMUNICATION,contentType=CONTENT_TYPE_SPEECH
- 录音监控或者语音消息 → usage=USAGE_ASSISTANT或者USAGE_MEDIA,contentType=CONTENT_TYPE_SPEECH
- 闹钟 → usage=USAGE_ALARM,contentType=CONTENT_TYPE_SONIFICATION
- 通知音 → usage=USAGE_NOTIFICATION,contentType=CONTENT_TYPE_SONIFICATION
如果你只设置了contentType,usage保持默认UNKNOWN,系统会拿不准这段音频的应用场景。比如你把contentType设置成SPEECH但usage是MEDIA,那它到底走语音还是媒体流?最终以usage为准决定音量流。所以很多“设置不生效”的问题,根源往往不是contentType没设,而是usage太含糊。
反过来也一样,只用usage不用contentType,声音处理策略就可能跑偏。比如usage=USAGE_MEDIA但contentType=SPEECH,这在某些设备上会触发语音处理链路,导致播放人声素材时声音发闷。我在实际项目中遇到过类似问题,后面会展开讲。
3. 完整用法实例:从创建到生效
3.1 基础代码示例:手写一个AudioAttributes
先来一个最标准的写法,这段代码是AudioManager或者MediaPlayer、SoundPool、AudioTrack都能用的基础结构:
AudioAttributes audioAttributes = new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .setUsage(AudioAttributes.USAGE_MEDIA) .build();如果你用的是Kotlin,写法几乎一样:
val audioAttributes = AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .setUsage(AudioAttributes.USAGE_MEDIA) .build()就这么简单吗?对,创建确实简单。真正的关键在于你怎么把这个AudioAttributes挂到实际播放对象上。不同播放API有不同的挂载方式,下面逐个讲。
先看MediaPlayer怎么用:
MediaPlayer mediaPlayer = new MediaPlayer(); mediaPlayer.setAudioAttributes(audioAttributes); mediaPlayer.setDataSource("https://example.com/audio.mp3"); mediaPlayer.prepareAsync(); mediaPlayer.setOnPreparedListener(mp -> mp.start());注意setAudioAttributes必须在setDataSource之前调用,这个顺序一旦搞反,系统会直接跑出IllegalStateException。我见过不少新人在这个细节上翻车,明明代码逻辑没错,播放也正常,但属性就是没生效。
再看SoundPool,这个在游戏和提示音场景里特别常见。SoundPool的构造函数自带AudioAttributes参数:
AudioAttributes soundAttrs = new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) .build(); SoundPool soundPool = new SoundPool.Builder() .setMaxStreams(3) .setAudioAttributes(soundAttrs) .build();SoundPool的contentType选择有个讲究:如果只是播放短促的按键音、提示音,用CONTENT_TYPE_SONIFICATION最合适。有些开发者图省事直接照搬MUSIC,结果就是提示音在部分手机上音量偏低或者延迟偏大,因为系统对SONIFICATION类型有独立的低延迟处理。
AudioTrack也一样,直接在Builder里传入AudioAttributes就行:
AudioAttributes trackAttrs = new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE) .setUsage(AudioAttributes.USAGE_MEDIA) .build(); AudioTrack audioTrack = new AudioTrack.Builder() .setAudioAttributes(trackAttrs) .setAudioFormat(new AudioFormat.Builder() .setEncoding(AudioFormat.ENCODING_PCM_16BIT) .setSampleRate(44100) .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO) .build()) .setBufferSizeInBytes(2048) .setTransferMode(AudioTrack.MODE_STREAM) .build();如果你是做视频播放器、跨平台播放引擎,AudioTrack加AudioAttributes这个组合是最常见的。我建议把音频属性的设置下沉到引擎层,这样上层业务不需要关心属性细节。
3.2 如何用AudioManager做音量控制联动
AudioAttributes跟音量的关系,很多开发者没有直接感知,但系统内部确实用它在多个音量流之间做路由。下面是AudioManager与AudioAttributes配合的典型代码,这个操作在Android 10之后经常用于“按音频类型调整音量”:
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); // 为指定的音频属性调整音量 float volume = 0.8f; // 范围 0.0f ~ 1.0f int volumeIndex = Math.round(volume * audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)); audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volumeIndex, 0); // 使用 AudioAttributes 查询或调节音量组(API 2.5+ 才有真正意义的属性音量,但 AudioAttributes 同样参与索引计算) int index = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);这里要澄清一个概念:真正决定音量归属的是usage,不是contentType。STREAM_MUSIC对应USAGE_MEDIA,STREAM_ALARM对应USAGE_ALARM,STREAM_NOTIFICATION对应USAGE_NOTIFICATION。所以你在设置contentType的同时,要保证usage跟实际音量流一致,否则就会出现“明明码率正常,但音量键控制的是另一个流”的情况。
3.3 在Android 16新版本上的适配要点
到了Android 16,也就是API 36,AudioAttributes的本身用法没有大的变化,但系统音频产品策略(AudioProductStrategy)对属性合法性的校验更严格了。有一种情况在旧版可能只是打个Log,但在Android 16上直接崩溃:
// 注意:这个组合在部分新版系统上会抛异常 AudioAttributes invalidAttrs = new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN) .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) .build();为什么?因为语音通信usage默认期望的是SPEECH类型的contentType,如果contentType明显不匹配,系统策略模块在解析属性组合时无法确定该走哪一套策略,就会以非法参数为由拒绝。
所以在新版本适配中我强烈建议:先定义一个属性校验方法,把常见的组合枚举校验一遍,再决定走哪套播放链路。
一个可靠的校验思路是这样:如果usage是USAGE_VOICE_COMMUNICATION、USAGE_ASSISTANT这类跟人声强相关的场景,contentType必须显式设置成CONTENT_TYPE_SPEECH,不能用UNKNOWN带过。如果usage是USAGE_MEDIA但实际内容是语音聊天记录,contentType设置成SPEECH没问题,但你要接受系统可能对它做语音处理的事实。
3.4 参数组合选择的逻辑推导
很多人在选参数的时候靠猜,这里我给一个更工程化的推导流程。你拿到一个播放需求,先问三个问题:
第一个问题:这段音频是人声为主吗?是,contentType就选SPEECH;不是,往下走。
第二个问题:这段音频是音乐还是音效?音乐选MUSIC,短音效选SONIFICATION,电影或者混音场景选MOVIE。如果实在不确定,选UNKNOWN兜底。
第三个问题:用户与这段音频的交互场景是什么?用手动播放选MEDIA,闹钟提醒选ALARM,通话选VOICE_COMMUNICATION,导航选ASSISTANCE_NAVIGATION_GUIDANCE,按键反馈选ASSISTANCE_SONIFICATION。
把两个问题的答案组合起来就得到了最终配置。比如“人声为主的播客App”:contentType选SPEECH,usage选MEDIA,看起来似乎有点矛盾但实际合理,因为播客内容本质是人声,但用户的播放场景跟听音乐一致。这种情况下部分设备会启用语音优化,导致声音听感与众不同,你就需要在播放器里做一个音效均衡器的补偿。
再比如“游戏里的爆炸声”:contentType选SONIFICATION比较合理,因为短促音效需要低延迟;usage选USAGE_GAME。如果选MUSIC,游戏的背景音乐可以,但音效的延迟可能不受控。
4. 常见问题与排查技巧实录
4.1 设置了contentType但播放没生效
这是遇到最多的问题。排查思路要按链路走,别一上来就怀疑Android 16的兼容性。
第一步,确认AudioAttributes确实挂载到了播放器上。MediaPlayer要在setDataSource之前setAudioAttributes;SoundPool要用Builder传入;AudioTrack要在构造的时候传入。任何一步做错,系统都用默认属性。
第二步,检查usage是否正确。很多设置“没生效”其实是usage不对,系统按usage优先选择了音量流和路由。比如你做闹钟,usage应该用ALARM,如果你用MEDIA,那系统就把它当普通音乐,静音开关一开就没了。
第三步,用adb命令验证实际的AudioAttributes。这个方法特别好用,可以在播放的同时执行:
adb shell dumpsys audio | grep -A 20 "players"输出里会列出当前活跃的播放器条目,包括AudioAttributes。你直接看里面的content_type和usage字段是不是你想要的值。
第四步,检查是不是被AudioFocus等其他机制覆盖了。有些播放器框架在申请焦点的时候会临时替换AudioAttributes,比如ExoPlayer默认会合并自定义属性,如果你用的播放器内部有自己的一套属性覆盖逻辑,你可能已经传了正确的值,但被框架改掉了。
4.2 不同Android版本之间的行为差异
Android版本差异是最防不胜防的坑。AudioAttributes从API 21引入,到今天已经很多年,但不同大版本对属性的解释还是有细微差别。
API 21到25,系统对contentType的解析比较宽松,即使组合不太合理也只是按默认策略处理,很少报错。API 26开始引入AudioProductStrategy,系统开始按属性组合匹配到具体策略,这时候不合理组合可能表现异常。API 29之后,audio_mode和attributes的联动更多,尤其是通话模式和媒体播放的切换。到了API 34和36,合法性校验变得很严格,前面说的崩溃问题就是典型例子。
我的建议是:如果你的App最低支持的版本跨度大,不要只在一台新设备上测试。拿一台Android 8、一台Android 12、一台Android 16,播放同一段音频,用dumpsys audio观察contentType被系统解释成的实际策略,差异一目了然。
4.3 音频焦点冲突时contentType的意义
音频焦点的处理跟AudioAttributes密切相关,尤其是contentType会导致不同的闪避策略。举个例子,两个应用同时申请焦点,一个播放音乐(contentType=SPEECH),另一个播放语音消息(contentType=SPEECH),系统判断后者更需要清晰度,就会给前者下发AUDIOFOCUS_LOSS_TRANSIENT。
说白了,contentType在这个过程中扮演了“声源分类器”的角色。系统会根据你是音乐还是语音来决定冲突的优先级和让步方式。如果你在所有场景都用CONTENT_TYPE_MUSIC,那么语音消息播放时容易被系统误判成音乐,闪避行为就可能不合预期。
建议在换取焦点的请求里也带上AudioAttributes:
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); AudioAttributes playbackAttributes = new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) .setUsage(AudioAttributes.USAGE_MEDIA) .build(); AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) .setAudioAttributes(playbackAttributes) .setOnAudioFocusChangeListener(this, getMainExecutor()) .build(); int result = audioManager.requestAudioFocus(focusRequest);这里的setAudioAttributes跟播放时的属性要尽量保持一致,否则焦点判断的时候系统拿到的属性跟实际播放属性不一致,会出很多奇怪的并发问题。
4.4 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 音量键控制错误音流 | usage设置不对,走错音量分组 | 检查usage与STREAM对应关系 |
| 播放声音发闷、被处理过 | contentType误用SPEECH,触发语音链路 | 对纯音乐场景改用MUSIC |
| 提示音延迟大 | contentType误用MUSIC,未走低延迟链路 | 短音效用SONIFICATION |
| 设置属性后崩溃 | 属性组合非法,Android 16强校验 | 检查usage与contentType匹配度 |
| 焦点冲突时不闪避 | 焦点请求里的attributes与播放不一致 | 统一属性的设置 |
| 蓝牙耳机无声或切换模式 | SPEECH类型自动切到HFP | 对音乐场景避免SPEECH |
4.5 独家避坑技巧
再分享几个常规文档里不会写的经验。
第一,不要在setAudioAttributes之后再去修改AudioAttributes的内容,因为AudioAttributes是不可变对象,你要修改只能重新创建。有些人想当然地拿同一个对象在不同播放器里复用,结果setUsage被framework的某个环节悄悄覆盖了,就变得很难查。
第二,在Android 16上做音频路由变更监听时,如果使用AudioDeviceCallback,要留意广播时带回来的AudioDeviceInfo。部分设备在切换音频路由后会重置某些音频属性,尤其是蓝牙HFP和A2DP之间的切换。建议在路由切换完成之后重新设置一次播放器的AudioAttributes,不要等播放异常再去处理。
第三,AudioAttributes的cache策略。如果你在一款产品里需要频繁创建播放器实例,AudioAttributes可以做成全局单例。因为它是不可变对象,线程安全,多个播放器复用完全没问题,还能避免频繁创建对象带来的GC压力。
5. 实操心得与场景扩展
5.1 从AudioAttributes出发看系统音频策略设计
说句实在话,做Android音频开发,值钱的不是你会调几个API,而是你能理解系统怎么根据参数做决策。AudioAttributes就是参透这套决策机制的钥匙。
你在开发中只要把contentType、usage理解透了,很多看似诡异的现象都能找到合理解释。比如同一个播放器在不同手机上音量不一致、同一个提示音在部分平板上延迟明显、语音消息在蓝牙耳机上音质奇怪,这些问题八成都能在属性设置上找到突破口。
我自己做项目时的通用做法是:在播放器内核里暴露一个“音频场景”枚举,把常见场景映射到预先定义好的AudioAttributes组合。业务层只需要告诉播放器“我现在播放的是播客、闹钟还是游戏音效”,播放器内部自动处理属性组装。
5.2 借助SoundPool优化短音频播放体验
再展开说一个高频场景:提示音的播放。除了前面提到的setAudioAttributes,SoundPool本身还有几个细节能直接影响体验,这几条我在多个游戏项目里验证过。
第一个细节是setMaxStreams的取值。提示音通常会同时出现多个(比如连击音效),但资源是有限的,设置过大可能超过设备允许的流上限,设置过小又会导致后面的音效直接不播。个人经验是普通游戏3~4个就够,如果同时混合了背景音乐和技能音效,取5~6个更稳。
第二个细节是setLoop。短促音效不需要循环,设置0就行,设置非0值会导致音效播放完不自动停止,而且这个状态一旦混入流里,下次播放同一个音效可能出现时间戳错乱。
第三个细节是音量的归一化。SoundPool的setVolume参数范围是0.0到1.0,但这个值并不是直接映射到系统音量,它是在整体音频流之上的一个乘性因子。如果你的提示音素材本身响度偏低,光调这个值效果有限,需要在做素材时就把响度拉平。
soundPool.setOnLoadCompleteListener((soundPool1, sampleId, status) -> { if (status == 0) { float vol = 0.9f; soundPool.play(sampleId, vol, vol, 1, 0, 1.0f); } });第四个细节是onLoadCompleteListener的回调时机。如果你在音频还没加载完成时就play,会得到一个0返回值,这段音效彻底不会播放。很多音乐类App的连击音效听起来偶尔丢一拍,就是这个原因。
5.3 关于音频路由和音频驱动的坑
热搜词里有一个“max98357a音频”和“tda2030音频放大电路”,这属于嵌入式音频的范畴,跟Android系统层的AudioAttributes关系不大,但有一个共通点:硬件链路确定之后,软件层再怎么调属性,也只是在已有的链路里做选择。如果你对音质有硬性要求,建议先把底层硬件支持的采样率和位深搞清楚,再决定上层用PCM还是压缩格式,否则就算contentType调得再准,最终出声的硬件也不一定能还原。
另外提到“scrcpy怎么禁用音频转发”,这个跟Android音频机制也有点关联。scrcpy转发音频是通过系统音频回采做到的,如果你想在开发调试时只转发画面不转发声音,在较新的版本里直接加上--no-audio参数就行:
scrcpy --no-audio这个操作跟AudioAttributes没有直接关系,但做音频开发调试的时候,频繁关闭打开模拟器的音频设备确实让人头疼,这个小参数能帮你省很多事。
5.4 再聊一个隐藏场景:播放器音量跟系统音量的适配
回到正题。很多播放器有三种音量模式:跟随系统音量、独立音量、固定音量。在Android 16上如果你想做“独立音量”而不是跟随系统媒体音量,只能在AudioAttributes之外再引入一个音量映射层,自己维护一个线性增益,再叠到系统的stream音量上。
但这个做法有个副作用:系统在音量变化时的UI展示、蓝牙绝对音量同步、车载系统的音量旋钮都会跟你自己的音量映射脱节。我在某个智能座舱项目里就遇到过这个问题,音频属性全都设置正确,但车机音量旋钮拧到头,播放器声音却只有50%,因为播放器内部又叠了一层自己的音量增益。
我的建议是,除非产品需求强约束不能直接调系统音量,否则不要自己再做一套音量映射。Android系统音量本身已经是一个平滑曲线,第三方播放器强行做二次映射,只会带来更多的兼容性成本。这个结论在我做过的多个项目中验证过,包括在线音乐、语音社交、儿童内容播放器,都适用。
5.5 向后兼容与多设备测试清单
最后整理一份多设备测试清单,适配AudioAttributes时按这个表过一遍:
| 测试项目 | 关注点 | 预期结果 |
|---|---|---|
| 手机外放播放音乐 | 音量流、音质 | STREAM_MUSIC正常,声音无处理痕迹 |
| 蓝牙耳机播放音乐 | 路由、编码 | A2DP连接,声音正常 |
| 蓝牙耳机播放语音 | 协议切换 | 可能切到HFP,音质下降属预期 |
| 闹钟提醒 | 音量流、焦点 | 走STREAM_ALARM,媒体音量不影响 |
| 短促提示音连发 | 延迟、丢失 | 延迟尽可能低,不丢播 |
| 多应用并发播放 | 焦点交互 | 按属性策略正确闪避或暂停 |
| 路由切换中途播放 | 状态保持 | 切换后继续播放不卡顿 |
这套清单我们在Android 8、Android 10、Android 12、Android 16四档系统上都完整跑过,踩坑率最高的反而是“闹钟提醒”和“路由切换”这两项,因为它们涉及到跟系统其他模块的交互,单测不容易覆盖。
就我个人经验来说,AudioAttributes && setContentType这套配置本身很简单,但它牵扯到的系统行为链路比想象中长得多。建议你拿到一个真实设备,花十分钟把dumpsys audio输出读一遍,看到系统如何解释你设置的属性,比看十篇文档都有用。下次遇到音频表现不符合预期,先别急着改逻辑,回到属性配置这一步重新审视,往往答案就在这个小角落里。