拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Android AudioManager全解析:音量控制、音频焦点与设备路由实战

我平时做Android开发最怕听到的一句话不是又崩了而是你这App怎么没声音。声音这种体验看起来不起眼真出问题的时候用户第一个骂的就是你。很多时候问题的根源都指向同一个系统服务——AudioManager。AudioManager是Android系统里负责音频资源统一调度的核心类应用通过它来调音量、抢音频焦点、切换输出设备、管理铃声模式和通话路由。它几乎承包了App所有跟声音相关的能力也是各种诡异问题的高发区。这篇文章我会从实际项目角度把AudioManager拆开讲清楚包括音量控制、音频焦点、模式切换、设备路由以及我在线上遇到过的一堆坑希望能给正在跟声音死磕的同行一点参考。这篇文章适合谁看如果你是刚接触Android的初级开发可以把它当一份AudioManager的入门地图如果你已经写了两三年业务但没系统梳理过音频这块里面关于焦点管理和设备切换的细节应该能帮你省不少排查时间。我不打算按官方文档的顺序念稿子而是按你接到一个音频需求时真正会遇到的问题来组织内容。1. AudioManager整体认知一个总装车间不是一个音量按钮很多开发者对AudioManager的认知停留在调音量上这其实是个很大的误解。音频在Android系统里是一套多层协作的链路AudioManager是App和底层AudioFlinger之间的门户App通过它发出的每一条指令最终都会作用到系统的音频策略服务上。1.1 从一次没声音的排查说起之前有个项目上线后收到一堆反馈说用户插上耳机打电话对方听不到声音但用户自己能听到。当时第一反应是检查音频路由代码结果翻了一圈发现走的全是系统默认逻辑没做任何手动路由。后来打了AudioManager的设备连接日志才定位到问题出在蓝牙耳机回连的时序上手机先连上了蓝牙耳机然后用户才插入有线耳机此时系统把音频路由切到了蓝牙但UI上还显示有线耳机已插入用户以为在用有线耳机说话实际麦克风走的是蓝牙通道。这个坑暴露出一个事实音频路由不是一个可以拍脑袋决定的状态它是一个需要监听变化、及时响应的动态过程。AudioManager不是简单的调音量API集合它管的是整个声音世界的交通调度你得先有这个认知后面学起来才不偏。1.2 AudioManager的核心能力全景Android系统在开机时会创建一个音频服务进程而AudioManager就是进程与App之间的门面。它对外暴露的能力大致可以分为四类音量控制读写各类音频流的音量、最大最小音量、音量增减方向。音频焦点协调多个App同时发声时的优先级避免出现两个App一起响的混乱局面。音频模式与策略切换手机的正常、静音、振动模式以及通话、娱乐等底层模式。设备路由查询和切换当前的音频输出输入设备包括扬声器、有线耳机、蓝牙设备等。这四块并不是独立的。举一个最常见的例子你在App里播放音乐用户接了个电话通话进来后系统会先请求音频焦点音乐侧收到焦点丢失回调暂停播放挂断电话后焦点被释放音乐App可以重新请求焦点并恢复播放。整个流程里音量、焦点、模式、路由四个维度全都在联动。2. 音量控制先把丢了声道这件事搞清楚音量控制是AudioManager最常用的功能但它有个前置概念容易被人忽略Android把声音按用途分成了多个独立的流Stream互不干扰。如果你没搞清楚自己操作的是哪条流就会出现明明把音量调到最大了外放还是没声音这类乌龙。2.1 音频流类型不要只认识STREAM_MUSICAudioManager中定义了一套音流常量常用的大概有这几条音频流对应场景默认音量控件STREAM_VOICE_CALL通话中的对端声音通话音量键STREAM_SYSTEM系统提示音部分版本已不再使用系统音量键STREAM_RING来电铃声、短信提醒铃声/通知音量键STREAM_MUSIC媒体播放、游戏、视频媒体音量键STREAM_ALARM闹钟闹钟音量键STREAM_NOTIFICATION通知音通知音量键STREAM_DTMF拨号键盘音通话音量按键音STREAM_ACCESSIBILITY无障碍播报跟随媒体或独立设置不同的流之间音量独立存储、独立调节这个设计的初衷是闹钟声音不该因为媒体音量调小就变小对用户来说是合理的。但对开发者来说最容易犯的错就是用STREAM_MUSIC的音量去控制通话音量或者用调整STREAM_RING的方式去改媒体音量结果在部分机型上完全不生效。另外Android 9 (API 28)以后又加了一个可调节的流的概念系统通过adjustStreamVolume调节时的有效性判断跟旧版本存在差异阻塞式弹音量条的行为也变了。如果想要自定义音量条样式你仍然需要监听音量变化但系统弹窗依然会在某些场景下出现这是个兼容性上的老熟人了。2.2 音量的读取与设置先看基本API这几行代码是理解一切音量操作的起点AudioManager audioManager (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); // 获取当前媒体音量、最大音量 int currentVolume audioManager.getStreamVolume(AudioManager.STREAM_MUSIC); int maxVolume audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); // 直接设置音量0到最大之间 audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, currentVolume 1, 0); // 按步进调节direction为正负一flags可以为0 audioManager.adjustStreamVolume( AudioManager.STREAM_MUSIC, AudioManager.ADJUST_RAISE, AudioManager.FLAG_SHOW_UI );setStreamVolume的第三个参数是flags常用值有FLAG_SHOW_UI显示系统的音量条、FLAG_PLAY_SOUND播放按键提示音。如果你在自己的App里做了音量UI要特别注意设置音量后如果还带FLAG_SHOW_UI用户会看到双层音量条这是典型的重复UI问题。我自己的习惯是自己画了UI就不带FLAG_SHOW_UI用FLAG_REMOVE_SOUND_AND_VIBRATE来避免多余音效。还有一点setStreamVolume不是所有流都能调。系统限制了一些受保护流如闹钟在某些安全场景下可能调整失败代码里应该判断返回值并做好失败降级。2.3 音量键行为与自适应控制Android 5.0之后系统默认音量键控制的是当前活跃的流而不是固定某个流。如果用户当前在播放音乐按音量键调的是媒体音量如果在铃声设置界面调的是铃声音量。这个行为对用户是友好的但对开发者来说有一个隐患你的App在后台时如果用户按了音量键但焦点还在另外的App上你的App完全感知不到。如果产品需求是进入App后音量键固定调节媒体音量可以监听onKeyDown拦截音量键按键事件然后自己调用adjustStreamVolume(STREAM_MUSIC, ...)最后返回true拦截默认行为。但这招要慎用因为强行重写用户对音量键的预期很容易被应用商店以干扰系统交互为由拒绝。另一个容易被忽略的点是音量曲线的非线性。同样的setStreamVolume数值在不同音量段上人耳感受到的响度变化不是线性的这是Android音频框架实现的音量映射曲线导致的。比如从音量1调到2可能感觉变化不大但8调到9变化就很明显。如果你做的是专业音乐类App最好通过系统的曲线上获取真实增益或者引入自己的响度均衡方案否则用户会觉得你的音量跳变不均匀。3. 音频焦点管理多个App抢声音时的红绿灯如果说音量控制是AudioManager的入门那音频焦点Audio Focus就是它的深水区。很多奇怪的声音问题比如播放视频时通知音把视频声音压没了、后台播放的App跟当前App互相抢声音基本都是焦点处理不完善导致的。3.1 音频焦点到底解决了什么问题Android上多个App可以同时运行但扬声器只有一个。如果每个App都直接往扬声器放声音结果就是大家一起响体验极差。音频焦点的本质是一个资源使用权的协商机制某个App想播放声音要先向系统申请焦点系统根据当前焦点的持有情况决定是给、是拒绝还是让你降低音量继续播。焦点分为几种永久和临时状态AUDIOFOCUS_GAIN长时间使用比如播放音乐、看视频。AUDIOFOCUS_GAIN_TRANSIENT短时间使用比如语音提示、短音效。AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK短暂使用但允许闪避——也就是让其他正在播放的App降低音量而不是暂停。当焦点状态变化时系统通过OnAudioFocusChangeListener回调通知原持有者回调参数包括AUDIOFOCUS_LOSS你永久失去了焦点应该停止播放并清理资源。AUDIOFOCUS_LOSS_TRANSIENT暂时失去焦点暂停播放过一会儿可以恢复。AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK音量大点会被压低建议降低音量播放而不是暂停。3.2 请求与释放焦点的完整流程传统写法是使用requestAudioFocus这在现代版本中虽然还能用但更推荐API 26之后使用AudioFocusRequest。先看传统写法方便我们在老代码库里快速看懂逻辑AudioManager audioManager (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); AudioManager.OnAudioFocusChangeListener focusListener new AudioManager.OnAudioFocusChangeListener() { Override public void onAudioFocusChange(int focusChange) { switch (focusChange) { case AudioManager.AUDIOFOCUS_GAIN: // 恢复播放重新初始化播放器设置音量为正常 player.start(); break; case AudioManager.AUDIOFOCUS_LOSS: // 永久失去焦点停止播放释放资源 player.stop(); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: // 临时失去焦点暂停播放 player.pause(); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: // 需要闪避降低音量但继续播放 player.duck(); break; } } }; int result audioManager.requestAudioFocus( focusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN ); if (result AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { // 成功获得焦点开始播放 }释放焦点时调用abandonAudioFocus(focusListener)。这里有个小的经验点请求焦点时requestAudioFocus的三个参数里durationHint一定要贴近你的实际场景。如果你只播放一个几秒钟的短音效却传了AUDIOFOCUS_GAIN碰到另一个正在听音乐的用户音乐会暂停好几秒体验很糟糕。短音效应该用AUDIOFOCUS_GAIN_TRANSIENT播放音乐用AUDIOFOCUS_GAIN两者别混用。3.3 AudioFocusRequest与新版本适配API 26之后Google推荐使用AudioFocusRequest因为它把焦点类型是否允许闪避延迟焦点等参数封装得更规范。一个典型用法是这样AudioFocusRequest focusRequest; if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { AudioAttributes attributes new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build(); focusRequest new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) .setAudioAttributes(attributes) .setFocusGain(AudioManager.AUDIOFOCUS_GAIN) .setOnAudioFocusChangeListener(focusListener) .build(); requestResult audioManager.requestAudioFocus(focusRequest); } // 释放 if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { audioManager.abandonAudioFocusRequest(focusRequest); }这里AudioAttributes的作用是告诉系统你用声音的目的比如USAGE_MEDIA表示媒体播放USAGE_ASSISTANCE_SONIFICATION表示辅助音效。不同用途在焦点协商时优先级不同比如导航语音提示通常比媒体音乐优先级高系统可能会让音乐闪避来保证导航清晰。实际项目中如果你的targetSdkVersion比较新同时还要兼容老机型我建议直接封装一个工具类内部判断版本走两条分支避免到处散落着Build.VERSION.SDK_INT判断。3.4 焦点丢失后的处理策略这节我觉得值得单独说因为大多数App的焦点逻辑写得太粗暴了。很多人拿到AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK就直接暂停播放其实这是个比较重的处理方式用户听音乐时突然来了个语音提醒如果音乐完全停了体验反而很突兀。闪避的正确做法不是暂停而是把播放音量降到原来的两三成等焦点回来后再恢复。恢复音量也有讲究。不能直接拿setStreamVolume去设一个固定值因为用户可能在闪避期间自己调了音量如果你恢复时还按原来的固定音量设置就覆盖了用户的操作。正确做法是保存闪避前的音量在AUDIOFOCUS_GAIN回调里恢复private float preDuckVolume 1.0f; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: preDuckVolume player.getVolume(); player.setVolume(0.3f); break; case AudioManager.AUDIOFOCUS_GAIN: player.setVolume(preDuckVolume); break;很多App在焦点恢复时还会遇到一个衍生问题播放进度被重置。所以暂停和恢复的时候一定要主动记录播放位置尤其是视频类App恢复后应该跳到暂停前的进度而不是从头开始。4. 模式切换与设备路由外放、耳机、蓝牙之间的调度中心音量讲完了焦点讲完了接下来是另一个大块音频模式和输出路由。这块出问题的频率很高尤其在通话、录音、蓝牙交互的场景里。4.1 音频模式MODE_*详解AudioManager提供的音频模式决定了当前系统整体处于什么使用状态常见值有MODE_NORMAL普通模式无铃声无通话大多数App播放媒体时都是这个模式。MODE_RINGTONE来电响铃模式。MODE_IN_CALL通话中包括VoIP和普通电话。MODE_IN_COMMUNICATION语音通话、语音聊天等通信场景比MODE_IN_CALL更常用在网络通话中。设置模式的方式是audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION)用完记得恢复为MODE_NORMAL。这个API在通话录音和VoIP项目里几乎是必用的因为系统模式直接决定麦克风、扬声器和回声消除的行为。第一次接触这块的开发者很容易踩的坑是setMode之后不恢复。如果你的App启动了VoIP通话把模式切成了MODE_IN_COMMUNICATION但通话结束时忘了切回MODE_NORMAL那整个系统的声音行为都会被带偏比如媒体播放音量异常、某些机型的外放没声音。所以所有模式切换的代码都要写在finally块或者用生命周期回调兜底确保一定会还原。4.2 输出设备的监听与切换设备路由这块很多人只知道isSpeakerphoneOn、isWiredHeadsetOn这些老接口但这些接口在今天越来越不可靠。不同厂商、不同蓝牙方案对isBluetoothA2dpOn的实现差异很大很容易拿到错误状态。更推荐的做法是注册AudioDeviceCallback监听设备的接入和移除AudioManager audioManager (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); AudioManager.AudioDeviceCallback callback new AudioManager.AudioDeviceCallback() { Override public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { for (AudioDeviceInfo device : addedDevices) { // 判断设备类型耳机、蓝牙、扬声器等 if (device.getType() AudioDeviceInfo.TYPE_WIRED_HEADPHONES || device.getType() AudioDeviceInfo.TYPE_WIRED_HEADSET) { // 切换UI或提示 } } } Override public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { // 设备移除时的处理 } }; audioManager.registerAudioDeviceCallback(callback, null); // 页面销毁时注销 audioManager.unregisterAudioDeviceCallback(callback);AudioDeviceInfo有TYPE_BUILTIN_SPEAKER、TYPE_WIRED_HEADSET、TYPE_BLUETOOTH_A2DP、TYPE_USB_DEVICE等类型几乎覆盖了所有常见输出设备。这个回调比轮询设备状态优雅得多而且能做到真正的及时感知我强烈建议用这个替代旧接口。不过要注意的是AudioDeviceCallback只是告诉你设备发生了变化不会告诉你当前的音频流是否真的路由到了这个设备。系统音频策略在设备切换上的表现很复杂比如蓝牙和有线同时接入时的优先级以及不同音乐App自己做的暂停恢复逻辑都会影响实际的播放输出。真正确认路由是否生效还是得靠日志和实测。4.3 通话场景下的音频管理实战音视频通话是AudioManager出问题的高发区因为通话涉及麦克风、扬声器、听筒、蓝牙多路音频通道的交叉。我梳理一个完整的最小化通话音频管理逻辑进入通话前setMode(MODE_IN_COMMUNICATION)请求AUDIOFOCUS_GAIN同时让系统进入通信模式保证回声消除和麦克风策略正确。根据用户选择切换路由默认走听筒点击免提把音频路由到扬声器点击蓝牙则尝试连接蓝牙设备。通话结束时释放焦点恢复模式为MODE_NORMAL并还原麦克风和扬声器状态。路由切换的代码大致长这样// 切换到扬声器 audioManager.setSpeakerphoneOn(true); // 切换到听筒 audioManager.setSpeakerphoneOn(false); // 蓝牙耳机假设已经连接 audioManager.startBluetoothSco(); audioManager.setBluetoothScoOn(true);这里面有个经典雷区setSpeakerphoneOn这个API的名字具有迷惑性虽然它带一个On后缀但传false并不代表关闭扬声器而是关闭强制外放这个偏好。如果你的代码在通话结束前没有把setSpeakerphoneOn(false)还回去就可能影响下一次通话的初始状态。蓝牙SCO和A2DP的区别也值得一说。A2DP是高音质媒体流通道适合放歌SCO是窄带语音通道适合通话。很多开发者搞混了这两个概念在通话里调了startBluetoothSco却忘了关setBluetoothScoOn结果通话结束后的媒体播放全变成电话音质用户听着就像在电话里放音乐一样非常明显。所以SCO用完一定要关而这个状态恰恰是最容易被遗忘的。5. 实战案例做一个可复用的音频管理工具类前面讲了一堆API和理论我估计有人已经想动手整理了。与其七零八落地写在Activity里不如榨出一个独立的工具类封装AudioManager操作从项目一开始就建立整洁的音频管理入口。下面这份思路和代码我直接用的线上项目的结构改过来的可以直接抄去用但建议你按自己的业务场景裁剪。5.1 需求设计与结构规划一个能用得住的音频管理工具类我的经验是需要考虑这几块音量读写、焦点请求与释放、模式切换、设备监听、状态查询。为了方便维护我通常给工具类定义清晰的方法名外部调用根本不需要知道内部用了什么API将来替换实现也不会影响业务层。这里有一个设计要点AudioManager的操作全部是系统级同步调用涉及UI线程的我习惯封装成监听器模式把焦点变化、设备变化直接回调到业务层。业务层不需要自己实现OnAudioFocusChangeListener避免重复监听和回调丢失。另外针对不同App的类型工具类还应该允许外部设置当前使用的音频流类型、定位用途媒体、语音、游戏等这样才能在调用焦点时构造成正确的AudioAttributes。5.2 核心代码实现封装音量与焦点直接给一套参考实现注意这里只保留了核心逻辑你的业务代码需要额外处理空判断和异常public class AudioManagerHelper { private final AudioManager audioManager; private final Context context; private AudioFocusRequest focusRequest; private final AudioManager.OnAudioFocusChangeListener focusListener; public AudioManagerHelper(Context context) { this.context context.getApplicationContext(); this.audioManager (AudioManager) this.context.getSystemService(Context.AUDIO_SERVICE); this.focusListener createFocusListener(); } private AudioManager.OnAudioFocusChangeListener createFocusListener() { return change - { switch (change) { case AudioManager.AUDIOFOCUS_GAIN: onFocusGain(); break; case AudioManager.AUDIOFOCUS_LOSS: onFocusLoss(); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: onFocusLossTransient(); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: onFocusDuck(); break; default: break; } }; } public boolean requestFocus() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { AudioAttributes attributes new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build(); focusRequest new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) .setAudioAttributes(attributes) .setOnAudioFocusChangeListener(focusListener) .build(); return audioManager.requestAudioFocus(focusRequest) AudioManager.AUDIOFOCUS_REQUEST_GRANTED; } else { return audioManager.requestAudioFocus( focusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN ) AudioManager.AUDIOFOCUS_REQUEST_GRANTED; } } public void releaseFocus() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.O focusRequest ! null) { audioManager.abandonAudioFocusRequest(focusRequest); } else { audioManager.abandonAudioFocus(focusListener); } } public int getVolume(int streamType) { return audioManager.getStreamVolume(streamType); } public int getMaxVolume(int streamType) { return audioManager.getStreamMaxVolume(streamType); } public void setVolume(int streamType, int volume, boolean showUi) { int flags showUi ? AudioManager.FLAG_SHOW_UI : 0; audioManager.setStreamVolume(streamType, volume, flags); } // 子类或业务模块重写这些回调 protected void onFocusGain() {} protected void onFocusLoss() {} protected void onFocusLossTransient() {} protected void onFocusDuck() {} }这个类把版本差异封装在了内部。调用方只需要关心请求焦点和释放焦点不同Android版本的系统行为差异都被屏蔽了接入成本降到最低。真正做项目时你完全可以在子类里重写四个回调把暂停、恢复、闪避的具体业务逻辑写在里面。5.3 Activity生命周期整合经验工具类有了还得知道挂在生命周期的哪里。我的经验是如果是全局媒体播放器建议把它放进播放器模块的单例或者服务里而不是绑定某个Activity。因为Activity旋转、跳转都有可能导致工具类被重复创建或者监听泄漏。以音乐播放为例最稳妥的绑定方式是这样onPlay()时请求焦点成功后才做真正的start操作。onPause()时释放焦点但要保证用户主动暂停和焦点丢失被动暂停是两套不同逻辑。用户主动暂停不要在onAudioFocusChange里恢复否则用户点了暂停来了一条通知通知消失后焦点回来了App自动开始播放想想都尴尬。onDestroy()时注销设备回调释放焦点并恢复模式为MODE_NORMAL。很多线上音频问题都出在恢复的时机判断上。判断当前是否是用户主动暂停可以用一个布尔变量标记焦点回调里只有在不是用户主动暂停时才恢复。这个小细节是我在处理过一次明明点了暂停蓝牙断开重连后又自动播放的bug后总结出来的。蓝牙断开重连会让焦点短暂丢失再恢复如果逻辑没区分主动暂停就会触发自动播放用户会一脸懵。5.4 多设备多场景的调试记录我自己测试时习惯把常见的设备组合列成一个清单每改一次音频逻辑就跑一遍场景设备状态预期行为媒体播放来电外放播放中来电音乐暂停或闪避铃声响起通话中插有线耳机通话中插入耳机音频自动切到耳机听筒扬声器关闭播放中蓝牙断开蓝牙播放中断开连接媒体自动切到扬声器或暂停播放中拔掉耳机有线耳机播放拔掉媒体暂停避免扬声器突然响这个清单看着简单但每次动音频代码我都建议跑一遍因为音频问题往往在切换瞬间才暴露光看代码很难一眼看出故障。做音视频开发的一定要准备一台真机模拟器在音频设备模拟上基本不可靠。6. 常见问题与排查技巧实录最后这部分我把这些年做音频功能踩过的坑按问题类型做个速查表再说几个通用排查思路。都是真金白银买来的经验能帮你少走很多弯路。6.1 问题高频场景速查表表现可能原因排查方向音量调到最大外放还是小操作了错误的音频流系统音量曲线问题打印当前流类型检查是否用了STREAM_MUSIC播放音乐时通知音把媒体音压没没处理好AUDIOFOCUS_LOSS_TRANSIENT在焦点回调里做暂停或闪避处理插拔耳机后播放器不恢复没监听AudioDeviceCallback注册设备监听在设备移除时暂停播放蓝牙连接后通话没有音频输出没有启动SCO或路由没切换检查startBluetoothSco和setBluetoothScoOn通话结束后媒体音质变差SCO状态没关闭恢复模式关闭SCO和setBluetoothScoOn焦点恢复后播放进度丢失暂停/恢复没用实际进度记录播放位置在AUDIOFOCUS_GAIN恢复6.2 我的三个独家排查技巧第一个技巧是善用日志过滤关键词。系统音频框架的日志Tag一般是AudioManager、AudioService、AudioFlinger、AudioPolicyManager在adb里用这几个Tag组合过滤能很快看到请求音频焦点、路由切换的完整链路。很多第三方ROM还会保留自己的音频日志Tag做厂商适配时尤其管用。第二个技巧是两步走验证焦点问题。如果一个App抢了焦点导致你的App不能出声最快的定位方式是做一次最小化复现把其他App全部关掉只保留你的App播放再一步步打开可疑的App找出是哪个在抢焦点。这个办法虽然笨但在不知道是哪个三方SDK抢焦点时非常有效。第三个技巧是不要盲目信任设备状态接口。isWiredHeadsetOn、isBluetoothA2dpOn这类接口在部分机型上是被废弃或实现有偏差的尤其在国产ROM上。我后来统一改成AudioDeviceCallback加getDevices的方式可靠性高很多。我个人在实际项目里还有一个体会AudioManager相关的问题百分之八十不是API不会用而是没理解系统音频策略的全局视角。你只想让你自己的App出声但系统要平衡的是整个手机的声音秩序。在做任何音频功能之前先画清楚谁在什么时候该出声音、谁该让路、设备变化了该怎么走比多敲几行代码重要得多。如果你正准备接手一个带音频播放模块的项目我建议先花半天时间把系统日志里的音频流程过一遍再动手改代码这样后面调试起来心里会特别有底。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门