转载:https://blog.youkuaiyun.com/Innost/article/details/47419025
第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
由图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) {
caseKeyEvent.KEYCODE_VOLUME_UP:
caseKeyEvent.KEYCODE_VOLUME_DOWN:
case KeyEvent.KEYCODE_VOLUME_MUTE: {
// 直接调用到AudioManager的handleKeyUp里面去了。
//第二个参数,它的意义是指定音量键将要改变哪一种流类型的音量
getAudioManager().handleKeyDown(event,mVolumeControlStreamType);
return true;
}
……
}
注意handleKeyDown()函数的第二个参数,它的意义是指定音量键将要改变哪一种流类型的音量。在Android中,音量的控制与流类型是密不可分的,每种流类型都独立地拥有自己的音量设置,绝大部分情况下互不干扰,例如音乐音量、通话音量就是相互独立的。所以说,离开流类型谈音量是没有意义的。在Android中,音量这个概念一定是描述的某一种流类型的音量。
这里传入了mVolumeControlStreamType,那么这个变量的值是从哪里来的呢?做过多媒体应用程序的读者应该知道,Activity类中有一个函数名为setVolumeControlStream(int streamType)。应用可以通过调用这个函数来指定显示这个Activity时音量键所控制的流类型。这个函数的内容很简单,就一行如下:
[Activity.java-->Activity.setVolumeControlStream()]
getWindow().setVolumeControlStream(streamType);
getWindow()的返回值的就是用于显示当前Activity的PhoneWindow。从名字就可以看出,这个调用改变了mVolumeControlStreamType,于是也就改变了按下音量键后传入AudioManager.handleKeyUp()函数的第二个参数,从而达到了setVolumeControlStream的目的。同时,还应该能看出,这个设置是被绑定到Activity的Window上的,不同Activity之间切换时,接受按键事件的Window也会随之切换,所以应用不需要去考虑在其生命周期中音量键所控制的流类型的切换问题。
AudioManager的handleKeyDown()的实现很简单,在一个switch中,它调用了AudioService的adjustSuggestedStreamVolume(),所以直接看一下AudioService的这个函数。
2. adjustSuggestedStreamVolume()分析
我们先来看函数原型,
public void adjustSuggestedStreamVolume(int direction,
int suggestedStreamType,
int flags)
adjustSuggestedStreamVolume()有三个参数,而第三个参数flags的意思就不那么容易猜了。其实AudioManager在handleKeyDown()里设置了两个flags,分别是FLAG_SHOW_UI和FLAG_VIBRATE。从名字上我们就能看出一些端倪。前者用于告诉AudioService我们需要弹出一个音量控制面板。而在handleKeyUp()里设置了FLAG_PLAY_SOUND,这是为什么当松开音量键后“有时候”会有一个提示音。注意,handleKeyUp()设置了FLAG_PLAY_SOUND,但是只是有时候这个flag才会生效,我们在下面的代码中能看到为什么。还须要注意的是,第二个参数名为suggestedStreamType,从其命名来推断,这个参数传入的流类型对于AudioService来说只是一个建议,是否采纳这个建议AudioService则有自己的考虑。
[AudioService.java-->AudioService.adjustSuggestedStreamVolume()]
public void adjustSuggestedStreamVolume(intdirection, int suggestedStreamType,
int flags) {格式要调整好
int streamType;
// ①从这一小段代码中,可以看出在 AudioService中还有地方可以强行改变音量键控制的流类型
if(mVolumeControlStream != -1) {
streamType = mVolumeControlStream;
} else {
// ②通过getActiveStreamType()函数获取要控制的流类型
// 这里根据建议的流类型与AudioService的实际情况,返回一个值
streamType = getActiveStreamType(suggestedStreamType);
}
// ③这个啰嗦的if判断的目的,就是只有在特定的流类型下,并且没有处于锁屏状态时才会播放声音
if((streamType != STREAM_REMOTE_MUSIC) &&
(flags & AudioManager.FLAG_PLAY_SOUND) != 0 &&
((mStreamVolumeAlias[streamType] != AudioSystem.STREAM_RING)
|| (mKeyguardManager != null &&mKeyguardManager.isKeyguardLocked()))) {
flags&= ~AudioManager.FLAG_PLAY_SOUND;
}
if(streamType == STREAM_REMOTE_MUSIC) {
…… //我们不讨论远程播放的情况
} else {
// ④调用adjustStreamVolume
adjustStreamVolume(streamType, direction, flags);
}
}
注意初看着段代码时,可能有读者会对下面这句话感到疑惑:
VolumeStreamState streamState =mStreamStates[mStreamVolumeAlias[streamType]];
其实这是为了满足所谓的“将 铃声音量 用作 通知音量”这种需求。这样就需要实现在两个有这个需求的流A与B之间建立起一个A→B映射。当我们对A流进行音量操作时,实际上是在操作B流。其实笔者个人认为这个功能对用户体验的提升并不大,但是却给AudioService的实现增加了不小的复杂度。直观上来想,我们可能想使用一个HashMap解决这个问题,键是源流类型,值目标流类型。而Android使用了一个更简单那但是却不是那么好理解的方法来完成这件事。AudioService用一个名为mStreamVolumeAlias的整形数组来描述这个映射关系。
如果想要实现“以铃声音量用作音乐音量”,只需要修改相应位置的值为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()分析
[AudioService.java-->AudioService.adjustStreamVolume()]
public void adjustStreamVolume(int streamType, intdirection, int flags) {
// 首先还是获取streamType映射到的流类型。这个映射的机制确实给我们的分析带来不小的干扰
// 在非必要的情况下忽略它们吧
int streamTypeAlias = mStreamVolumeAlias[streamType];
// 注意VolumeStreamState类
VolumeStreamState streamState = mStreamStates[streamTypeAlias];
final intdevice = getDeviceForStream(streamTypeAlias);
// 获取当前音量,注意第二个参数的值,它的目的是如果这个流被静音,则取出它被静音前的音量
final intaliasIndex = streamState.getIndex(device,
(streamState.muteCount()!= 0)
booleanadjustVolume = true;
// rescaleIndex用于将音量值的变化量从源流类型变换到目标流类型下
// 由于不同的流类型的音量调节范围不同,所以这个转换是必需的
int step= rescaleIndex(10, streamType, streamTypeAlias);
//上面准备好了所需的所有信息,接下来要做一些真正有用的动作了
// 比如说checkForRingerModeChange()。调用这个函数可能变更情景模式
// 它的返回值adjustVolume是一个布尔变量,用来表示是否有必要继续设置音量值
// 这是因为在一些情况下,音量键用来改变情景模式,而不是设置音量值
if(((flags & AudioManager.FLAG_ALLOW_RINGER_MODES) != 0) ||
(streamTypeAlias == getMasterStreamType())) {
……
adjustVolume = checkForRingerModeChange(aliasIndex, direction, step);
……
}
int index;
// 取出调整前的音量值。这个值稍后被用在sendVolumeUpdate()的调用中
final int oldIndex = mStreamStates[streamType].getIndex(device,
(mStreamStates[streamType].muteCount() != 0) /* lastAudible */);
// 接下来我们可以看到,只有流没有被静音时,才会设置音量到底层去,否则只调整其静音前的音量
// 为了简单起见,暂不考虑静音时的情况
if(streamState.muteCount() != 0) {
……
} else {
// 为什么还要判断streamState.adjustIndex的返回值呢?
// 因为如果音量值在adjust之后并没有发生变化,比如说达到了最大值,就不需要继续后面的操作了
if(adjustVolume && streamState.adjustIndex(direction * step, device)) {
// 发送消息给AudioHandler
// 这个消息在setStreamVolumeInt()函数的分析中已经看到过了
// 这个消息将把音量设置到底层去,并将其存储到SettingsProvider中去
sendMsg(mAudioHandler,
MSG_SET_DEVICE_VOLUME,
SENDMSG_QUEUE,
device,
0,
streamState,
0);
}
index= mStreamStates[streamType].getIndex(device, false /* lastAudible */);
}
// 最后,调用sendVolumeUpdate函数,通知外界音量值发生了变化
sendVolumeUpdate(streamType, oldIndex, index, flags);
}
在这个函数的实现中,有一个非常重要的类型:VolumeStreamState。前面我们提到过,Android的音量是依赖于某种流类型的。如果Android定义了N个流类型,AudioService就需要维护N个音量值与之对应。另外每个流类型的音量等级范围不一样,所以还需要为每个流类型维护他们的音量调节范围。VolumeStreamState类的功能就是为了保存了一个流类型所有音量相关的信息。AudioService为每一种流类型都分配了一个VolumeStreamState对象,并以流类型的值为索引,保存在一个名为数组mStreamStates中。在这个函数中调用了VolumeStreamState对象的adjustIndex()函数,于是就改变了这个对象中存储的音量值。不过,仅仅是改变了它的存储值,并没有把这个变化设置到底层。
总结一下这个函数都作了什么:
· 准备工作。计算按下音量键的音量步进值。细心的读者一定注意到了,这个步进值是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()分析
我们看一下这个函数的定义:
[AudioService.java-->VolumeStreamState.adjustIndex()]
public boolean adjustIndex(int deltaIndex, intdevice) {
// 将现有的音量值加上变化量,然后调用setIndex设置下去
// 返回值与setIndex一样
return setIndex(getIndex(device, false /* lastAudible */) + deltaIndex,
device,
true /* lastAudible */);
}
这个函数很简单,我们再看一下setIndex()的实现:
[AudioService.java-->VolumeStreamState.setIndex()]
public synchronized boolean setIndex(int index, intdevice, boolean lastAudible) {
int oldIndex = getIndex(device, false /*lastAudible */);
index =getValidIndex(index);
// 在VolumeStreamState中保存设置的音量值,注意是用了一个HashMap
mIndex.put(device, getValidIndex(index));
if(oldIndex != index) {
// 保存到lastAudible
//保存了静音前的音量。当取消静音时,AudioService就会恢复到这里保存的音量。
if(lastAudible) {
mLastAudibleIndex.put(device, index);
}
// 同时设置所有映射到当前流类型的其他流的音量
boolean currentDevice = (device == getDeviceForStream(mStreamType));
intnumStreamTypes = AudioSystem.getNumStreamTypes();
for(int streamType = numStreamTypes - 1; streamType >= 0; 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()函数:
[AudioService.java-->AudioHandler.setIndex()]
private void setDeviceVolume(VolumeStreamStatestreamState, int device) {
// 调用VolumeStreamState的applyDeviceVolume。
// 这个函数的内容很简单,就是在调用AudioSystem.setStreamVolumeIndex()
// 到这里,音量就被设置到底层的AudioFlinger里面去了
streamState.applyDeviceVolume(device);
// 和上面一样,需要处理流音量映射的情况。这段代码和上面setIndex的相关代码很像,不是么
intnumStreamTypes = AudioSystem.getNumStreamTypes();
for (int streamType = numStreamTypes - 1; streamType >= 0;streamType--) {
……
}
}
// 发送消息给mAudioHandler,其处理函数将会调用persitVolume()函数这将会把音量的
//设置信息存储到SettingsProvider中
// AudioService在初始化时,将会从SettingsProvider中将音量设置读取出来并进行设置
sendMsg(mAudioHandler,
MSG_PERSIST_VOLUME,
SENDMSG_QUEUE,
PERSIST_CURRENT|PERSIST_LAST_AUDIBLE,
device,
streamState,
PERSIST_DELAY);
}
注意 sendMsg()是一个异步的操作,这就意味着,完成adjustIndex()更新音量信息后adjustStreamVolume()函数就返回了,但是音量并没有立刻地被设置到底层。而且由于Handler处理多个消息的过程是串行的,这就隐含着一个风险:当Handler正在处理某一个消息时发生了阻塞,那么当按下音量键时,调用adjustStreamVolume()虽然可以立刻返回,而且从界面上看或者用getStreamVolume()获取音量值发现都是没有问题的,但是手机发出声音时的音量大小并没有改变。
6. sendVolumeUpdate()分析
接下来,分析一下sendVolumeUpdate()函数,它用于通知外界音量发生了变化。
[AudioService.java-->AudioService.sendVolumeUpdate()]
private void sendVolumeUpdate(int streamType, intoldIndex, int index, int flags) {
// 读者可能会对这句话感到有点奇怪,mVoiceCapable是从SettingsProvider中取出来的一个常量
// 从某种意义上来说,它可以用来判断设备是否拥有通话功能。对于没有通话能力的设备来说,RING流类
// 型自然也就没有意义了。这句话应该算是一种从语义操作上进行的保护
if(!mVoiceCapable && (streamType == AudioSystem.STREAM_RING)) {
streamType = AudioSystem.STREAM_NOTIFICATION;
}
//mVolumePanel是一个VolumePanel类的实例,就是它显示了音量提示框
mVolumePanel.postVolumeChanged(streamType, flags);
// 发送广播。通知其他模块,可以看到它们都有(x+5)/10的一个操作。为什么要除以10可以理解,但是+5的意义呢
// 原来是为了实现四舍五入
oldIndex= (oldIndex + 5) / 10;
index =(index + 5) / 10;
Intentintent = new Intent(AudioManager.VOLUME_CHANGED_ACTION);
intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, streamType);
intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, index);
intent.putExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, oldIndex);
mContext.sendBroadcast(intent);
}
这个函数将音量的变化通过广播的形式通知给了其他感兴趣得模块。同时,它还特别通知了mVolumePanel。mVolumePanel是VolumePanel类的一个实例。我们所看到的音量调节通知框就是它了。
至此,从按下音量键开始的整个处理流程就完结了。在继续分析音量调节通知框的工作原李之前,先对之前的分析过程作一个总结,请参考下面的序列图:
图 3-2 音量键调整音量的处理流程
结合上面分析的结果,由图 3-2可知:
· 音量键处理流程的发起者是PhoneWindow。
· AudioManager仅仅起到代理的作用。
· AudioService接受AudioManager的调用请求,操作VolumeStreamState的实例进行音量的设置。
· VolumeStreamState负责保存音量设置,并且提供了将音量设置到底层的方法。
· AudioService负责将设置结果以广播的形式通知外界。
到这里,相信大家对音量量调节的流程已经有了一个比较清晰的认识了。接下来我们将介绍音量调节通知框的工作原理。
4. 音量调节通知框的工作原理
在分析sendVolumeUpdate()函数时曾经注意到它调用了mVolumePanel的postVolumeChanged()函数。mVolumePanel是一个VolumePanel的实例。作为一个Handler的子类,它承接了音量变化的UI/声音的通知工作。在继续上面的讨论之前,先了解一下其工作的基本原理。
VolumePanel位于android.view包下,但是却没有在API中被提供。因为它只能被AudioService使用,所以和AudioService放在一个包下可能更合理一些。从这个类的注释上可以看到,谷歌的开发人员对它被放在android.view下也有极大的不满(What A Mass! 他们这么写道……)。
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
};
VolumePanel将从这个STREAMS数组中获取它所支持的流类型的相关资源。这么做是不是觉得有点啰嗦呢?事实上,在这里使用枚举并没有什么特殊的意义,使用普通的一个Java类来定义StreamResources就已经足够了。
StreamControl类则保存了一个流类型的通知框所需要显示的控件。其定义如下:
[VolumePanel.java-->VolumePanel.StreamControl]
private class StreamControl {
intstreamType;
ViewGroupgroup;
ImageViewicon;
SeekBarseekbarView;
inticonRes;
inticonMuteRes;
}
很简单对不对?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的实现方式有很好的参考意义。
看一下onVolumeChanged()函数的实现:
[VolumePanel.java-->VolumePanel.onVolumeChanged()]
protected void onVolumeChanged(int streamType, intflags) {
// 需要flags中包含AudioManager.FLAG_SHOW_UI才会显示音量调通知框
if((flags & AudioManager.FLAG_SHOW_UI) != 0) {
synchronized (this) {
if (mActiveStreamType != streamType) {
reorderSliders(streamType); // 在Dialog里装载需要的StreamControl
}
// 这个函数负责最终的显示
onShowVolumeChanged(streamType, flags);
}
}
// 是否要播出Tone音,注意有个小延迟
if((flags & AudioManager.FLAG_PLAY_SOUND) != 0 && ! mRingIsSilent) {
removeMessages(MSG_PLAY_SOUND);
sendMessageDelayed(obtainMessage(MSG_PLAY_SOUND, streamType, flags),PLAY_SOUND_DELAY);
}
// 取消声音与振动的播放
if((flags & AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE) != 0) {
removeMessages(MSG_PLAY_SOUND);
removeMessages(MSG_VIBRATE);
onStopSounds();
}
// 开始安排回收资源
removeMessages(MSG_FREE_RESOURCES);
sendMessageDelayed(obtainMessage(MSG_FREE_RESOURCES), FREE_DELAY);
// 重置音量框超时关闭的时间。
resetTimeout();
}
注意最后一个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;
}
……
}
// 取出Music流类型对应的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()分析
setStreamVolume()是SDK中提供给应用的API,它的作用是为特定的流类型设置范围内允许的任意音量。我们看一下它的实现:
[AudioService.java-->AudioService.setStreamVolume()]
public void setStreamVolume(int streamType, intindex, int flags) {
// 这里先判断一下流类型这个参数的有效性
ensureValidStreamType(streamType);
// 获取保存了指定流类型音量信息的VolumeStreamState对象。
// 注意这里面使用mStreamVolumeAlias对这个数组进行了流类型的转换
VolumeStreamState streamState =mStreamStates[mStreamVolumeAlias[streamType]];
// 获取当前流将使用哪一个音频设备进行播放。它最终会调用到AudioPolicyService里去
final intdevice = getDeviceForStream(streamType);
// 获取流当前的音量
final intoldIndex = streamState.getIndex(device,
(streamState.muteCount()!= 0) /* lastAudible */);
// 将原流类型下的音量值映射到目标流类型下的音量值
// 因为不同流类型的音量值刻度不一样,所以需要进行这个转换
index =rescaleIndex(index * 10, streamType, mStreamVolumeAlias[streamType]);
//暂时先忽略下面这段if中的代码。它的作用根据flags的要求修改手机的情景模式
if(((flags & AudioManager.FLAG_ALLOW_RINGER_MODES) != 0) ||
(mStreamVolumeAlias[streamType] == getMasterStreamType())) {
……
}
// 调用setStreamVolumeInt()
setStreamVolumeInt(mStreamVolumeAlias[streamType], index, device, false,true);
// 获取设置的结果
index =mStreamStates[streamType].getIndex(device,
(mStreamStates[streamType].muteCount() != 0) /*lastAudible */);
// 广播通知
sendVolumeUpdate(streamType, oldIndex, index, flags);
}
看明白这个函数了吗?抛开被忽略掉的那个if块归纳一下:它的工作其实很简单的,就是执行下面这三方面的工作:
· 为调用setStreamVolumeInt准备参数。
· 调用setStreamVolumeInt。
· 广播音量发生变化的通知。
分析的主线将转向setStreamVolumeInt()的内容了。
2. setStreamVolumeInt()分析
看一下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‑3setStreamVolume的处理流程
注意看到这个序列图后,是否有读者感到眼熟呢?如果我们把setStreamVolumeInt()的内容替换掉在setStreamVolume()的对它的调用,再和adjustStreamVolume()函数进行以下比较,就会发现他们的内容出奇地相似。Android在其他地方也有这样的情况出现。从这一点上来说,已经发展到4.1版本的Android源码仍然尚不够精致。读者可以思考一下,有没有办法把这两个函数融合为一个函数呢?
到此,对于音量设置相关的内容就告一段落。接下来我们将讨论和音量相关的另一个重要的内容——静音。
3.2.3静音控制
静音控制的情况与音量调节又很大的不同。因为每个应用都有可能进行静音操作,所以为了防止状态发生紊乱,就需要为静音操作进行计数,也就是说多次静音后需要多次取消静音才可以。
不过,如果进行了静音计数后还会引入另外一个问题。如果一个应用在静音操作(计数加1)后因为某种原因不小心挂了,那么将不会有人再为它进行取消静音的操作,静音计数无法再回到0,也就是说这个倒霉的流将被永远静音下去。
那么怎么处理应用异常退出后的静音计数呢?AudioService的解决办法是记录下来每个应用的自己的静音计数,当应用崩溃时,在总的静音计数中减去崩溃应用自己的静音计数,也就是说,由我们为这个应用完成它没能完成的取消静音这个操作。为此,VolumeStreamState定义了一个继承自DeathRecepient的内部类名为VolumeDeathHandler,并为每个进行静音操作的进程创建一个实例。它保存了对应进程的静音计数,并在进程死亡时进行计数清零的操作。从这个名字来看可能是Google希望这个类将来能够承担更多与音量相关的事情吧,不过眼下它只负责静音。我们将在后续的内容对这个类进行深入的讲解。
经过前面的介绍,我们不难得出AudioService、VolumeStreamState与VolumeDeathHandler的关系如下:
图 3‑4 与静音相关的类
1. setStreamMute()分析
同音量设置一样,静音控制也是相对于某一个流类型而言的。而且正如本节开头所提到的,静音控制涉及到引用计数和客户端进程的死亡监控。所以相对与音量控制来说,静音控制有一定的复杂度。不过还好,静音控制对外入口只有一个函数,就是AudioManager.setStreamMute()。第二个参数state为true表示静音,否则为解除静音。
[AudioManager.java-->AudioManager.setStreamMute()]
public void setStreamMute(int streamType, booleanstate) {
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各个系统服务的实现中是一种很常见的用法。
另外,本例中传入的mICallBack是直接从Binder类实例化出来的,是一个很原始的IBinder对象。进一步讲,如果传递了一个通过AIDL定义的IBinder对象,这个对象就有了交互能力,服务端可以它向客户端进行回调。在后面探讨AudioFocus机制时会遇到这种情况。
2. VolumeDeathHandler分析
我们继续跟踪AudioService.setStreamMute()的实现,记得注意第三个参数cb,它是代表特定客户端的标识。
[AudioService.java-->AudioService.setStreamMute()]
public void setStreamMute(int streamType, booleanstate, IBinder cb) {
// 只有可以静音的流类型才能执行静音操作。这说明,并不是所有的流都可以被静音
if(isStreamAffectedByMute(streamType)) {
// 直接调用了流类型对应的mStreamStates的mute()函数
// 这里没有做那个令人讨厌的流类型的映射。这是出于操作语义上的原因。读者可以自行思考一下
mStreamStates[streamType].mute(cb, state);
}
}
接下来是VolumeStreamState的mute()函数。VolumeStreamState的确是音量相关操作的核心类型。
[AudioService.java-->VolumeStreamState.mute()]
public synchronized void mute(IBinder cb, booleanstate) {
// 这句话是一个重点,VolumeDeathHandler与cb一一对应
// 用来管理客户端的静音操作,并且监控客户端的生命状态
VolumeDeathHandler handler = getDeathHandler(cb, state);
if(handler == null) {
Log.e(TAG, "Could not get client deathhandler for stream: "+mStreamType);
return;
}
// 通过VolumeDeathHandler执行静音操作
handler.mute(state);
}
上述代码引入了静音控制的主角,VolumeDeathHandler,也许叫做MuteHandler更合适一些。它其实只有两个成员变量,分别是mICallBack和mMuteCount。其中mICallBack保存了客户端的传进来的标识,mMuteCount则保存了当前客户端执行静音操作的引用计数。另外,它继承自IBinder.DeathRecipient,所以它拥有监听客户端生命状态的能力。而成员函数则只有两个,分别是mute()和binderDied()。说到这里,再看看上面VolumeStreamState.mute()的实现,读者能否先想想VolumeDeathHandler的具体实现是什么样子的么?
/*********binder死亡监听相关知识**********/
若服务端进程因为某种原因“死亡”,那么Binder对象也将随之而去
若binder对象意外死亡(一个典型的例子就是宿主进程被系统回收),那么死亡代理DeathRecipient的binderDied()方法将被调用
status_t BpBinder::linkToDeath(
const sp<DeathRecipient>& recipient, void* cookie, uint32_t flags)
{
Obituary ob;
ob.recipient = recipient;
ob.cookie = cookie;//用来标识死亡接受者
ob.flags = flags;//用来标识死亡接受者
我们可以为一个Binder代理对象同时注册多个死亡通知接受者,它们保存在Binder代理对象内部的一个卟告列表mObituaries中。
unlinkToDeath移除先前注册的死亡通知,如果binder对象死亡,那么接受者将不再被调用。
1)方法返回true:如果接受者已经成功断开,要确保其binderDied()方法不会再被调用。
2)方法返回false:如果目标IBinder对象已经死亡,意味着binderDied()方法已经(或者不久之后)被调用。
即正在注册的Binder引用对象所引用的Binder本地对象已经死亡了。这时候驱动程序就需要马上向Client进程发送一个死亡接受通知;否则,以后就没有机会发送这个通知了
在这种死亡机制中,首先是Binder代理对象将一个死亡接收通知注册到Binder驱动程序中,然后当Binder驱动程序监控到它所引用的Binder本地对象死亡时,Binder驱动程序就会向它发送一个死亡通知。
如何判断一个Binder引用对象 所引用 的Binder本地对象已经死亡了呢?一个Binder引用对象所引用的Binder实体对象保存在它的成员变量node中,而一个Binder实体对象的宿主进程结构体为NULL,那么就说明它所引用的Binder本地对象已经死亡了,因为它所在的进程已经不存在了。
参考 https://blog.youkuaiyun.com/jltxgcy/article/details/27791513
Binder驱动程序是如何知道一个Server进程退出运行了呢?Binder驱动程序将设备文件/dev/binder的释放操作方法设置为函数binder_release。Server进程在启动时,会调用函数open来打开设备文件/dev/binder。一方面,在正常情况下,它退出时会调用函数close来关闭设备文件/dev/binder,这时候就会触发函数binder_releasse被调用;另一方面,如果Server进程异常退出,即它没有正常关闭设备文件/dev/binder,那么内核就会负责关闭它,这个时候也会触发函数binder_release被调用。因此,Binder驱动程序就可以在函数binder_release中检查进程退出时,是否有Binder本地对象在里面运行。如果有,就说明它们是死亡了的Binder本地对象了。
服务端进程死了,客户端的Binder对象就会被置为null,所以可以不必设置死亡监听,检测binder为null了也可知道服务进程死了
/*********binder死亡监听相关知识**********/
继续上面的脚步,看一下它的mute()函数。它的参数state的取值指定了进行静音还是取消静音。所以这个函数也就分成了两部分,分别处理静音与取消静音两个操作。其实,这完全可以放在两个函数中完成。先看看静音操作是怎么做的吧。
[AudioService.java-->Volume
DeathHandler.mute()part1]
public void mute(boolean state) {
if (state) {
// 静音操作
if(mMuteCount == 0) {
// 如果mMuteCount等于0,则表示客户端是第一次执行静音操作
//此时我们linkToDeath,开始对客户端的生命状况进行监听
//这样做的好处是可以避免非静音状态下对Binder资源的额外占用
try {
// linkToDeath! 为什么要判断是否为空?AudioManager不是写死了会把一个有效的
// Binder传递进来么?原来AudioManager也可能会调用mute()
// 此时的mICallback为空
if (mICallback != null) {
// 设置死亡通知,将调用对应的binderDied()方法
mICallback.linkToDeath(this, 0);
}
// 保存到mDeathHandlers列表中去
mDeathHandlers.add(this);
// muteCount() 我们在后面会介绍,这是全局的静音操作的引用计数
// 如果它的返回值为0,则表示这个流目前还没有被静音
if (muteCount() == 0) {
// 在这里设置流的音量为0
......//你打出来的省略号咋这么小呢?^_^
}
}catch (RemoteException e) {
......
}
}
// 引用计数加1
mMuteCount++;
} else {
// 暂时先不看取消静音的操作
……
}
}
看明白了么?这个函数的条件嵌套比较多,仔细归纳一下,就会发现这段代码的思路是非常清晰的。静音操作根据条件满足与否,有三个任务要做:
· 无论什么条件下,只要执行了这个函数,静音操作的引用计数都会加1。
· 如果这是客户端第一次执行静音,则开始监控其生命状态,并把自己加入到VolumeStreamState的mDeathHandlers列表中去。这是这段代码中很精练的一个操作,只有在客户端执行过静音操作后才会对其生命状态感兴趣,才有保存其VolumeDeathHandler的必要。
· 更进一步的,如果这是这个流类型第一次被静音,则设置流音量为0,这才是真正的静音动作。
不得不说,这段代码是非常精练的,不是说代码量少,而是它的行为非常干净。决不会做多余的操作,也不会保存多余的变量。
下面我们要看一下取消静音的操作。取消静音作为静音的逆操作,相信读者已经可以想象得到取消静音都做什么事情了吧?我们就不再对其进行说明了。
[AudioService.java-->VolumeDeathHandler.mute()part 2]
public void mute(boolean state) {
if (state) {
// 忽略掉静音操作
......
} else {// 取消静音的操作
if(mMuteCount == 0) {
Log.e(TAG, "unexpected unmute for stream: "+mStreamType);
}else {
// 引用计数减1先
mMuteCount--;
if (mMuteCount == 0) {
// 如果这是客户端最后一次有效地取消静音,去掉死亡监听
mDeathHandlers.remove(this);
if (mICallback != null) {
// 去掉死亡监听
mICallback.unlinkToDeath(this, 0);
}
if (muteCount() == 0) {
// 将流的音量值设置回静音前的音量,也就是lastAudibleIndex
……
}
}
}
}
}
然后就剩下最后的binderDied()函数了。当客户端发生异常,没能取消其执行过的静音操作时,需要替它完成它应该做却没做的事情。
[AudioService.java-->VolumeDeathHandler.binderDied()]
public void binderDied() {
if(mMuteCount != 0) {
mMuteCount = 1;
mute(false);
}
}
这个实现不难理解。读者可以将自行分析一下为什么这么做可以消除意外退出的客户端遗留下来的影响。
3.2.4 音量控制总结
音量控制是AudioService最重要的功能之一。经过上面的讨论,相信读者对AudioService的音量管理流程已经有了一定的理解。
总结一下我们在这一节里所学到的内容:
· AudioService音量管理的核心是VolumeStreamState。它保存了一个流类型所有的音量信息。
· VolumeStreamState保存了运行时的音量信息,而音量的生效则是在底层AudioFlinger完成。所以音量设置需要做两件事情,更新VolumeStreamState存储的音量值。设置音量到Audio底层系统。
· VolumeDeathHandler是VolumeStreamState的一个内部类。它的实例对应了在一个流类型上执行了静音操作的一个客户端,是实现静音功能的核心对象。
3.3音频外设的管理
这一节将探讨AudioService的另一个重要功能,那就是音频外设的管理。看过卷I第7
章的读者应该对音频外设这个概念并不陌生。在智能机全面普及的时代,对有线耳机、蓝牙
耳机等音频外设的支持已经是手机的标准,有些机型甚至支持HDMI、USB声卡等输出接口。
再加上手机本身自带的扬声器与听筒,这样一来,一台手机上同时能进行音频输出的设备往
往会有三四种甚至更多。如何协调这些设备的工作,使其符合用户的使用习惯、满足用户的
需求变得非常重要。
卷I的第7章详细介绍过AudioPolicy如何进行设备的路由切换,然而并没有讨论音频设
备为什么出现在AudioPolicy的设备候选列表中,这一节将以有线耳机为例讨论这个问题。
3.3.1 WiredAccessoryObserver 设备状态的监控
1.WiredAccessoryObserver简介
这要从WiredAccessoryObserver开始讲起,它是内核通知有线耳机插入事件所到达的第
一个环节。
WiredAccessoryObserbver继承自UEventObserver。UEventObserver是Android用来接收
UEvent的一个工具类。UEventObserver类维护着一个读取UEvent的线程,注意这个线程是
UEventObserver的一个静态成员,也就是说,一个进程只有一个。当调用UEventObserver的
startObserving()函数开始监听时,会告诉这个线程UEventObserver关心什么样的UEvent,当匹
配的事件到来时,监听线程会通过回调UEventObserver的onUEvent函数进行通知。读者可以
看一下UEventObserver的源代码以了解其具体实现,这并不复杂。
WiredAccessoryObserver接收内核上报的和耳机/HDMI/USB相关的UEvent事
件,并将其翻译成设备的状态变化。由于每种外设都有自己的UEvent与状态文件,因此
WiredAccessoryObserver定义了一个内部类名为UEventlnfo,并且为自己感兴趣的每一个音频
外设创建一个实例,其内部保存了对应外设的名字、UEvent地址及状态文件的地址。每当有
合适的UEvent到来时,WiredAccessoryObserver就会查找匹配的UEventlnfo实例,并且更新
可用设备的状态列表,同时通知AudioService。
关于可用外设的状态列表,虽然称为列表,事实上,它只是一个整型的变量,名为
mHeadsetState。在可用外设的状态列表中用一个二进制标志位表示某个外设的状态可用与否,
这与AudioPolicyManager的mAvailableOutputDevices的用法是一样的。下面是各种外设的标
志位的定义:
private static final int BIT_HEADSET=(1<<0);
private static final int BIT_HEADSET_NO_MIC-(1<<1);
private static final int BIT_USB_HEADSET_ANLG-(1<<2);
private static final int BIT_USB_HEADSET_DGTL=(1<<3);
private static final int BIT_HDMI_AUDIO=(1<<4);
private static final int SUPPORTED_HEADSETS = (BIT_HEADSETIBIT_HEADSET_NO_MIC I
BIT_USB_HEADSET_ANLGIBIT_USB_HEADSET_DGTL I
BIT_HDMI_AUDIO);
private static final int HEADSETS_WITH_MIC-BIT_HEADSET;
举个例子,如果mHeadsetState等于0x00000002,也就是BIT_HEADSET_NO_MIC,表
示目前手机上插入一个不带麦克风的耳机。而如果mHeadsetState等于0x00000011,也就是
HEADSETS_WITH_MICIBIT_HDML_AUDIO,则表示目前手机上同时插入一个带有麦克风的
耳机及HDMI输出线。
WiredAccessoryObserver工作原理就这么简单,我们接下来将以有线耳机为例子对其进行
详细讨论。
2.启动与初始化
虽然WiredAccessoryObserver不是一个服务,但是它拥有系统服务的待遇——在system_ server中同系统服务一起被加载,
如下所示:
[SystemServer java->ServerThread.run0]
try {
new WiredAccessoryobserver(context);
}catch (Throwablee){
reportWtf("starting WiredAccessoryobserver",e);
}
只有一个构造函数,其实,构造函数中并没有做太多的初始化工作,而是注册了一
个BroadcastReceiver,监听ACTION_BOOT_COMPLETE。其真正的初始化工作是在这个
BootCompletedReceiver中完成的。
[WiredAccessoryObserver.java->BootCompletedReceiver.onReceive]
public void onReceive (Context context,Intent intent){
//初始化
init();
//开始对所有感兴趣的UEvent进行监听
for (inti=0;i< uEventInfo.size();++i){
UEventInfo uei = uEventInfo.get(i);
startobserving("DEVPATH="+uei.getpevpath());
}
这里的inito函数的作用是为了在开机后对外设的状态进行初始化。
[WiredAccessoryObserver.java->WiredAccessoryObserver.init()]
private synchronized final void init() {
char[] buffer = new char[1024];
mPrevHeadsetstate = mHeadsetState;
for(inti=0;i < UEventInfo.size(); ++i){
UEventInfo uei = uEventInfo.get(i);
try{
int curstate;
/*打开状态文件并从中读取状态信息。状态文件中保存着一个整数,非0则表示设备已插入。
通过UEventInfo的定义可以知道,有线耳机的状态文件路径为
/sys/class/switch/h2w/state*/
FileReader file=new FileReader(uei.getSwitchstatepath());
int len-file.read(buffer,0,1024);
file.close();
curState=Integer.valueof((new String(buffer,0,1en)).trim());
//如果设备已插入,则更新设备的状态,否则不作处理
if(curState>0){
updatestate(uei.getDevPath(),uei.getDevName(),curstate);
}
}catch (Exception e){ }
到这里WiredAccessoryObserver已经完成初始化了,已经对第一条UEvent的到来准备就绪。
3.耳机插入或拔出时的处理
如果有外设被插入或拔出,WiredAccessoryObserver的onUEvent()函数会被回调。参数
event中保存了其详细的信息。
[WiredAccessoryObserver.java-->WiredAccessoryObserver.onUEvent()]
public void onUEvent(UEventobserver.UEvent event){
try{
//UEvent事件的路径
string devPath=event.get("DEVPATH");
//这个name其实就是UEventInfo中的mDevName,通过这个变量确定发生状态变化的设备名字
String name =event.get("SWITCH_NAME");
//这个state与保存在状态文件中的数值的意义是一致的
//事实上,当这条UEvent上报时,状态文件中的值也被更新成这个值
int state=Integer.parseInt(event.get("SWITCH_STATE"));
//像初始化的init()函数一样,调用updateState()进行状态更新
updatestate(devpath,name,state);
}catch (NumberFormatException e){ }
[WiredAccessoryObserverjava-->WiredAccessoryObserver.updateState()]
private synchronized final void updatestate(String devPath,String name,int state) {
for (int i=0;i < uEventInfo.size();++i){
iUEventInfo uei=uEventInfo.get(i);
if(devpath.equals(uei.getDevpath())){
//找到状态发生变化的外设所对应的UEventInfo并更新状态
//uei.computeNewHeadsetState)这个函数,它的目的是通过UEvent上报的状态值计算
//出新的可用外设列表
update(name,uei.computeNewHeadsetState(mlHeadsetState,state));
return;
}
}
}
看到这里,读者是否觉得updateState的实现有些笨拙了呢?如果以devName为键,将
uEventinfo保存在Hashtable中,无论对代码的整洁还是执行的效率都是有帮助的。
注意 uei.computeNewHeadsetState)这个函数,它的目的是通过UEvent上报的状态值计算
出新的可用外设列表。
computeNewHeadsetState()这个函数的扩展性并不是太好,只是目前够用而已,读者
可以自行研究。
继续前面的脚步,现在到了update()函数。这个函数的目的是对前面传入的newState进
行全面检查,防止出现不正确的状态。这个函数的运算稍多些,为了方便分析,仅留下和有
线耳机(h2w)相关的代码。
[WiredAccessoryObserver,java-->WiredAccessoryObserver.update()]
private synchronized final void update(String newName,int newstate){
/*从headsetState中去掉不支持的外设,所以,如果不希望手机支持某种外设,比
如说USB_HEADSET,不需要从kerne1改起,只要将其从SUPPORTED_HEADSETS中去
掉即可*/
int headsetState =newstate & SUPPORTED_HEADSETS;
int h2w_headset = headsetState & (BIT_HEADSET I BIT_HEADSET NO_MIC);
boolean h2wstateChange = true;
//下面这行代码比较有意思,首先我们的目的是判断有线耳机的状态是否发生了变化
//mHeadsetState ==headsetState这种条件很好理解,可是后面那个条件呢
if(mleadsetState==headsetState ll ((h2w_headset & (h2w_headset -1))!=0)){
h2wStateChange = false;
}
//如果是不正确的状态转换则直接忽略
if(lh2wstateChange){
return;
}
//更新可用外设列表
mHeadsetName = newName;
mPrevHeadsetState = mlHeadsetstate;
mHeadsetState = headsetstate;
//为什么要申请一个电源锁呢
mWakeLock.acquire();
//状态已经更新完毕,发送消息给mHandler,通知Audiosevicervice
/ /注意mHandler的定义,可以看出它运行在创建WiredAccessoryobserver的ServerThread中
mHandler.sendMessage(mHandler.obtainMessage(0,
mHeadsetState,
mPrevHeadsetState,
mHeadsetName));
这个函数的意图比较很明显,只是其中一个判断条件让人一时摸不着头脑,
(h2w_headset&(h2w_headset-1))!=0。按照注释中的说法,此函数不接受同时有两种耳机出
现的情况,也就是h2wheadst=BIT_HEADSET | BIT_HEADSET_NO_MIC,直接做这
个判断不就可以了吗?。直接判断仅限
于只有两种可能的外设时才能起作用,超过两个就很难处理了。而谷歌的这个做法既
快捷,又可以应对任意多种可能的外设。读者可以思考一下为什么。
另外,这段代码在执行mHandler.sendMesage)的调用之前先中请了一个电源锁。这
是一个很细节但很重要的做法。当发送消息给一个Handler时,必须考虑设备有可能
在Handler得以处理消息之前进入深睡眠状态的极端情况(对延时消息来说,可能就
是常见情况了)。在这种情况下,CPU将会进入休眠状态,从而使得消息无法得到及
时处理,影响程序执行的正确性。
可用外设列表更新完毕后发送了一条消息给mHandler。当消息生效时,直接调用
setDevicesState()函数,它会遍历所有SUPPORTED_HEADSET,然后对每个外设调用
setDeviceState()。注意,这两个函数是devices与device的区别。setDeviceState()的目的就是
要把指定外设的状态汇报给AudioService,我们看一下它的实现:
[WiredAccessoryObserver.java->WiredAccessoryObserver.setDeviceState0]
private final void setDevicestate(int headset,
int headsetstate,
int prevHeadsetState,
String headsetName){
if((headsetstate& headset)!=(prevlleadsetstate & headset)){
//只有当这个外设的接入状态发生变化时才会继续
int device;
int state;
//1表示可用,0表示不可用
if((headsetState & headset)!=0){
state=1;
}else{
state=0;
}//翻译可用外设列表中的外设为Audio系统的设备号
if (headset==BIT_HEADSET){
device=AudioManager.DEVICE OUT WIRED_HEADSET;
}else if (headset == BIT HEADSET NO MIC){
device=AudioManager.DEVICE OUT_WIRED_HEADPHONE;
}else if (headset==BIT USB_ HEADSET_ANLG){
device=AudioManager.DEVICE OUT_ANLG_DOCK_ HEADSET;
}else if (headset==BIT_USB_HEADSET_DGTL){
device=AudioManager.DEVICE OUT_DGTL DOCK_ HEADSET;
}else if (headset==BIT_HDMI_AUDIO)(
device=AudioManager.DEVICE OUT AUX DIGITAL;
}else{
slog.e(TAG,"setDevicestate()invalid headset type:"theadset);
return;
}
//通知AudioService
mAudioManager.setWiredDeviceConnectionstate(device,state,headsetName);
之后,程序的流程将会离开WiredHeadsetObserver,再次前往AudioService。
4.总结一下WiredAccessoryObserver
对WiredHeadsetObserver的分析就先告一段落,这里再简单回顾一下关于它的知识。
口 它是站在最前方的一个哨兵,时刻监听着和音频外设拔插相关的UEvent事件。
口 它接收到UEvent事件后,会翻译事件的内容为外设可用状态的变化。
口 它是为AudioService服务的,一旦有变化就立刻通知AudioService。
口 它虽然不是一个服务,但是它却运行在system server中。
口 它不是唯一的音频外设状态监听者,它只负责监控有线连接的音频外设。其他的,
如蓝牙耳机,在其他相关模块中维护。但是它们的本质是类似的,最终都要通知给
AudioServic。有兴趣的读者可以自行研究。
3.3.2 AudioService的外设状态管理
最终还是要回到AudioService中来,它才是音频相关操作的主基地。
1.处理来自WiredAccessoryObserver的通知
AudioService会如何处理外设的可用状态变化呢?仔细想想,在开发播放器的时候一定
接触过ACTION_AUDIO_BECOMING_NOISY 和ACTION_HEADSET_PLUG这两个广播吧。
另外,更重要的是,这些变化需要让底层的AudioPolicy知道。所以,笔者认为AudioService
外设状态管理分为三个内容:
口 管理发送ACTION_AUDIO_BECOMING_NOISY广播。
口 发送设备状态变化的广播,通知应用。
口 将其变化通知底层。
从WiredHeadsetObserver 调用的 setWiredDeviceConnectionState()函数开始:
[AudioService.java-->AudioService.setWiredDeviceConnectionState()]
public void setwiredpeviceConnectionstate(int device,int state,String name){
synchronized(mConnectedDevices){
//发送ACTIONAUDIO_BECOMING_NOISY广播的地方
int delay=checkSendBecomingNoisyIntent(device,state);
//又是发送消息给mAudiolHandler,注意这个消息有可能是延时的
//这取决于checkSendBecomingNoisyIntent的返回值:delay
queueMsgUnderWakeLock(mAudioHandler,
MSG _SET_WIRED_DEVICE_CONNECTION_STATE,
device,
state,
name,
delay);
}
}
此函数负责两项工作:调用checkSendBecomingNoisyIntent()函数及发送SET_WIRED
DEVICE_CONNECTION_STATE 消息给mAudioHandler。
checkSendBecomingNoisylntent()函数的目的是判断当前状态的变化是否有必要发送
BECOMINGNOISY广播。这个广播用于警告所有媒体播放应用声音即将从手机外放中进
行播放。在绝大部分情况下,收到这个广播的应用都应当立即暂停播放,以避免用户无意识
地泄露自己的隐私或打扰到周围的其他人。另外,这个函数的返回值决定了SET WIRED
DEVICE_CONNECTION_STATE消息是否需要延时处理。其代码如下:
[AudioService.java->AudioService.checkSendBecomingNoisyIntent()]
private int checkSendBecomingNoisyIntent(int device,int state){
int delay=0;
//发送BECOMINGNOISY广播的前两个条件如下:
//1.外设被拔除
//2.外设是mBecomingNoisyIntentDevices指定的外设之一
//既然这些设备从手机拔除后会AUDTOBECOMINGNOISY,不妨称它们为安静外设
if((state ==0)&&((device s mBecomingNoisyIntentDevices)!=0)){
int devices=0;
//收集所有连接在手机上的安静外设
for (int dev:mConnectedDevices.keyset()){
if((dev& mBecomingNoisyIntentDevices)!=0){
devices l=dev;
}
}
//发送BECOMINGNOISY广播的第三个条件:移除的设备必须是连接在手机上的最后一个安静外设
//同时也是推迟后续处理的第一个条件:发送了BECOMING_NOISY广播
if(devices== device){
delay=1000;//确定后续对这个状态变化的处理向后推迟1秒
sendBecomingNoisyIntent();//发送BECOMING_NOISY广播
}
}
//推迟后续处理的另外一个条件:如果有和外设连接状态相关的延迟消息尚未被处理,那么
//也必须推后消息的处理
if(mAudioHandler.hasMessages(MSG_SET_A2DP_CONNECTION_STATE) ||
mAudioHandler.hasMessages(MSG_SET_WIRED_DEVICE_CONNECTION_STATE)){
delay=1000;
}
return delay;
}
代码不长,有价值的内容不少。BECOMING_NOISY广播发出的条件是最后一个安静外
设被拔出,这个很好理解。而推迟MSG_ SET_WIRED_DEVICE_CONNECTION_STATE 消息
的生效时间这种做法可能一时难以弄明白。不过暂时先不管它,等我们了解了外设连接状态
变化的流程后再解释它的意义。
回到setWiredDeviceConnectionState (),调用checkSendBecomingNoisylntent()函数后,它
发送MSG_SET_WIRED_DEVICE_ CONNECTION_STATE给mAudioHandler,此消息生效后,
mAudioHandler 调用onSetWiredDeviceConnectionState函数。
[AudioService,java-->AudioHandler.onSetWiredDeviceConnectionState()]
private void onSetWiredDeviceConnectionstate(int device,int state,String name) {
synchronized(mConnectedpevices){
//如果拔下普通耳机,则会强制要求使用蓝牙耳机作为输出设备
if((state==0) && ((device ==AudioSystem.DEVICE OUT_WIRED_HEADSET) ||
(device==AudioSystem.DEVICE_OUT_WIRED_HEADPHONE))){
setBluetoothA2dpOnInt(true);
}
//这个函数对Audiopolicy进行了通知
handleDeviceConnection((state==1),device,"");
//如果插入普通耳机,则会取消强制使用蓝牙耳机的设置
if((state!=0)&&((device==AudioSystem.DEVICE_OUT_WIRED_HEADSET)l1
(device==AudioSystem.DEVICE_OUT_WIRED_HEADPHONE))){
setBluetoothA2dpOnInt(false);
}
//从名字就可以看出,这是用来广播外设状态变化通知的,将通知对此感兴趣的应用程序
sendDeviceConnectionIntent(device,state,name);
}
}
在这个函数中,我们需要重点关注的是对handleDeviceConnection()和sendDevice-
ConnectionIntent两个函数的调用。它们分别用来通知AudioPolicy与上层应用。
另外,还可以看到,在handleDeviceConnection()函数上下有一对关于蓝牙耳机的操作。
从其实现上可以看出,如果拔出普通耳机,系统将会强制使用蓝牙耳机进行输出。如果插入
耳机则会取消这个设置。这种操作完全可以放在AudioPolicyManager中实现。
看一下通知AudioPolicy的handleDeviceConnection)函数的实现吧!
[AudioService.java-->AudioService.handleDeviceConnection()]
private boolean handleDeviceConnection (boolean connected,int device,String params){
synchronized(mConnectedDevices){
boolean isConnected=(mConnectedpevices.containsKey(device) &&
(params.isEmpty() || mConnectedpevices.get(device).equals(params)));
if(isConnected &&!connected){
//外设被拔出,通过AudioSystem将状态设置到底层的AudioPolicyService
//将更新底层的AudioPolicy中缓存的
可用设备列表,同时,如果正在进行音频播放,那么这个函数还将触发音频设备的重新
选择。
Audiosystem.setDeviceConnectionstate(device,
AudioSystem.DEVICE_STATE_UNAVAILABLE,
mConnectedDevices.get(device));
mConnectedDevices.remove(device);
return true;
}else if (!isConnected && connected){
//外设被插入
AudioSystem.setDeviceConnectionstate(device,
AudioSystem.DEVICE_STATE_AVAILABLE,
params);
mConnectedDevices.put(new Integer(device),params);
return true;
}
return false;
}
很简单吧?如果读者对卷I第7章的内容比较熟悉,那么一定知道AudioSystem.
setDeviceConnectionState)这个函数意味着什么。它将更新底层的AudioPolicy中缓存的
可用设备列表,同时,如果正在进行音频播放,那么这个函数还将触发音频设备的重新
选择。
这一节提到“可用设备列表”的次数很多,很多地方都使用了这个概念。归纳一下,
在本节所讨论的内容里,有三个地方有可用设备列表:
1)WiredAccessoryObserver:目的是确认外设的状态变化是否合法,是否需要报告给
AudioService。
2)AudioService:它以一个Hashtable的形式保存了一个可用设备列表,它为
AudioService向应用及底层AudioPolicyManager发送通知提供依据。
3)AudioPolicyManager:它保存的可用设备列表在AudioPolicyManager需要重新选择
音频输出设备时提供候选。
2.关于推迟处理外设状态
前面讨论checkSendBecomingNoisyIntent()函数的实现时提到了根据某些条件,有可能使
MSG_ SET_WIRED_DEVICE_CONNECTION_STAT延迟生效1秒。在这种情况下应用会在1
秒之后才能收到设备状态变化的广播,同时,AudioPolicy也要在1秒之后才能更新可用设备
列表并进行必要的设备切换。为什么要这么做呢?想想推迟的条件:
口 最后一个安静外设被移除,发送了BECOMING_NOISY广播。
口 队列中尚有两个消息在等候处理:MSG SET WIREDDEVICE CONNECTION_STATE 和 MSG_SET_A2DP_CONNECTION_STATE。
只要这两个条件有一个满足,就会发生1秒推迟。下面分别讨论。
关于第一个条件,当最后一个安静外设被移除后,手机上可用的音频输出设备就只剩
下扬声器了(听筒不能算是常规的音频输出设备,它只有在通话过程中才会用到)。那么在
MSG_SET_WIRED_DEVICE_CONNECTION_STAT生效后,AudioPolicyManager将会切换输
出到扬声器,此时正在播放的音频就会被外放出来。
很多时候,这并不是用户所期望的,用户可能不希望他人知道自己在听什么,或者不希
望在某些场合下扬声器发出的声音打扰到其他人。何况耳机被拔除有可能还是个意外。所以,
正在进行音频播放的应用可能希望收到耳机等安静设备被拔出时的通知,并且在收到后暂停
播放。
读者可能会有疑问,在sendDeviceConnectionlntent()中不是发送了状态通知的广播了
吗?其实,这个状态通知广播用在其他情况下可以,但是用在上述情况中是有问题的。按照
上面的讨论,执行sendDeviceConnectionIntent)之前,先执行了handleDeviceConnection(),它
会更新底层的可用设备列表,并且触发设备切换。于是应用有可能在收到状态通知之前,输
出设备已经被切换成扬声器了,直到应用收到通知后暂停回放,这段时间内就会发生扬声器
的漏音。
所以,Android引入了一个新的广播来应对这个问题,那就是BECOMING_NOISY广
播。这个广播只有在最后一个安静外设被移除后才会发出,于是应用可以精确地知道音频即
将从扬声器进行播放,而且后续的设备切换等动作被推迟了1秒,应用就有充足的时间收到
BECOMING_NOISY广播并暂停播放。在正常情况下,这种做法可以杜绝漏音的情况出现。
这是第一个延时条件的意义。
至于第二个条件,队列中尚有以下两个消息等候处理:MSG_SET_WIRED_DEVICE_CONNECTION_STATE
和MSG_SET_A2DP_CONNECTION_STATE,这其实是不得已的一种
做法。一个是mAudioHandler所在的 线程发生了阻塞,另一个就是这两个消息被延迟发送了。
根据Handler现有的接口没有办法得知是哪一种情况,
但是在正常情况下都是第二种,也是比较麻烦的一种情况。因为在这种情况
下,如果正常发送MSG_SET_WIRED_DEVICE_CONNECTION_STATE消息,那么它的生效时
间将会早于正在队列中排队的那两个消息。如此一来,就会发生外设可用状态紊乱的问题。所
以,AudioService迫不得已在这种情况下推迟发送1秒。读者可以做个试验,快速地在手机上
拔插耳机,将会看到通知栏内的耳机图标的变化总是会延迟1秒。
我们在之前的分析中没有见过MSG_SET_A2DP_CONNECTION_STATE,它和讨论的
MSG_ SET_WIRED_DEVICE_CONNECTION_STATE意义是一样的,而且有着几乎相
同的处理逻辑,不过它是与蓝牙耳机相关的。
3.3.3音频外设管理小结
这一节以有线音频外设为例,探讨了从WiredAccessoryObserver收到UEvent开始到
AudioService通知底层应用为止的AudioService对音频外设的管理机制。
总结一下音频外设拔插的处理过程:
口 由负责相关外设的模块监听从硬件上报的状态通知。将状态变化提交给AudioService
进行处理。
口 AudioService得到相关模块发来的通知,根据需要发送BECOMINGNOISY消息给应
用,并更新自己的可用设备列表。
口 AudioService将外设可用状态的变化通知AudioPolicy。AudioPolicy更新自己的可用
设备列表,并重新选取音频输出设备。
口 AudioService将外设可用状态以广播的形式发送给应用等其他对此感兴趣的应用程序
或系统模块。
蓝牙模块负责蓝牙耳机的连接/断开状态的监控并通知AudioService。AudioService收到
此通知之后的代码路径虽然与本节所讨论的内容不完全一样,但其处理原则与有线耳机是一
致的,读者可以自行分析学习。
3.4AudioFocus 机制的实现
AudioFocus是自Android2.3建立起来的一个新的机制。这套新机制的目的在于统一协调
多个回放实例之间的交互。
我们知道,手机的多媒体功能越来越强大,听音乐、看视频、听收音机已经成为这台小
小的设备的重要功能。加上手机本身的闹铃、信息通知以及电话铃声等,一台手机中有很多
情况需要播放音频。我们称每一次音频播放为一次回放实例。这就需要我们能够对这些回放
实例的并发情况做好协调,否则就会出现多个音频不合理地同时播放的恼人结果。
在2.3以前,Android并没有一套统一的管理机制。每个音频回放实例只能通过发送广播
的方式告知其他人自己的播放状态。这不仅造成了广播满天飞的情况,而且可扩展性与一致
性非常差,基本上只能在同一厂商的应用之间使用。好在,Android 2.3对AudioFocus的引入
大大地改善了这个状况。
AudioFocus的含义可以和Windows的窗口焦点机制做类比,只不过我们的焦点对象是音
频的回放实例。在同一时间,只能有一个音频回放实例拥有焦点。每个回放实例开始播放前,
必须向AudioService申请获取AudioFocus,只有申请成功才允许开始回放。在回放实例播放
结束后,要求释放AudioFocus。在回放实例播放的过程中,AudioFocus有可能被其他回放实
例抢走,这时,被抢走AudioFocus的回放实例需要根据情况采取暂停、静音或降低音量的操
作,以突出拥有AudioFocus的回放实例的播放。当AudioFocus被还回来时,回放实例可以恢
复被抢走之前的状态,继续播放。
总体上来说,AudioFocus是一个没有优先级概念的抢占式的机制。在一般情况下后一个
申请者都能从前一个申请者的手中获取AudioFocus。不过只有一个例外,就是通话。通话作为
手机的首要功能,同时也是一种音频的播放过程,所以从来电铃声开始到通话结束这个过程,
Telephony相关的模块也会申请AudioFocus,但是它的优先级是最高的。Telephony可以从所有
人手中抢走AudioFocus,但是任何人无法从它手中将其夺回。这在后面的代码分析中可以看到。
值得一提的是,AudioFocus机制完全是一个建议性而不是强制性的机制。也就是说,上
述的行为是建议回放实例遵守,而不是强制的。所以,市面上仍有一些带有音频播放功能的
应用没有采用这套机制。
3.4.1AudioFocus最简单的例子
AudioFocus相关的API一共有三个,分别是requestAudioFocus()、abandonAudioFocus()
以及AudioFocusListener回调接口,它们分别负责AudioFocus的申请、释放以及变化通知。
为了让大家了解AudioFocus的实现原理,我们先来看一个AudioFocus使用方法。下面
是一个播放器的部分代码:
public void play(){
//在开始播放前,先申请AudioFocus,注意传入的参数
int result=mAudioManager.requestAudioFocus(
mAudioFocusListener,AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);
//只有成功申请到AudioFocus之后才能开始播放
if(result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
mMediaPlayer.start();
else//申请失败,如果系统没有问题,这一定是正在通话过程中,所以,还是不要播放了
showMessageCannotstartPlaybackDuringACal1();
}
public void stop(){
//停止播放时,需要释放AudioFocus
mAudioManager.abandonAudioFocus(mAudioFocusListener);
}
在开始播放前,应用首先要申请AudioFocus。申请AudioFocus时传入了3个参数。
口 mAudioFocusListener:AudioFocus变化通知回调。当AudioFocus被其他
AudioFocus 使用者抢走或归还时将通过这个回调对象进行通知。
口 AudioManager.STREAM_MUSIC:参数名为streamType。这个参数表明了申请者即将
使用哪种流类型进行播放。目前这个参数仅提供一项信息而已,对AudioFocus的机
制没有任何影响,不过Android在后续的升级中可能会使用此参数。
口 AudioManager.AUDIOFOCUS_GAIN:参数名为durationHint。这个参数指明了申请者
将拥有这个AudioFocus多长时间。例子中传入的AUDIOFOCUSGAIN表明了申请
者将长期占用这个AudioFocus。另外还有两个可能的取值,它们是AUDIOFOCUS_
GAIN_TRANSIENT 和AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK。这两个
取值的含义都表明申请者将暂时占用AudioFocus,不同的是,后者还指示了即将被
申请者抢走AudioFocus的使用者不需要暂停,只要降低一下音量就可以了(这就是
“DUCK”的意思)。
在停止播放时,需要调用abandonAudioFocus释放AudioFocus,会将其归还给之前被抢
走AudioFocus的那个使用者。
接下来,我们看一下mAudioFocusListener是如何实现的。
private onAudioFocusChangeListener mAudioFocusListener=
new onAudioFocusChangeListener(){
//当AudioFocus发生变化时,这个函数将会被调用。其中参数focuschange指示发生了什么变化
public void onAudioFocusChange(int focusChange){
switch(focusChange){
//AudioFocus被长期夺走,需要中止播放,并释放AudioFocus
//这种情况对应于抢走AudioFocus的申请者使用了AUDIOFOCUS_GAIN
case AUDIOFOCUS_LOSS:
stop();
break;
//AudioFocus被临时夺走,不久就会被归还,只需要暂停,AudioFocus被归还后再恢 复播放
//这对应于抢走AudioFocus的申请者使用了AUDIOFOCUS_GAIN_TRANSTENT
case AUDIOFOCUS_LOSS_TRANSIENT:
saveCurrentPlayingstate();
pause();
break;
//AudioFocus被临时夺走,允许不暂停,所以降低音量
//这对应于抢走AudioFocus的回放实例使用了AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
case AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
saveCurrentPlayingState();
setVolume(getVolume()/2);
break;
//AudioFocus被归还,这时需要恢复被夺走前的播放状态
case AUDIOFOCUS_GAIN:
restorePlayingstate();
break;
};
从这里能够看出,AudioFocus机制的逻辑是完整而清晰的。理解上述例子以后,相信
AudioFocus的工作原理已经浮现在脑海里了吧?如果有兴趣,可以不用着急阅读下面的分析,
先思考一下AudioFocus可能的工作原理,然后再与Android的实现进行比较。
从调用requestAudioFocus()进行申请到abandonAudioFocus)释放的这段时间内,只能
说这个使用者参与了AudioFocus机制,不能保证一直拥有AudioFocus。
3.4.2 AudioFocus实现原理简介
看了前面的示例代码,可以推断出,AudioFocus的实现基础应该是一个栈。栈顶的使用
者拥有AudioFocus。
在申请AudioFocus成功时,申请者被放置在栈顶,同时,通知之前在栈顶的使用者,告
诉它新的申请者抢走了AudioFocus。
当释放AudioFocus时,使用者将从栈中被移除。如果这个使用者位于栈顶,则表明释放
前它拥有AudioFocus,因此AudioFocus将被返还给新的栈顶。
工作原理参考图3-5。
图 3-5 AudioFocus的工作原理
AudioFocus的工作原理就是如此。下面将分别对申请和释放这两个过程进行详细分析。
3.4.3申请 AudioFocus
先看一下AudioFocus的申请过程。
[AudioManager,java-->AudioManager.requestAudioFocus()]
public int requestAudioFocus(OnAudioFocusChangeListener 1, int streamrype,int durationHint){
int status = AUDIOFOCUS_REQUEST_FAILED;
//注册AudioFocusListener
registerAudioFocusListener(1);
IAudioService service = getService();
try {
//调用AudioService的requestAudioFocus函数
status=service.requestAudioFocus(streamrype,durationHint,mICallBack,
mAudioFocusDispatcher,getIdForAudioFocusListener(1),
mContext.getPackageName());
}catch(RemoteException e){ }
return status;
这段代码有两个关键的地方,分别是调用registerAudioFocusListener()和调用AudioService
的requestAudioFocusO函数。这个函数的参数很多,随着分析的深入,这些参数的意
思会逐渐明朗。
下面先看一下registerAudioFocusListener做了什么。
[AudioManager,java-->AudioManager.registerAudioFocusListener()]
public void registerAudioFocusListener(OnAudioFocusChangeListener 1){
synchronized(mFocusListenerLock){
if(mAudioFocusIdListenerMap.containsKey(getIdForAudioFocusListener(1))){
return;
}
mAudioFocusIdListenerMap.put(getIdForAudioFocusListener(1),1);
}
}
原来,应用程序提供的AudioFocusChangeListener是被注册进AudioManager的一个
Hashtable中而不是AudioService中。注意,这个Hashtable的KEY是getldForAudioFocusListener()
分配的一个字符串型的Id。这样看来,AudioManager这一侧一定有一个代理负责接受
AudioService的回调并从这个Hashtable中通过Id将回调转发给相应的Listener。
究竟是谁在做这个代理呢?就是稍候将作为参数传递给AudioService的mAudioFocusDispatcher
。
[AudioManager.java-->AudioManager.mAudioFocusDispather]
private final IAudioFocusDispatcher mAudioFocusDispatcher
= new IAudioFocusDispatcher.Stub(){
public void dispatchAudioFocusChange(int focuschange,String id){
Message m = mAudioFocusEventHandlerDelegate.getHandler()
.obtainMessage(focuschange,id);
mAudioFocusEventHandlerDelegate.getHandler().sendMessage(m);
}
}
可以看出,mAudioFocusDispatcher的实现非常轻量级,直接把focusChange和listener的id
发送给了一个Handler去处理。
这个看似繁元的做法其实是很有必要的。要知道,目前这个回调尚在Binder的调用
线程中,如果在这里因为用户传入的Listener的代码有问题而报出异常或阻塞甚至恶
意拖延,则会导致Binder的另一端因异常而崩溃或阻塞。到这里为止,AudioService
已经尽到了通知义务,应该通过Handler将后续的操作发往另一个线程,使
AudioService尽可能远离回调实现的影响。
看一下这个Handler的消息处理函数,msg.what存储了focusChange参数,msg.obj1存储
了Id。因此handleMessage函数一定像下面这个样子:
[AudioManager,java-->FocusEventHandlerDelegate.Handler.handleMessage()]
public void handleMessage(Message msg){
onAudioFocusChangeListener listener=null;
synchronized(mFocusListenerLock){
listener = findFocusListener((String)msg.obj);
}
if(listener!=null){
//通知使用者AudioFocus的归属发生了变化
listener.onAudioFocusChange(msg.what);
}
}
现在我们了解了AudioService的回调是如何传递给回放实例的。概括来说,
mAudioFocusDispather作为AudioService与AudioManager的沟通桥梁,将回调操作以消息的
方式发送给mFocusEventHandlerDelegate的Handler,在Handler的消息处理函数中通知回放
实例。
读者也许会有疑问,为什么不让AudioFocusChangeListener直接继承自AIDL描述
的接口,非要由mAudioFocusDispatcher去做转发,这不是很麻烦呢?这是为了节约
Binder的资源。虽说mAudioFocusDispatcher不是一个服务,但是其对Binder资源的
占用却与服务一样,所以大量使用Binder回调是有待商植的。这种把多个回调的信息
保存在Bp端,使用一个拥有Binder通信能力的回调对象做它们的代理是一种很值得
推荐的做法。
回到AudioManager.requestAudioFocus()的实现中。我们调用AudioService.requestAudioFocus()
时传入的参数很多。它们都是什么意义呢?为了方便后面探讨,我们先把
AudioService.requestAudioFocus的参数意义搞清楚。
口 mainStreamType:这个参数目前没有被使用,这是为了便于以后功能扩展所预留的一
个参数。
口 focusChangeHint:这个参数指明了申请者持有AudioFocus的方式。
口 cb:IBinder类型的一个参数,在探讨静音控制时曾经见过这个参数,就是
AudioManager的mICallBack。应该能够联想到,AudioService又要做linkToDeath了。
口 fd:IAudioFocusDispatcher对象,我们刚刚分析过,它是AudioService回调回放实例
的中介。
口 clientld:它是通过getIdForAudioFocusListener()
函数获取的一个字符串,用于唯一标识一个AudioFocusChangeListener。
口 callingPackageName:回放实例所在的包名。
接下来看看AudioService.requestAudioFocus()的工作原理。
[AudioService->AudioService.requestAudioFocus()]
public int requestAudioFocus(int mainstreamrype,int focuschangelint,IBinder cb,
IAudioFocusDispatcher fd,String clientId,String callingPackageName){
//首先检查客户端提供的mICallBack是不是一个有效的Binder
//否则对其作linkToDeath没有任何意义
if(!cb.pingBinder()){
return AudioManager.AUDIOFOCUS _REQUEST_FAILED;
}
synchronized(mAudioFocusLock){
//检查一下当前情况下AudioService是否能够让申请者获取AudioFocus
//前面说过,如果通话占用了AudioFocus,任何人都不能够再申请AudioFocus
if(!canReassignAudioFocus()){
return AudioManager.AUDIOFOCUS REOUEST FATLED;
}
/*看到AudioFocusDeathHandler后,可以对比一下VolumeDeathiandler的工作,这里之所
以需要监控其生命状态,就是为了防止一个使用者还没有来得及调用abandonAudioFocus就崩
溃。所以读者一定知道VolumeDeathiHandler的binderDied()函数的内容是什么*/
AudioFocusDeathHandler afdh=new AudioFocusDeathHandler(cb);
try{
cb.linkToDeath(afdh,0);
}catch(RemoteException e){
return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
}
//mFocusStack就是AudioFocus机制所基于的栈
//mFocusStatck的元素类型为FocusStackEntry,它保存了一个回放实例相关的所有信息
//这里先处理一下特殊情况,如果申请者已经拥有AudioFocus,那怎么办呢
if(!mFocusStack.empty()&&mFocusStack.peek().mClientId.equals(clientId)){
//申请者已经位于栈顶的位置,也就是拥有了AudioFocus
if(mFocusStack.peek().mFocusChangeType==focusChangeHint){
//持有方式也没有变化,则直接返回成功
//看到了吗?这里又要unlinkToDeath了。这明显表明没有好好规划这段代码
cb.unlinkroDeath(afdh,0);
return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
}
//改变了持有方式,就把这个回放线程从栈上先拿下来,后面再重新加进去。这相当于重新申请
FocusStackEntry fse = mFocusstack.pop();
fse.unlinkToDeath();
}
/*接下来,我们通知位于栈顶,也就是当前拥有AudioFocus的回放实例,它的AudioFocus
要被夺走了。这个操作的主角自然是我们已经熟悉的AudioFocusDispatcher。*/
if(.mFocusstack.empty()&&(mFocusStack.peek().mFocusDispatcher!=null)){
try{
mFocusStack.peek().mFocusDispatcher.dispatchAudioFocusChange(
-1*focusChangeHint,//GAIN和LOSS是相反数,很聪明,不是吗?
mFocusStack.peek().mClientId);
}catch(RemoteException e){ }
}
/*由于申请者可能曾经调用过requestAudioFocus,但是目前被别人夺走了。所以它现在应该在
栈的某个位置上。先把它从栈中删除,注意第二个参数的意思是不通知AudioFocus的变化*/
removeFocusStackEntry(clientId,false);
//现在,为申请者创建新的FocusStackEntry放置到栈顶,使其拥有AudioFocus
mFocusStack.push(new FocusStackEntry(mainStreamrype,focusChangeHint,fd,cb,
clientId,afdh,callingPackageName,Binder.getcallinguid()));
//AudioFocus易主,我们需要通知RemoteView,但这不是我们目前的讨论范围
……
//告诉申请者它成功获得了AudioFocus
return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
代码比较长,但是思路还是比较清晰的。总结一下申请AudioFocus的工作内容:
口 通过canReassignAudioFocus()判断当前是否在通话中,如果在通话过程中,则直接拒
绝申请。具体如何让通话拥有最高优先级的问题可参考canReasignAudioFocus()的 实现。
口 对申请者进行linkToDeath。使得在申请者意外退出后可以代其完成abandonAudioFocus操作。
口 对于已经持有AudioFocus的情况,如果没有改变持有方式,则不作任何处理,直接
返回申请成功。否则将其从栈顶删除,暂时将栈顶让给下一个回放实例。
口 通过回调告知此时处于栈顶的回放实例,它的AudioFocus将被夺走。
口 将申请者的信息加入栈顶,成为新的拥有AudioFocus的回放实例。
申请AudioFocus的方式已经了解,那么释放AudioFocus是什么样的一个流程呢?读者
可以自己先思考一下。
3.4.4释放AudioFocus
先看AudioManager的abandonAudioFocus()函数,从这个函数中可以看出,向AudioService申请释放
AudioFocus需要提供两个“证件”:mAudioFocusDispatcher和AudioFocusChangeListener的ID。
[AudioManager.java-->AudioManager.abandonAudioFocus()]
public int abandonAudioFocus(OnAudioFocusChangeListener l){
int status=AUDIOFOCUS_REQUEST_FAILED;
//取消注册listener。这里将把l从Hashtable中删除
unregisterAudioFocusListener(l);
IAudioService service=getservice();
try{
//调用AudioService的abandonAudiofocus()
status=service.abandonAudioFocus(mAudioFocusDispatcher,
getIdForAudioFocusListener(1));
}catch (RemoteException e){ }
return status;
}
AudioService的abandonAudioFocus并没有更多的内容,只是调用了removeFocusStackEntry()函数而已。参考requestAudioFocus的实现过程,可以推断这个函数的工作有:
口 从mFocusStack中删除拥有指定clientld的回放实例的信息。
口 执行unlinkToDeath,取消监听其死亡通知。
口 如果被删除的回放实例位于栈顶的位置,说明AudioFocus还给了另外一个回放实例,
这时就要通过它的mFocusDispatcher回调,通知它重新获得了AudioFocus。
removeFocusStackEntry()的工作就是如此,只是实现得不够简练。
[AudioService.java-->AudioService.removeFocusStackEntry()]
private void removeFocusStackEntry(String clientToRemove,boolean signal){
if(!mFocusStack.empty()&&
mEocusstack.peek().mclientId.equals(clientToRemove)){ // 在栈顶的情况
//取消对生命状态的监控
FocusStackEntry fse=mFocusStack.pop();
fse.unlinkToDeath();
//通知栈顶的回放实例,AudioFocus回来了
if(signal){
notifyTopofAudioFocusstack();
//通知Remotecontro1,AudioFocus发生变化,这个不是我们讨论的主题
synchronized(mRCStack){
checkUpdateRemoteControlDisplay_syncAfRCS(RC_INFO_ALL);
}
}
}else{
//从栈中寻找要删除的回放实例,然后将其从栈中删除
IteratorstackIterator=mFocusstack.iterator();
while(stackIterator.hasNext()){
FocusStackEntry fse=(FocusstackEntry)stackIterator.next();
if(fse.mClientId.equals(clientToRemove)){
stackIterator.remove();
fse.unlinkroDeath();
}
}
}
}
看完AudioFocus的申请与释放的实现代码,读者能否感受到它们的实现在细节上确
实有些膝肿和重复。对比曾经分析过的静音控制相关的代码,实在差距不小。阅读
Android源代码的时候,我们不仅仅是在学习某些功能的原理,同时也是博取其代码
组织与书写的精妙之处,发现其不足的地方并引以为戒。
关于AudioFocus,读者是否有自己的想法改造其实现,让其更加精炼吗?
3.4.5 AudioFocus小结
这一节学习了AudioFocus机制的工作原理。AudioFocus机制有三部分内容:申请、释放
与回调通知,这些内容都是围绕一个名为mFocusStack的栈完成的。
在对代码的分析过程中,可以看到AudioFocus基本上是自成一个小的系统,没有和
外部服务,尤其是Audio底层打过交道,而且AudioFocus的回调通知只是告诉回放实例
AudioFocus发生了变化,无法保证回放实例在回调中做什么。这说明了AudioFocus作为一个
协调工具,是没有任何强制力的。希望在以后版本的Android中AudioFocus可以适当地增加
一些约束能力使得这套机制可以发挥更大的作用。
即便如此,AudioFocus作为唯一的通用的音频交互策略,建议每一个涉及音频播放的应
用都能参与这套机制,并且认真遵守其规则,这样才能保证Android音频“社会”的和谐。
3.5AudioService的其他功能
这一章已经介绍了音量控制、外设管理及AudioFocus几个常用重要功能的实现。然而,
AudioService仍然有很多其他相互独立的功能。限于篇幅,这里没有办法一一详细说明。在这
里简单介绍一下,以便读者自行研究。
(1)RemoteConrolClient/Display 机制
RemoteControlClient/Display 是从Android 4.0引入的一套新机制。它定义了一个远程控制
端、一个远程显示端。这使得媒体播放过程中的元数据(例如标题、艺术家等)与其他信息可
以跨应用显示。远程控制端由进行播放的应用管理,而远程显示端被一个显示界面管理,比
如说一个AppWidget。由AudioService作为中介为它们进行配对与数据传递。
(2)MediaButton的管理
所谓的MediaButon是指线控耳机上的一个按键,虽然耳机线上只有这一个按键,但
是它的功能却异常得多,例如接听/挂断电话,启动音乐播放器,暂停/继续/下一首,等
等。加上其使用方便,很多应用,尤其是播放器,争相操作(Handle)这个按键的事件。
AudioService就是为了能够协调争抢这个按键的应用才插手管理这个按键的派发。
(3)指定声音的输出设备
这个功能在AudioManger中表现为一系列名为setxXXOn的函数,其中的XXX表示了
一个音频输出设备的名字。它们其实都使用了AudioService的 setForceUse()函数。准确地说,
AudioService并没有为这个功能做过实际工作,只是作为应用到AudioPolicy的一个中介。
(4)音效管理
AudioService在启动时,会使用SoundPool工具预加载一系列的音效文件,用于系统中的
一些短小而频繁的音频播放,比如按键音。
SoundPool的工作原理是什么呢?在初始化时,AudioService要求SoundPool加载所需的
音频文件。SoundPool会把这些音频文件解码为PCM音频流并缓存。同时为每段音频流分配
一个ID,每当AudioService需要播放一段音效时,把对应的ID传递给SoundPool,SoundPool
就会找到这块缓存的音频流,通过AudioTrack 直接写入AudioFlinger中,实现音效播放。相
对于MediaPlayer,由于每次播放时省却了prepare与编解码的过程,因此效率比其高很多,
很适合用在游戏等对声音的及时性要求很高的场合。问题是,这个工具太消耗内存了。
(5)情景模式
情景模式和音量的关联是比较紧密的,或者说,情景模式是在音量控制的基础上实现的
一个功能。
(6)音频状态管理
在AudioService中就是两个函数:getMode0与setMode)。音频状态表示了手机的4种状
态,待机状态、音频通话状态、视频/VoIP通话状态与响铃状态。这4种状态对底层的音频输
出设备的选择影响很大,同时也影响了AudioService的一些行为,例如MediaButton的管理。
3.6本章小结
这一章介绍了AudioService的几个重要的功能,相信大家通过这章对Audio系统在
Java Famework层面所做的事情有了一个比较深入的了解。由于AudioService的功能太过繁
杂,本章只能将几个有代表意义并且实际接触比较多的内容进行讲解与探讨。若想更加了解
AudioService及其周边模块的工作原理仍需要读者不懈努力。
另外,AudioService的一些功能都涉及AudioPolicy的相关内容,所以在学习本章时要多
参考AudioPolicy的相关知识。