深入理解AudioService
来源:互联网 发布:电视直播软件电脑版 编辑:程序博客网 时间:2024/05/22 08:27
《深入理解Android 卷III》即将发布,作者是张大伟。此书填补了深入理解Android Framework卷中的一个主要空白,即Android Framework中和UI相关的部分。在一个特别讲究颜值的时代,本书分析了Android 4.2中WindowManagerService、ViewRoot、Input系统、StatusBar、Wallpaper等重要“颜值绘制/处理”模块
第3章 深入理解AudioService
本章主要内容:
· 探讨AudioService如何进行音量管理。
· 了解音频外设的管理机制。
· 探讨AudioFocus的工作原理。
· 介绍Android 4.1下AudioService的新特性。
本章涉及的源代码文件名及位置:
· AudioManager.Java
framework\base\media\java\android\media\AudioManager.java
· AudioService.java
framework\base\media\java\android\media\AudioService.java
· AudioSystem.java
framework\base\media\java\android\media\AudioSystem.java
· VolumePanel.java
Framework\base\core\java\android\view\VolumePanel.java
· WiredAccessoryObserver.java
Framework\base\services\java\com\android\server\WiredAccessoryObserver.java
· PhoneWindow.java
Framework\base\policy\src\com\android\internal\policy\impl\PhoneWindow.java
· Activity.java
Framework\base\core\java\android\app\Activity.java
3.1概述
通过学习对《深入理解Android:卷I》(以后简称“卷I”)第7章的学习,相信大家已经对AudioTrack、AudioRecord、音频设备路由等知识有了深入的了解。这一章将详细介绍音频系统在Java层的实现,围绕AudioService这个系统服务深入探讨在Android SDK 中看到的音频相关的机制的实现。
在分析Android音频系统时,习惯将其实现分为两个部分:数据流和策略。数据流描述了音频数据从数据源流向目的地的过程。而策略则是管理及控制数据流的路径与呈现的过程。在卷I所探讨的Native 层音频系统里,AudioTrack、AudioRecord和AudioFlinger可以被划归到数据流的范畴去讨论。而AudioPolicy相关的内容则属于策略范畴。
音频系统在Java层中基本上是不参与数据流的。虽然有AudioTrack和AudioRecord这两个类,但是他们只是Native层同名类的Java封装。抛开这两个类,AudioService这个系统服务包含或使用了几乎所的音频相关的内容,所以说AudioService是一个音频系统的大本营,它的功能非常多,而且它们之间的耦合性也不大,本章将从三个方面来探讨AudioService。
· 音量控制。
· 从按下音量键到弹出音量调提示框的过程,以及静音功能的工作原理。
· 音频IO设备的管理。
我们将详细探讨从插入耳机到声音经由耳机发出这个过程中,AudioService的工作内容。
· AudioFocus(音频焦点)机制。
AudioService在2.3及以后版本中提供了AudioFocus机制用以结束多个音频应用混乱的交互现状。音频应用在播放音频的过程中需要合理的申请与释放AudioFocus,并根据AudioFocus所有权的变化来调整自己的播放行为。我们将从音频应用开始播放音频,到播放完成的过程中探讨AudioFocus的作用及原理。
AudioService的类图如下
由图3-1可知:
· AudioService继承自IAudioService.Stub。IAudioService.Stub类很明显是通过IAudioService.aidl自动生成的。AudioService位于Bn端。
· AudioManager拥有AudioService的Bp端,是AudioService在客户端的一个代理。几乎所有客户端对AudioManager进行的请求,最终都会交由AudioService实现。
· AudioService的功能实现依赖AudioSystem类,AudioSystem无法实例化,它是java层到native层的代理。AudioService将通过它与AudioPolicyService以及AudioFlinger进行交互。
那么,开始AudioService之旅吧。
3.2 音量管理
在Android手机上有两种改变系统音量的方式。最直接的做法就是通过手机的音量键进行音量调整,还有就是从设置界面中调整某一种类型音频的音量。另外,应用程序可以随时将某种类型的音频静音。他们都是都是通过AudioService进行的。
本节将从上述的三个方面对AudioService的音量管理进行探讨。
3.2.1音量键的处理流程
1. 触发音量键
音量键被按下后,Android输入系统将该事件一路派发给Activity,如果无人截获并消费这个事件,承载当前Activity的显示的PhoneWindow类的onKeyDown()或onKeyUp()函数将会将其处理,从而开始了通过音量键调整音量的处理流程。输入事件的派发机制以及PhoneWindow类的作用将在后续章节中详细介绍,现在只需要知道,PhoneWindow描述了一片显示区域,用于显示与管理我们所看到的Activity、对话框等内容。同时,它还是输入事件的派发对象,而且只有显示在最上面的PhoneWindow才会收到事件。
注意按照Android的输入事件派发策略,Window对象在事件的派发队列中排在Activity的后面(应该说排在队尾比较合适),所以应用程序可以重写自己的onKeyDown()函数,将音量键用作其他的功能。比如说,在一个相机应用中,按下音量键所执行的动作是拍照而不是调节音量。
PhoneWindow的onKeyDown()函数实现如下:
[PhoneWindow.java-->PhoneWindow.onKeyDown()]
switch (keyCode) { case KeyEvent.KEYCODE_VOLUME_UP: case KeyEvent.KEYCODE_VOLUME_DOWN: case KeyEvent.KEYCODE_VOLUME_MUTE: { // Similar code is in PhoneFallbackEventHandler in case the window // doesn't have one of these. In this case, we execute it here and // eat the event instead, because we have mVolumeControlStreamType // and they don't. getAudioManager().handleKeyDown(event, mVolumeControlStreamType); return true; }
注意handleKeyDown()函数的第二个参数,它的意义是指定音量键将要改变哪一种流类型的音量。在Android中,音量的控制与流类型是密不可分的,每种流类型都独立地拥有自己的音量设置,绝大部分情况下互不干扰,例如音乐音量、通话音量就是相互独立的。所以说,离开流类型谈音量是没有意义的。在Android中,音量这个概念一定是描述的某一种流类型的音量。
这里传入了mVolumeControlStreamType,那么这个变量的值是从哪里来的呢?做过多媒体应用程序的读者应该知道,Activity类中有一个函数名为setVolumeControlStream(int streamType)。应用可以通过调用这个函数来指定显示这个Activity时音量键所控制的流类型。
音频流的类型在AudioSystem 中定义,
/* The audio stream for phone calls */ public static final int STREAM_VOICE_CALL = 0; /* The audio stream for system sounds */ public static final int STREAM_SYSTEM = 1; /* The audio stream for the phone ring and message alerts */ public static final int STREAM_RING = 2; /* The audio stream for music playback */ public static final int STREAM_MUSIC = 3; /* The audio stream for alarms */ public static final int STREAM_ALARM = 4; /* The audio stream for notifications */ public static final int STREAM_NOTIFICATION = 5; /* @hide The audio stream for phone calls when connected on bluetooth */ public static final int STREAM_BLUETOOTH_SCO = 6; /* @hide The audio stream for enforced system sounds in certain countries (e.g camera in Japan) */ public static final int STREAM_SYSTEM_ENFORCED = 7; /* @hide The audio stream for DTMF tones */ public static final int STREAM_DTMF = 8; /* @hide The audio stream for text to speech (TTS) */ public static final int STREAM_TTS = 9; /* @hide The audio stream for FM */ public static final int STREAM_FM = 10; public static final int STREAM_AUX = 11; public static final int STREAM_NAV = 12; public static final int STREAM_NAV_NOVICE = 13; public static final int STREAM_RECORD = 14; public static final int STREAM_VOICE = 15; public static final int STREAM_WALKIETALKIE = 16; public static final int STREAM_BLUETOOTH_MUSIC = 17; public static final int STREAM_AM = 18; /* @hide The audio stream for phone calls when connected on bluetooth */ public static final int STREAM_TBOX_SCO = 19;
AudioManager的handleKeyDown()的实现很简单,在一个switch中,它调用了AudioService的adjustSuggestedStreamVolume(),所以直接看一下AudioService的这个函数。
2. adjustSuggestedStreamVolume() 分析
先看函数原型,
public void adjustSuggestedStreamVolume(int direction, int suggestedStreamType, int flags, String callingPackage) {adjustSuggestedStreamVolume()有三个参数,而第三个参数flags的意思就不那么容易猜了。其实AudioManager在handleKeyDown()里设置了两个flags,分别是FLAG_SHOW_UI和FLAG_VIBRATE。从名字上我们就能看出一些端倪。前者用于告诉AudioService我们需要弹出一个音量控制面板。而在handleKeyUp()里设置了FLAG_PLAY_SOUND,这是为什么当松开音量键后“有时候”会有一个提示音。注意,handleKeyUp()设置了FLAG_PLAY_SOUND,但是只是有时候这个flag才会生效,我们在下面的代码中能看到为什么。还须要注意的是,第二个参数名为suggestedStreamType,从其命名来推断,这个参数传入的流类型对于AudioService来说只是一个建议,是否采纳这个建议AudioService则有自己的考虑。
/** @see AudioManager#adjustVolume(int, int) */ public void adjustSuggestedStreamVolume(int direction, int suggestedStreamType, int flags, String callingPackage) { if (DEBUG_VOL) Log.d(TAG, "adjustSuggestedStreamVolume() stream="+suggestedStreamType); int streamType; //// ①从这一小段代码中,可以看出在 AudioService中还有地方可以强行改变音量键控制的流类型 if (mVolumeControlStream != -1) { streamType = mVolumeControlStream; } else { // ②通过getActiveStreamType()函数获取要控制的流类型 // 这里根据建议的流类型与AudioService的实际情况,返回一个值 streamType = getActiveStreamType(suggestedStreamType); } Log.v(TAG, "adjustSuggestedStreamVolume() : suggestedStreamType " + suggestedStreamType + ", activeStreamType " + streamType + ", direction " + direction + ", flags " + flags); // Play sounds on STREAM_RING only and if lock screen is not on. if ((streamType != STREAM_REMOTE_MUSIC) && (flags & AudioManager.FLAG_PLAY_SOUND) != 0 && // ALIAS of STREAM_RING is MUSIC, and if getActiveStreamType returned RING, // it means no audio played. // If now playing RING, RING is also be returned. //((mStreamVolumeAlias[streamType] != AudioSystem.STREAM_RING) ((streamType != AudioSystem.STREAM_RING) || (mKeyguardManager != null && mKeyguardManager.isKeyguardLocked()))) { flags &= ~AudioManager.FLAG_PLAY_SOUND; } if (streamType == STREAM_REMOTE_MUSIC) { // don't play sounds for remote flags &= ~(AudioManager.FLAG_PLAY_SOUND|AudioManager.FLAG_FIXED_VOLUME); //if (DEBUG_VOL) Log.i(TAG, "Need to adjust remote volume: calling adjustRemoteVolume()"); mMediaFocusControl.adjustRemoteVolume(AudioSystem.STREAM_MUSIC, direction, flags); } else { //调用adjustStreamVolume adjustStreamVolume(streamType, direction, flags, callingPackage); } }
注意初看着段代码时,可能有读者会对下面这句话感到疑惑:
VolumeStreamState streamState =mStreamStates[mStreamVolumeAlias[streamType]];
其实这是为了满足所谓的“将铃声音量用作通知音量”这种需求。这样就需要实现在两个有这个需求的流A与B之间建立起一个A→B映射。当我们对A流进行音量操作时,实际上是在操作B流。
如果想要实现“以铃声音量用作音乐音量”,只需要修改相应位置的值为STREAM_RING即可,就像下面这样:
mStreamVolumeAlias[AudioSystem.STREAM_MUSIC] =AudioSystem.STREAM_RING;
之后,因为需求要求对A流进行音量操作时,实际上是在操作B流,所以就不难理解为什么在很多和流相关的函数里都会先做这样的一个转换:
streamType = mStreamVolumeAlias[streamType];
其具体的工作方式就留给读者进行思考了。在本章的分析过程中,大可忽略这种转换,这并不影响我们对音量控制原理的理解。
这个函数简单来说,做三件事:
· 确定要调整音量的流类型。
· 在某些情况下屏蔽FLAG_PLAY_SOUND。
· 调用adjustStreamVolume()。
关于这个函数仍然有几点需要说明一下。它刚开始的时候有一个判断,条件是一个名为mVolumeControlStream的整型变量是否等于-1,从这块代码来看,mVolumeControlStream比参数传入的suggestedStreamType厉害多了,只要它不是-1,那么要调整音量的流类型就是它。那这么厉害的控制手段,是做什么用的呢?其实,mVolumeControlStream是VolumePanel通过forceVolumeControlStream()函数设置的。什么是VolumePanel呢?就是我们按下音量键后的那个音量条提示框了。VolumePanel在显示时会调用forceVolumeControlStream强制后续的音量键操作固定为促使它显示的那个流类型。并在它关闭时取消这个强制设置,即置mVolumeControlStream为-1。这个我们在后面分析VolumePanel时会看到。
接下来我们继续看一下adjustStreamVolume()的实现。
3. adjustStreamVolume()分析
/** @see AudioManager#adjustStreamVolume(int, int, int) */ public void adjustStreamVolume(int streamType, int direction, int flags, String callingPackage) { if (mUseFixedVolume) { return; } if (DEBUG_VOL) Log.d(TAG, "adjustStreamVolume() stream="+streamType+", dir="+direction); ensureValidDirection(direction); ensureValidStreamType(streamType); // use stream type alias here so that streams with same alias have the same behavior, // including with regard to silent mode control (e.g the use of STREAM_RING below and in // checkForRingerModeChange() in place of STREAM_RING or STREAM_NOTIFICATION) int streamTypeAlias = mStreamVolumeAlias[streamType]; // 注意VolumeStreamState类 VolumeStreamState streamState = mStreamStates[streamTypeAlias]; //查询输出设备 final int device = getDeviceForStream(streamTypeAlias); // // 获取当前音量 int aliasIndex = streamState.getIndex(device); boolean adjustVolume = true; int step; // skip a2dp absolute volume control request when the device // is not an a2dp device if ((device & AudioSystem.DEVICE_OUT_ALL_A2DP) == 0 && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) != 0) { return; } if (mAppOps.noteOp(STEAM_VOLUME_OPS[streamTypeAlias], Binder.getCallingUid(), callingPackage) != AppOpsManager.MODE_ALLOWED) { return; } // reset any pending volume command synchronized (mSafeMediaVolumeState) { mPendingVolumeCommand = null; } ...... } else { // rescaleIndex用于将音量值的变化量从源流类型变换到目标流类型下 // 由于不同的流类型的音量调节范围不同,所以这个转换是必需的 step = rescaleIndex(10, streamType, streamTypeAlias); } // If either the client forces allowing ringer modes for this adjustment, // or the stream type is one that is affected by ringer modes if (((flags & AudioManager.FLAG_ALLOW_RINGER_MODES) != 0) || (streamTypeAlias == getMasterStreamType())) { int ringerMode = getRingerMode(); // do not vibrate if already in vibrate mode if (ringerMode == AudioManager.RINGER_MODE_VIBRATE) { flags &= ~AudioManager.FLAG_VIBRATE; } //上面准备好了所需的所有信息,接下来要做一些真正有用的动作了 // 比如说checkForRingerModeChange()。调用这个函数可能变更情景模式 // 它的返回值adjustVolume是一个布尔变量,用来表示是否有必要继续设置音量值 // 这是因为在一些情况下,音量键用来改变情景模式,而不是设置音量值 adjustVolume = checkForRingerModeChange(aliasIndex, direction, step); } // 取出调整前的音量值。这个值稍后被用在sendVolumeUpdate()的调用中 int oldIndex = mStreamStates[streamType].getIndex(device); if (adjustVolume && (direction != AudioManager.ADJUST_SAME)) { // Check if volume update should be send to AVRCP if (streamTypeAlias == AudioSystem.STREAM_MUSIC && (device & AudioSystem.DEVICE_OUT_ALL_A2DP) != 0 && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) { synchronized (mA2dpAvrcpLock) { if (mA2dp != null && mAvrcpAbsVolSupported) { mA2dp.adjustAvrcpAbsoluteVolume(direction); } } } if ((direction == AudioManager.ADJUST_RAISE) && !checkSafeMediaVolume(streamTypeAlias, aliasIndex + step, device)) { Log.e(TAG, "adjustStreamVolume() safe volume index = "+oldIndex); mVolumePanel.postDisplaySafeVolumeWarning(flags); } else if (streamState.adjustIndex(direction * step, device)) { /*2233 at the ringtone mode ,ADJUST_LOWER key can't mute the sound, it cause the second ringtone sound mute ,and can't add the volume*/ if(mMode==AudioSystem.MODE_RINGTONE && direction == AudioManager.ADJUST_LOWER){ int index1=streamState.getIndex(device); if(index1==0) streamState.adjustIndex(step, device); } // Post message to set system volume (it in turn will post a message // to persist). Do not change volume if stream is muted. // 发送消息给AudioHandler // 这个消息在setStreamVolumeInt()函数的分析中已经看到过了 // 这个消息将把音量设置到底层去,并将其存储到SettingsProvider中去 sendMsg(mAudioHandler, MSG_SET_DEVICE_VOLUME, SENDMSG_QUEUE, device, 0, /* * -- Pass original streamType (This func's input param) * to sublayers, but not ALIAS streamType */ // TODO: Review this modification // If using this modification, all other places sending MSG_SET_DEVICE_VOLUME // also need to be modified. // And the logic in setDeviceVolume() using Stream Alias needs to be modified. streamState, //mStreamStates[streamType], 0); } } int index = mStreamStates[streamType].getIndex(device); // 最后,调用sendVolumeUpdate函数,通知外界音量值发生了变化 sendVolumeUpdate(streamTypeAlias, oldIndex, index, flags); }
在这个函数的实现中,有一个非常重要的类型:VolumeStreamState。前面我们提到过,Android的音量是依赖于某种流类型的。如果Android定义了N个流类型,AudioService就需要维护N个音量值与之对应。另外每个流类型的音量等级范围不一样,所以还需要为每个流类型维护他们的音量调节范围。VolumeStreamState类的功能就是为了保存了一个流类型所有音量相关的信息。AudioService为每一种流类型都分配了一个VolumeStreamState对象,并以流类型的值为索引,保存在一个名为数组mStreamStates中。VolumeStreamState 维护一个哈希表,以 保存所有devices 类型的音量。在这个函数中调用了VolumeStreamState对象的adjustIndex()函数,于是就改变了这个哈希表中存储的音量值。不过,仅仅是改变了它的存储值,并没有把这个变化设置到底层。
注释:
RingerMode 有三种模式,RINGER_MODE_SILENT(既没有声音,也不震动), RINGER_MODE_VIBRATE(振动模式), RINGER_MODE_NORMAL( 既可能响铃,也可能震动, 具体行为取决于音量和震动设置)。
总结一下这个函数都作了什么:
· 准备工作。计算按下音量键的音量步进值。细心的读者一定注意到了,这个步进值是10而不是1。原来,在VolumeStreamState中保存的音量值是其实际值的10倍。为什么这么做呢?这是为了在不同流类型之间进行音量转换时能够保证一定精度的一种奇怪的实现,其转换过程读者可以参考rescaleIndex()函数的实现。我们可以将这种做法理解为在转换过程中保留了小数点后一位的精度。其实,直接使用float类型来保存岂不是更简单呢?
· 检查是否需要改变情景模式。checkForRingerModeChange()和情景模式有关。读者可以自行研究其实现。
· 调用adjustIndex()更改VolumeStreamState对象中保存的音量值。
· 通过sendMsg()发送消息MSG_SET_DEVICE_VOLUME到mAudioHandler。
· 调用sendVolumeUpdate()函数,通知外界音量发生了变化。
我们将重点分析后面三个内容:adjustIndex()、MSG_SET_DEVICE_VOLUME消息的处理和sendVolumeUpdate()。
4. VolumeStreamState的adjustIndex()分析
adjustIndex() 调用 public synchronized boolean setIndex(int index, int device)方法, 我们看一下setIndex方法。
public synchronized boolean setIndex(int index, int device) { int oldIndex = getIndex(device); index = getValidIndex(index); synchronized (mCameraSoundForced) { if ((mStreamType == AudioSystem.STREAM_SYSTEM_ENFORCED) && mCameraSoundForced) { index = mIndexMax; } } //保存设置的音量值 mIndex.put(device, index); if (oldIndex != index) { // Apply change to all streams using this one as alias // if changing volume of current device, also change volume of current // device on aliased stream // 同时设置所有映射到当前流类型的其他流的音量 boolean currentDevice = (device == getDeviceForStream(mStreamType)); int numStreamTypes = AudioSystem.getNumStreamTypes(); for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) { if (streamType != mStreamType && mStreamVolumeAlias[streamType] == mStreamType) { int scaledIndex = rescaleIndex(index, mStreamType, streamType); mStreamStates[streamType].setIndex(scaledIndex, device); if (currentDevice) { mStreamStates[streamType].setIndex(scaledIndex, getDeviceForStream(streamType)); } } } return true; } else { return false; } }
在这个函数中有三个工作要做:
· 首先是保存设置的音量值,这是VolumeStreamState的本职工作,这和4.1之前的版本不一样,音量值与设备相关联了。于是对于同一种流类型来说,在不同的音频设备下将会拥有不同的音量值。
· 然后就是根据参数的要求保存音量值到mLastAudibleIndex里面去。从名字就可以看出,它保存了静音前的音量。当取消静音时,AudioService就会恢复到这里保存的音量。
· 再就是对流映射的处理。既然A->B,那么设置B的音量时,同时要改变A的音量。这就是后面那个循环的作用。
可以看出,VolumeStreamState.adjustIndex()除了更新自己所保存的音量值外,没有做其他的事情,接下来就看一下MSG_SET_DEVICE_VOLUME的消息处理做了什么
其实很简单。
5. MSG_SET_DEVICE_VOLUME消息的处理
adjustStreamVolume()函数使用sendMsg()函数发送了MSG_SET_DEVICE_VOLUME消息给了mAudioHandler,这个Handler运行在AudioService的主线程上。直接看一下在mAudioHandler中负责处理MSG_SET_DEVICE_VOLUME消息的setDeviceVolume()函数:
private void setDeviceVolume(VolumeStreamState streamState, int device) { // 调用VolumeStreamState的applyDeviceVolume。 // 这个函数的内容很简单,就是在调用AudioSystem.setStreamVolumeIndex() // 到这里,音量就被设置到底层的AudioFlinger里面去了 streamState.applyDeviceVolume(device); // 和上面一样,需要处理流音量映射的情况 int numStreamTypes = AudioSystem.getNumStreamTypes(); for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) { if (streamType != streamState.mStreamType && mStreamVolumeAlias[streamType] == streamState.mStreamType) { // Make sure volume is also maxed out on A2DP device for aliased stream // that may have a different device selected int streamDevice = getDeviceForStream(streamType); if ((device != streamDevice) && mAvrcpAbsVolSupported && ((device & AudioSystem.DEVICE_OUT_ALL_A2DP) != 0)) { mStreamStates[streamType].applyDeviceVolume(device); } mStreamStates[streamType].applyDeviceVolume(streamDevice); } } // 发送消息给mAudioHandler,其处理函数将会调用persitVolume()函数这将会把音量的 //设置信息存储到SettingsProvider中 // AudioService在初始化时,将会从SettingsProvider中将音量设置读取出来并进行设置 sendMsg(mAudioHandler, MSG_PERSIST_VOLUME, SENDMSG_QUEUE, device, 0, streamState, //mStreamStates[mStreamVolumeAlias[streamState.getStreamType()]], PERSIST_DELAY); }
注意 sendMsg()是一个异步的操作,这就意味着,完成adjustIndex()更新音量信息后adjustStreamVolume()函数就返回了,但是音量并没有立刻地被设置到底层。而且由于Handler处理多个消息的过程是串行的,这就隐含着一个风险:当Handler正在处理某一个消息时发生了阻塞,那么当按下音量键时,调用adjustStreamVolume()虽然可以立刻返回,而且从界面上看或者用getStreamVolume()获取音量值发现都是没有问题的,但是手机发出声音时的音量大小并没有改变。
6. sendVolumeUpdate()分析
接下来,分析一下sendVolumeUpdate()函数,它用于通知外界音量发生了变化。
这个函数将音量的变化通过广播的形式通知给了其他感兴趣得模块。同时,它还特别通知了mVolumePanel。mVolumePanel是VolumePanel类的一个实例。我们所看到的音量调节通知框就是它了。
至此,从按下音量键开始的整个处理流程就完结了。在继续分析音量调节通知框的工作原理之前,先对之前的分析过程作一个总结,请参考下面的序列图:
图 3-2 音量键调整音量的处理流程
结合上面分析的结果,由图 3-2可知:
· 音量键处理流程的发起者是PhoneWindow。
· AudioManager仅仅起到代理的作用。
· AudioService接受AudioManager的调用请求,操作VolumeStreamState的实例进行音量的设置。
· VolumeStreamState负责保存音量设置,并且提供了将音量设置到底层的方法。
· AudioService负责将设置结果以广播的形式通知外界。
到这里,相信大家对音量量调节的流程已经有了一个比较清晰的认识了。接下来我们将介绍音量调节通知框的工作原理。
4. 音量调节通知框的工作原理
在分析sendVolumeUpdate()函数时曾经注意到它调用了mVolumePanel的postVolumeChanged()函数。mVolumePanel是一个VolumePanel的实例。作为一个Handler的子类,它承接了音量变化的UI/声音的通知工作。在继续上面的讨论之前,先了解一下其工作的基本原理。
VolumePanel下定义了两个重要的子类型,分别是StreamResources和StreamControl。StreamResources实际上是一个枚举。它的每一个可用元素保存了一个流类型的通知框所需要的各种资源,如图标、提示文字等等。其定义就像下面这样:
[VolumePanel.java-->VolumePanel.StreamResources]
private enum StreamResources {
BluetoothSCOStream(AudioManager.STREAM_BLUETOOTH_SCO,
R.string.volume_icon_description_bluetooth,
R.drawable.ic_audio_bt,
R.drawable.ic_audio_bt,
false),
// 后面的几个枚举项我们省略了其构造参数,与BluetoothSCOStream的内容是一致的
RingerStream(……),
VoiceStream(……),
AlarmStream(……),
MediaStream(……),
NotificationStream(……),
MasterStream(……),
RemoteStream(……);
intstreamType; // 流类型
intdescRes; // 描述信息
inticonRes; // 图标
inticonMuteRes;// 静音图标
booleanshow; // 是否显示
StreamResources(intstreamType, int descRes, int iconRes, int iconMuteRes, boolean show) {
……
}
};
这几个枚举项组成了一个数组名为STREAM如下:
[VolumePanel.java-->VolumePanel.STREAMS]
private static final StreamResources[] STREAMS = {
StreamResources.BluetoothSCOStream,
StreamResources.RingerStream,
StreamResources.VoiceStream,
StreamResources.MediaStream,
StreamResources.NotificationStream,
StreamResources.AlarmStream,
StreamResources.MasterStream,
StreamResources.RemoteStream
};
StreamControl类则保存了一个流类型的通知框所需要显示的控件。其定义如下:
/** Object that contains data for each slider */ private class StreamControl { int streamType; ViewGroup group; ImageView icon; LinearLayout seekbarView; int iconRes; int iconMuteRes; }
很简单对不对?StreamControl实例中保存了音量条提示框中所需的所用控件。关于这个类在VolumePanel的使用,我们可能很直观的认为只有一个StreamControl实例,在对话框显示时,使其保存的控件按需加载指定流类型的StreamResources实例中定义的资源。其实不然,应该是出于对运行效率的考虑,StreamControl实例也是每个流类型人手一份,和StreamResources实例形成了一个一一对应的关系。所有的StreamControl 实例被保存在了一个以流类型的值为键的Hashtable中,名为mStreamControls。我们可以在StreamControl的初始化函数createSliders()中一窥其端倪:
[VolumePanel-->VolumePanel.createSliders()]
private void createSliders() {
……
// 遍历STREAM中所有的StreamResources实例
for (inti = 0; i < STREAMS.length; i++) {
StreamResources streamRes = STREAMS[i];
intstreamType = streamRes.streamType;
……
// 为streamType创建一个StreamControl
StreamControl sc = new StreamControl();
// 这里将初始化sc的成员变量
……
// 将初始化好的sc放入mStreamControls中去。
mStreamControls.put(streamType, sc);
}
}
值得一提的是,这个初始化的工作并没有在构造函数中进行,而是在postVolumeChanged()函数里处理的。
既然已经有了通知框所需要的资源和通知框的控件了,那么接下来就要有一个对话框承载它们。没错,VolumePanel保存了一个名为mDialog的Dialog实例,这就是通知框的本尊了。每当有新的音量变化到来时,mDialog的内容就会被替换为制定流类型对应的StreamControl中所保存的控件,并根据音量变化情况设置其音量条的位置,最后调用mDialog.show()显示出来。同时,发送一个延时消息MSG_TIMEOUT,这条延时消息生效时,将会关闭提示框。
StreamResource、StreamControl与mDialog的关系就像下面这附图一样,StreamControl可以说是mDialog的配件,随需拆卸。
图 3-3 StreamResource、StreamControl与mDialog的关系
接下来具体看一下VolumePanel在收到音量变化通知后都做了什么。我们在上一小节中说到了mVolumePanel.postVolumeChanged()函数。它的内容很简单,直接发送了一条消息MSG_VOLUME_CHANGED,然后在handleMessage中调用onVolumeChanged()函数进行真正的处理。
注意 VolumePanel在MSG_VOLUME_CHANGED的消息处理函数中调用onVolumeChanged()函数而不直接在postVolumeChanged()函数中直接调,。这么做是有实际意义的。由于Android要求只能在创建控件的线程中对控件进行操作。postVolumeChanged()作为一个回调性质的函数,不能要求调用者位于哪个线程中。所以必须通过向Handler发送消息的方式,将后续的操作转移到指定的线程中去。在大家设计具有UI Controller功能的类时,VolumePanel的实现方式有很好的参考意义。
注意最后一个resetTimeout()的调用。它其实是重新延时发送了MSG_TIMEOUT消息。当MSG_TIMEOUT消息生效时,mDialog将会被关闭。
之后就是onShowVolumeChanged了。这个函数负责为通知框的内容填充音量、图表等信息,然后再把通知框显示出来,如果还没有显示的话。以铃声音量为例,省略掉其他的代码。
[VolumePanel.java-->VolumePanel.onShowVolumeChanged()]
protectedvoid onShowVolumeChanged(int streamType, int flags) {
// 获取音量值
intindex = getStreamVolume(streamType);
// 获取音量最大值,这两个将用来设置进度条
intmax = getStreamMaxVolume(streamType);
switch (streamType) {
// 这个switch语句中,我们要根据每种流类型的特点,进行各种调整。
// 例如Music就有时就需要更新它的图标,因为使用蓝牙耳机时的图标和和平时的不一样
// 所以每一次都需要更新一下
case AudioManager.STREAM_MUSIC: {
// Special case for when Bluetooth is active for music
if ((mAudioManager.getDevicesForStream(AudioManager.STREAM_MUSIC) &
(AudioManager.DEVICE_OUT_BLUETOOTH_A2DP |
AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES|
AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER)) != 0) {
setMusicIcon(R.drawable.ic_audio_bt,
R.drawable.ic_audio_bt_mute);// 设置蓝牙图标
} else {
setMusicIcon(R.drawable.ic_audio_vol,
R.drawable.ic_audio_vol_mute);//设置为普通图标
}
break;
}
……
}
// 取出流类型对应的StreamControl。并设置其SeekBar的音量显示
StreamControl sc = mStreamControls.get(streamType);
if(sc != null) {
if (sc.seekbarView.getMax() != max) {
sc.seekbarView.setMax(max);
}
sc.seekbarView.setProgress(index);
……
}
if(!mDialog.isShowing()) { // 如果对话框还没有显示
/* forceVolumeControlStream()的调用在这里,一旦此通知框被显示,之后的按下音量键,都只能调节当前流类型的音量。直到通知框关闭时,重新调用forceVolumeControlStream(),并设置streamType为-1。*/
mAudioManager.forceVolumeControlStream(streamType);
// 为Dialog设置显示控件
// 注意,mView目前已经在reorderSlider()函数中安装好了Music流所对应的
//StreamControl了
mDialog.setContentView(mView);
……
//显示对话框
mDialog.show();
}
}
至此,音量条提示框就被显示出来了。总结一下它的工作过程:
· postVolumeChanged() 是VolumePanel显示的入口。
· 检查flags中是否有FLAG_SHOW_UI。
· VolumePanel会在第一次被要求弹出时初始化其控件资源。
· mDialog 加载指定流类型对应的StreamControl,也就是控件。
· 显示对话框,并开始超时计时。
· 超时计时到达,关闭对话框。
到此为止,AudioService对音量键的处理流程就介绍完了。而 Android还有另外一种改变音量的方式。
3.2.2 通用的音量设置函数setStreamVolume()
除了通过音量键可以调节音量以外,用户还可以在系统设置中进行调节。AudioManager.setStreamVolume()是系统设置界面中调整音量所使用的接口。
1. setStreamVolume()分析
public void setStreamVolume(int streamType, int index, int flags, String callingPackage) { if (mUseFixedVolume) { return; } ensureValidStreamType(streamType); int streamTypeAlias = mStreamVolumeAlias[streamType]; VolumeStreamState streamState = mStreamStates[streamTypeAlias]; final int device = getDeviceForStream(streamType); int oldIndex = streamState.getIndex(device); if (mHighTemp == true) { Log.i(TAG, "High Temp stream " + streamType + " volume from " + (oldIndex / 10) + " to " + index); if (index > mHighTempVolume) { Log.i(TAG, "High Temp toast"); mVolumePanel.postVolumeChanged(streamType, AudioManager.FLAG_HIGH_TEMP); return; } } // skip a2dp absolute volume control request when the device // is not an a2dp device if ((device & AudioSystem.DEVICE_OUT_ALL_A2DP) == 0 && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) != 0) { return; } if (mAppOps.noteOp(STEAM_VOLUME_OPS[streamTypeAlias], Binder.getCallingUid(), callingPackage) != AppOpsManager.MODE_ALLOWED) { return; } synchronized (mSafeMediaVolumeState) { // reset any pending volume command mPendingVolumeCommand = null; oldIndex = streamState.getIndex(device); index = rescaleIndex(index * 10, streamType, streamTypeAlias); if (streamTypeAlias == AudioSystem.STREAM_MUSIC && (device & AudioSystem.DEVICE_OUT_ALL_A2DP) != 0 && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) { synchronized (mA2dpAvrcpLock) { if (mA2dp != null && mAvrcpAbsVolSupported) { mA2dp.setAvrcpAbsoluteVolume(index); } } } flags &= ~AudioManager.FLAG_FIXED_VOLUME; if ((streamTypeAlias == AudioSystem.STREAM_MUSIC) && ((device & mFixedVolumeDevices) != 0)) { flags |= AudioManager.FLAG_FIXED_VOLUME; // volume is either 0 or max allowed for fixed volume devices if (index != 0) { if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE && (device & mSafeMediaVolumeDevices) != 0) { index = mSafeMediaVolumeIndex; } else { index = streamState.getMaxIndex(); } } } if (!checkSafeMediaVolume(streamTypeAlias, index, device)) { mVolumePanel.postDisplaySafeVolumeWarning(flags); mPendingVolumeCommand = new StreamVolumeCommand( streamType, index, flags, device); } else { // onSetStreamVolume 方法调用setStreamVolumeInt() onSetStreamVolume(streamType, index, flags, device); index = mStreamStates[streamType].getIndex(device); } } // 广播通知 sendVolumeUpdate(streamType, oldIndex, index, flags); }
看明白这个函数了吗?它的工作其实很简单的,就是执行下面这三方面的工作:
· 为调用setStreamVolumeInt准备参数。
· 调用setStreamVolumeInt(通过onSetStreamVolume)。
· 广播音量发生变化的通知。
分析的主线将转向setStreamVolumeInt()的内容了
2. setStreamVolumeInt()分析
[AudioService.java-->AudioService.setStreamVolumeInt()]
private void setStreamVolumeInt(int streamType,
int index,
int device,
boolean force,
booleanlastAudible) {
// 获取保存音量信息的VolumeStreamState对象
VolumeStreamState streamState = mStreamStates[streamType];
if(streamState.muteCount() != 0) {
// 这里的内容是为了处理当流已经被静音后的情况。我们在讨论静音的实现时在考虑这段代码
……
} else {
// 调用streamState.setIndex()
if(streamState.setIndex(index, device, lastAudible) || force) {
// 如果setIndex返回true或者force参数为true的话就在这里给mAudioHandler
//
sendMsg(mAudioHandler,
MSG_SET_DEVICE_VOLUME,
SENDMSG_QUEUE,
device,
0,
streamState,
0);
}
}
}
此函数有两个工作,一个是streamState.setIndex() 另一个则是根据setIndex()的返回值和force参数决定是否要发送MSG_SET_DEVICE_VOLUME消息。这两个内容在3.2.1节中已经又介绍了。在此不再赘述。
其执行过程可以参考下面的序列图:
3.2.3 静音控制
静音控制的情况与音量调节又很大的不同。因为每个应用都有可能进行静音操作,所以为了防止状态发生紊乱,就需要为静音操作进行计数。
不过,如果进行了静音计数后还会引入另外一个问题。如果一个应用在静音操作(计数加1)后因为某种原因不小心挂了,那么将不会有人再为它进行取消静音的操作,静音计数无法再回到0,也就是说这个倒霉的流将被永远静音下去。
那么怎么处理应用异常退出后的静音计数呢?AudioService的解决办法是记录下来每个应用的自己的静音计数,当应用崩溃时,在总的静音计数中减去崩溃应用自己的静音计数,也就是说,由我们为这个应用完成它没能完成的取消静音这个操作。为此,VolumeStreamState定义了一个继承自DeathRecepient的内部类名为VolumeDeathHandler,并为每个进行静音操作的进程创建一个实例。它保存了对应进程的静音计数,并在进程死亡时进行计数清零的操作。从这个名字来看可能是Google希望这个类将来能够承担更多与音量相关的事情吧,不过眼下它只负责静音。我们将在后续的内容对这个类进行深入的讲解。
经过前面的介绍,我们不难得出AudioService、VolumeStreamState与VolumeDeathHandler的关系如下:
图 3‑4 与静音相关的类
1. setStreamMute()分析
同音量设置一样,静音控制也是相对于某一个流类型而言的。而且正如本节开头所提到的,静音控制涉及到引用计数和客户端进程的死亡监控。所以相对与音量控制来说,静音控制有一定的复杂度。不过还好,静音控制对外入口只有一个函数,就是AudioManager.setStreamMute()。第二个参数state为true表示静音,否则为解除静音。
public void setStreamMute(int streamType, boolean state) { IAudioService service = getService(); try { // 调用AudioService的setStreamMute,注意第三个参数mICallBack。 service.setStreamMute(streamType, state, mICallBack); } catch (RemoteException e) { Log.e(TAG, "Dead object in setStreamMute", e); } }
AudioManager一如既往地充当着一个AudioService代理的一个角色。但是这次有一个小小的却很重要的动作。AudioManager给AudioService传入了一个名为mICallBack的变量。查看一下它的定义:
private final IBinder mICallBack = new Binder();
真是简单得不得了。全文搜索一下,我们发现它只被用来作为AudioService的几个函数调用的参数。从AudioManager这边看来它没有任何实际意义。其实,这在Android中进程间交互通讯中是一种常见且非常重要的技术。mICallBack这个简单的变量可以充当Bp端在Bn端的一个唯一标识。Bn端,也就是AudioService拿到这个标识后,就可以通过DeathRecipient机制获取到Bp端异常退出的回调。这是AudioService维持静音状态正常变迁的一个基石。
注意服务端把客户端传入的这个Binder对象作为客户端的一个唯一标识,能做的事情不仅仅DeathRecipient这一个。还以这个标识为键创建一个Hashtable,用来保存每个客户端相关信息。这在Android各个系统服务的实现中是一种很常见的用法。
也可以参考我的博客:跨进程双向通信(http://blog.csdn.net/shizhonghuo19870328/article/details/72765355).
2. VolumeDeathHandler分析
我们继续跟踪AudioService.setStreamMute()的实现,记得注意第三个参数cb,它是代表特定客户端的标识。
public void setStreamMute(int streamType, boolean state, IBinder cb) { if (mUseFixedVolume) { return; } // 只有可以静音的流类型才能执行静音操作。这说明,并不是所有的流都可以被静音 if (isStreamAffectedByMute(streamType)) { // 直接调用了流类型对应的mStreamStates的mute()函数 mStreamStates[streamType].mute(cb, state); } }
接下来是VolumeStreamState的mute()函数。VolumeStreamState的确是音量相关操作的核心类型。
public synchronized void mute(IBinder cb, boolean state) { // 这句话是一个重点,VolumeDeathHandler与cb一一对应,用来管理客户端的静音操作,并且监控客户端的生命状态 // 当cd 是第一次调用,静音命令 新建VolumeDeathHandler 并返回, 取消静音返回null. VolumeDeathHandler handler = getDeathHandler(cb, state); int muteCount = mDeathHandlers.size(); int oldIndex = getIndex(AudioSystem.DEVICE_OUT_DEFAULT); if (state == true) { if (handler == null) { Log.e(TAG, "Could not get client death handler for stream: "+mStreamType); return; } // 静音命令, handler.mute(state); } else { // 我们设备的特殊要求, 取消静音讲所有的客户端静音都取消。 int size = mDeathHandlers.size(); for (int i = size - 1; i >= 0; i--) { handler = mDeathHandlers.get(i); handler.mute(state); } }
上述代码引入了静音控制的主角,VolumeDeathHandler,也许叫做MuteHandler更合适一些。它其实只有两个成员变量,分别是mICallBack和mMuteCount。其中mICallBack保存了客户端的传进来的标识,mMuteCount则保存了当前客户端执行静音操作的引用计数。另外,它继承自IBinder.DeathRecipient,所以它拥有监听客户端生命状态的能力。而成员函数则只有两个,分别是mute()和binderDied()。说到这里,再看看上面VolumeStreamState.mute()的实现,读者能否先想想VolumeDeathHandler的具体实现是什么样子的么?
继续上面的脚步,看一下它的mute()函数。它的参数state的取值指定了进行静音还是取消静音。所以这个函数也就分成了两部分,分别处理静音与取消静音两个操作。其实,这完全可以放在两个函数中完成。先看看静音操作是怎么做的吧。
// must be called while synchronized on parent VolumeStreamState public void mute(boolean state) { boolean updateVolume = false; if (state) { if (mMuteCount == 0) { // 如果mMuteCount等于0,则表示客户端是第一次执行静音操作 //此时我们linkToDeath,开始对客户端的生命状况进行监听 //这样做的好处是可以避免非静音状态下对Binder资源的额外占用 try { // mICallback can be 0 if muted by AudioService if (mICallback != null) { mICallback.linkToDeath(this, 0); } //保存的mDeathHandlers列表中去 VolumeStreamState.this.mDeathHandlers.add(this); // 如果流没有静音, 则 重新设置静音 if (!VolumeStreamState.this.isMuted()) { updateVolume = true; } } catch (RemoteException e) { // Client has died! binderDied(); return; } } else { Log.w(TAG, "stream: "+mStreamType+" was already muted by this client"); } //如果静音多次, 只记一次 mMuteCount++; if (mMuteCount > 1) mMuteCount = 1; } else { if (mMuteCount == 0) { Log.e(TAG, "unexpected unmute for stream: "+mStreamType); } else { mMuteCount--; if (mMuteCount == 0) { // Unregister from client death notification VolumeStreamState.this.mDeathHandlers.remove(this); // mICallback can be 0 if muted by AudioService if (mICallback != null) { mICallback.unlinkToDeath(this, 0); } if (!VolumeStreamState.this.isMuted()) { updateVolume = true; } } } } if (updateVolume) { sendMsg(mAudioHandler, MSG_SET_ALL_VOLUMES, SENDMSG_QUEUE, 0, 0, VolumeStreamState.this, 0); } }
mAudioHandler接收消息MSG_SET_ALL_VOLUMES 后调用setAllVolumes 方法。
private void setAllVolumes(VolumeStreamState streamState) { // Apply volume streamState.applyAllVolumes(); // Apply change to all streams using this one as alias int numStreamTypes = AudioSystem.getNumStreamTypes(); for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) { if (streamType != streamState.mStreamType && mStreamVolumeAlias[streamType] == streamState.mStreamType) { mStreamStates[streamType].applyAllVolumes(); } } }
public synchronized void applyAllVolumes() { // apply default volume first: by convention this will reset all // devices volumes in audio policy manager to the supplied value int index; // 根据isMuted() 判断是设置静音还是取消静音 if (isMuted()) { index = 0; } else { index = (getIndex(AudioSystem.DEVICE_OUT_DEFAULT) + 5)/10; } // 设置default devices 的是否静音 AudioSystem.setStreamVolumeIndex(mStreamType, index, AudioSystem.DEVICE_OUT_DEFAULT); // then apply device specific volumes Set set = mIndex.entrySet(); Iterator i = set.iterator(); while (i.hasNext()) { Map.Entry entry = (Map.Entry)i.next(); int device = ((Integer)entry.getKey()).intValue(); if (device != AudioSystem.DEVICE_OUT_DEFAULT) { if (isMuted()) { index = 0; } else if ((device & AudioSystem.DEVICE_OUT_ALL_A2DP) != 0 && mAvrcpAbsVolSupported) { index = (mIndexMax + 5)/10; } else { index = ((Integer)entry.getValue() + 5)/10; } //// 设置非default devices 的是否静音 AudioSystem.setStreamVolumeIndex(mStreamType, index, device); } } }
然后就剩下最后的binderDied()函数了。当客户端发生异常,没能取消其执行过的静音操作时,需要替它完成它应该做却没做的事情。
public void binderDied() {
Log.w(TAG, "Volume service client died for stream: "+mStreamType);
if (mMuteCount != 0) {
// Reset all active mute requests from this client.
mMuteCount = 1;
mute(false);
}
}
}
3.2.4 音量控制总结
音量控制是AudioService最重要的功能之一。经过上面的讨论,相信读者对AudioService的音量管理流程已经有了一定的理解。
总结一下我们在这一节里所学到的内容:
· AudioService音量管理的核心是VolumeStreamState。它保存了一个流类型所有的音量信息。
· VolumeStreamState保存了运行时的音量信息,而音量的生效则是在底层AudioFlinger完成。所以音量设置需要做两件事情,更新VolumeStreamState存储的音量值。设置音量到Audio底层系统。
· VolumeDeathHandler是VolumeStreamState的一个内部类。它的实例对应了在一个流类型上执行了静音操作的一个客户端,是实现静音功能的核心对象。
本文参考了阿拉神农的文章http://blog.csdn.net/innost/article/details/47419025。 基于我的代码与其文章有一些不同, 加上了我的一些总结。
谢谢阿拉神农 的分享。
- 深入理解AudioService
- 《深入理解Android 卷III》第三章 深入理解AudioService
- 《深入理解Android 卷III》第三章 深入理解AudioService
- 《深入理解Android 卷III》第三章 深入理解AudioService
- 深入理解Android卷III 第3章 深入理解AudioService (节选)
- AudioService
- 深入理解
- AudioService调整stream音量
- AudioService源码分析
- Android - AudioService(Java)
- 音频管理AudioService
- 002 AudioService音频设备管理
- 对AudioService 的认识(1)
- AudioService 之 AudioFocus简单例子
- Android笔记-AudioSystem与AudioService
- MapReduce理解-深入理解MapReduce
- 深入理解动态库
- 深入理解sizeof
- 射线检测
- [web]ES6学习笔记(二)
- ORACLE 创建视图时,出现权限不足 ORA-01031:权限不足
- linux安装c++版本eclipse以及编译增加指定库
- JavaWeb
- 深入理解AudioService
- 168题引出的思考
- Mac 解压rar格式文件(附解压工具包)
- ubuntu14.04右键菜单添加sublime打开选项
- DAM debug
- HTML5本地视频播放器设计(+弹幕功能)
- StringBuffer和StringBuilder的区别
- JAVA中的反射机制
- udevd worker unexpectedly returned with status 0x0100