天天看点

《深入理解Android:卷III A》一一3.2音量管理

本节书摘来华章计算机出版社《深入理解android:卷iii a》一书中的第3章,第3.2节,作者:张大伟 更多章节内容可以访问云栖社区“华章计算机”公众号查看。1

android手机有两种改变系统音量的方式。最直接的做法就是通过手机的音量键进行音量调整,还有一种做法是从设置界面中调整某一种类型音频的音量。另外,应用程序可以随时将某种类型的音频静音。它们都是通过audioservice进行的。

本节将从上述三个方面对audioservice的音量管理进行探讨。

3.2.1音量键的处理流程

触发音量键

在音量键被按下后,android输入系统将该事件一路派发给activity,如果无人截获并处理这个事件,承载当前activity的显示phonewindow类的onkeydown()或onkeyup()函数将会处理,从而开始通过音量键调整音量的处理流程。输入事件的派发机制及phonewindow类的作用将在后续章节中详细介绍,现在只需要知道,phonewindow描述了一片显示区域,用于显示与管理我们所看到的activity和对话框等内容。同时,它还是输入事件的派发对象,而且只有显示在最上面的phonewindow才会收到事件。

按照android的输入事件派发策略,window对象在事件的派发队列中位于activity的后面,所以应用程序可以重写自己的activity.onkeydown()函数以截获音量键的消息,将其用作其他的功能。比如说,在一个相机应用中,按下音量键所执行的动作是拍照而不是调节音量。

phonewindow的onkeydown()函数实现如下:

[phonewindow.java-->phonewindow.onkeydown()]

......

注意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的这个函数。

adjustsuggestedstreamvolume()分析

我们先来看函数原型:

public void adjustsuggestedstreamvolume(int direction, int suggestedstreamtype,

adjustsuggestedstreamvolume()有三个参数,第一个参数direction指示了音量的调整方向,1为增大,-1为减小;第二个参数suggestedstreamtype表示要求调整音量;第三个参数flags的意思就不那么容易理解了。其实audiomanager在handlekeydown()中设置了两个flag,分别是flag_show_ui和flag_vibrate。从名字上我们就能看出一些端倪。前者告诉audioservice我们需要弹出一个音量控制面板。而在handlekeyup()里设置了flag_play_sound,这是为什么当在松开音量键后“有时候”会有一个提示音。注意,handlekeyup()中设置了flag_play_sound,但只是有时候这个flag才会生效,在下面的代码中可以看到这是为什么。还需要注意的是,第二个参数名为suggestedstreamtype,从其命名来推断,这个参数传入的流类型对audioservice来说只是一个建议,是否采纳这个建议,audioservice有自己的考虑。

看一下它的实现:

[audioservice.java-->audioservice.adjustsuggestedstreamvolume()]

}

初看这段代码时,可能有读者对下面这句代码感到疑惑:

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()的实现。

adjuststreamvolume()分析

[audioservice.java-->audioservice.adjuststreamvolume()]

public void adjuststreamvolume(int streamtype, int direction, int 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()。

volumestreamstate的adjustindex()分析

我们先看一下这个函数的定义:

[audioservice.java-->volumestreamstate.adjustindex()]

public b3oolean adjustindex(int deltaindex, int device) {

这个函数很简单,下面再看一下setindex()的实现:

[audioservice.java-->volumestreamstate.setindex()]

public synchronized boolean setindex(int index, int device, boolean lastaudible) {

int oldindex = getindex(device, false / lastaudible /);

index = getvalidindex(index);

// 在volumestreamstate中保存设置的音量值,注意使用了一个hashmap

mindex.put(device, getvalidindex(index));

if (oldindex != index) {

} else {

在这个函数中有三项工作要做:

首先保存设置的音量值。这是volumestreamstate的本职工作,这和android 4.1之前的版本不一样,音量值与设备相关联了。因此对同一种流类型来说,在不同的音频设备下将会拥有不同的音量值。

然后根据参数的要求保存音量值到mlastaudibleindex中。从名字就可以看出,它保存了静音前的音量。当取消静音时,audioservice就会恢复到这里保存的音量。

再就是对流映射的处理。既然a→b,那么在设置b的音量的同时要改变a的音量。这就是后面那个循环的作用。

可以看出,volumestreamstate.adjustindex()除了更新自己所保存的音量值外,没有做其他的事情。接下来再看一下msg_set_device_volume的消息处理做了什么。

msg_set_device_volume消息的处理

adjuststreamvolume()函数使用sendmsg()函数发送msg_set_device_volume消息给maudiohandler,这个handler运行在audioservice的主线程上。直接看一下在maudio-handler中负责处理msg_set_device_volume消息的setdevicevolume()函数:

[audioservice.java-->audiohandler.setindex()]

private void setdevicevolume(volumestreamstate streamstate, int device) {

sendmsg()是一个异步操作,这就意味着,完成adjustindex()更新音量信息后adjuststreamvolume()函数就返回了,但是音量并没有立刻被设置到底层。不过由于handler处理多个消息的过程是串行的,这就隐含着一种风险:如果当handler正在处理某一个消息时发生了阻塞,那么按下音量键,虽然调用adjuststreamvolume()可以立刻返回,并且从界面上看或用getstreamvolume()获取音量值都是没有问题的,但是手机发出声音时的音量大小并没有改变。

sendvolumeupdate()分析

接下来,分析一下sendvolumeupdate()函数,它用于通知外界音量发生了变化。

[audioservice.java-->audioservice.sendvolumeupdate()]

private void sendvolumeupdate(int streamtype, int oldindex, int index, int flags) {

这个函数将音量的变化通过广播的形式通知给其他感兴趣的模块。同时,它还特别通知了mvolumepanel。mvolumepanel是volumepanel类的一个实例。我们所看到的音量调节通知框就是它。

至此,从按下音量键开始的整个处理流程就完结了。在继续分析音量调节通知框的工作原理之前,先对之前的分析过程进行总结,参考图3-2的序列图。

图 3-2通过音量键调整音量的处理流程

结合上面分析的结果,由图 3-2可知:

音量键处理流程的发起者是phonewindow。

audiomanager仅仅起到代理的作用。

audioservice接受audiomanager的调用请求,操作volumestreamstate的实例进行音量的设置。

volumestreamstate负责保存音量设置,并且提供了将音量设置到底层的方法。

audioservice负责将设置结果以广播的形式通知外界。

到这里,相信大家对音量调节的流程已经有了一个比较清晰的认识。接下来我们将介绍音量调节通知框的工作原理。

7.音量调节通知框的工作原理

在分析sendvolumeupdate()函数时曾经注意到,它调用了mvolumepanel的post-volumechanged()函数。mvolumepanel是一个volumepanel的实例,作为一个handler的子类,它承接了音量变化的ui/声音的通知工作。在继续上面的讨论之前,先了解一下volumepanel工作的基本原理。

volumepanel位于android.view包下,却没有在api中提供,因为它只能被audioservice使用,所以和audioservice放在一个包下可能更合理一些。从这个类的注释上可以看到,谷歌的开发人员对它被放在android.view下也有极大不满(what a mass! 他们这么写道……)。

volumepanel下定义了两个重要的子类型,分别是streamresources和streamcontrol。streamresources实际上是一个枚举,它的每一个可用元素保存了一个流类型的通知框所需要的各种资源,如图标、提示文字等。streamresources的定义就像下面这样:

[volumepanel.java-->volumepanel.streamresources]

private enum streamresources {

};

这几个枚举项组成了一个名为stream的数组,如下:

[volumepanel.java-->volumepanel.streams]

private static final streamresources[] streams = {

volumepanel将从这个streams数组中获取它所支持的流类型的相关资源。这么做是不是有点啰嗦呢?事实上,在这里使用枚举并没有什么特殊的意义,使用一个普通的java类来定义streamresources就已经足够了。

streamcontrol类则保存了一个流类型的通知框所需要显示的控件,其定义如下:

[volumepanel.java-->volumepanel.streamcontrol]

private class streamcontrol {

很简单对不对?streamcontrol实例中保存了音量调节通知框中所需的所有控件。关于这个类在volumepanel的使用,我们可能很直观地认为只有一个streamcontrol实例,在对话框显示时,使其保存的控件按需加载指定流类型的streamresources实例中定义的资源。其实不然,出于对运行效率的考虑,streamcontrol实例也是每个流类型人手一份,和streamresources实例形成一一对应的关系。所有的streamcontrol实例被保存在一个以流类型的值为键的hashtable中,名为mstreamcontrols。我们可以在streamcontrol的初始化函数createsliders()中一窥端倪。

[volumepanel-->volumepanel.createsliders()]

private void createsliders() {

// 遍历stream中所有的streamresources实例

for (int i = 0; i < streams.length; i++) {

值得一提的是,这个初始化的工作并没有在构造函数中进行,而是在postvolume-changed()函数中处理的。

既然已经有了通知框所需要的资源和通知框的控件,接下来就要有一个对话框承载它们。没错,volumepanel保存了一个名为mdialog的dialog实例,这就是通知框的本身了。每当有新的音量变化到来时,mdialog的内容就会被替换为指定流类型对应的streamcontrol中所保存的控件,并且根据音量变化情况设置其音量条的位置,最后调用mdialog.show()显示出来。同时,发送一个延时消息msg_timeout,这条延时消息生效时,将会关闭提示框。

streamresource、streamcontrol与mdialog的关系就像图3-3所示的那样,streamcontrol可以说是mdialog的配件,随需拆卸。

图 3-3streamresource、streamcontrol与mdialog的关系

接下来具体看一下volumepanel在收到音量变化通知后都做了什么。我们在上一小节中说到了mvolumepanel.postvolumechanged()函数。它的内容很简单,直接发送了一条消息msg_volume_changed,然后在handlemessage中调用onvolumechanged()函数进行真正的处理。

volumepanel在msg_volume_changed的消息处理函数中调用onvolume-changed()函数,而不是直接在postvolumechanged()函数中直接调用。这么做是有实际意义的。由于android要求只能在创建控件的线程中对控件进行操作。postvolumechanged()作为一个回调性质的函数,不能要求调用者位于哪个线程中。所以必须通过向handler发送消息的方式,将后续的操作转移到指定的线程中。在设计具有ui controller功能的类时,volumepanel的实现方式有很好的参考意义。

下面看一下onvolumechanged()函数的实现:

[volumepanel.java-->volumepanel.onvolumechanged()]

protected void onvolumechanged(int streamtype, int flags) {

注意最后一个resettimeout()的调用,其实它重新延时发送了msg_timeout消息。当msg_timeout消息生效时,mdialog将被关闭。

之后就是onshowvolumechanged了。这个函数负责为通知框的内容填充音量、图表等信息,然后再显示通知框(如果还没有显示)。以铃声音量为例,省略其他的代码。

[volumepanel.java-->volumepanel.onshowvolumechanged()]

protected void onshowvolumechanged(int streamtype, int flags) {

至此,音量调节通知框就被显示出来了,下面总结一下它的工作过程:

postvolumechanged() 是volumepanel显示的入口。

检查flags中是否有flag_show_ui。

volumepanel会在第一次被要求弹出时初始化其控件资源。

mdialog 加载指定流类型对应的streamcontrol,也就是控件。

显示对话框并开始超时计时。

超时计时到达,关闭对话框。

到此为止,audioservice对音量键的处理流程介绍完毕。而 android还有另外一种改变音量的方式,即音量设置函数etstreamvolume(),下面对其进行介绍。

3.2.2通用的音量设置函数setstreamvolume()

除了可以通过音量键调节音量以外,用户还可以在系统设置中进行调节。audio-manager.setstreamvolume()是系统设置界面中调整音量所使用的接口。

setstreamvolume()分析

setstreamvolume()是sdk中提供给应用的api,它的作用是为特定的流类型设置范围内允许的任意音量。我们看一下它的实现:

[audioservice.java-->audioservice.setstreamvolume()]

public void setstreamvolume(int streamtype, int index, int flags) {

看明白这个函数了吗?抛开被忽略掉的那个if块可以归纳为:这个函数的工作其实很简单,就执行了下面三方面的工作:

为调用setstreamvolumeint()准备参数。

调用setstreamvolumeint()。

广播音量发生变化的通知。

下面分析的主线将转向setstreamvolumeint()的内容。

setstreamvolumeint()分析

看一下setstreamvolumeint()函数的代码,和前面一样,暂时忽略目前与分析目标无关的部分代码。

[audioservice.java-->audioservice.setstreamvolumeint()]

private void setstreamvolumeint(int streamtype,

此函数有两个工作内容,一个是调用streamstate.setindex(),另一个则是根据setindex()的返回值和force参数决定是否发送msg_set_device_volume消息。这两项内容在3.2.1节中已经介绍过,在此不再赘述。

至此,setstreamvolume()的分析完成。

分析完setstreamvolume()的工作流程后,读者是否觉得有些熟悉呢?如果我们用setstreamvolumeint()的代码替换setstreamvolume()中对setstreamvolumeint()的调用,再和adjuststreamvolume()函数进行以下比较,就会发现它们的内容出奇得相似。android在其他地方也有这样的情况。从这一点上来说,已经发展到4.1版本的android源代码仍然不够精致。读者可以思考一下,有没有办法把这两个函数融合为一个函数呢?

到此,对于音量设置相关的内容就告一段落。接下来我们将讨论和音量相关的另一个重要的内容—静音。

3.2.3静音控制

静音控制的情况与音量调节有很大的不同。因为每个应用都有可能进行静音操作,所以为了防止状态发生紊乱,就需要为静音操作进行计数,也就是说多次静音后需要多次取消静音。

不过,进行了静音计数后还会引入另外一个问题。如果一个应用在静音操作(计数加1)后因为某种原因不小心崩溃了,那么将不会有人再为它进行取消静音的操作,静音计数无法再回到0,也就是说这个“倒霉”的流将被永远静音下去。

那么怎么处理应用异常退出后的静音计数呢?audioservice的解决办法是记录下每个应用自己的静音计数,当应用崩溃时,在总的静音计数中减去崩溃应用自己的静音计数,也就是说,为这个应用完成它没能完成的取消静音这个操作。为此,volumestreamstate定义了一个继承自deathrecepient的内部类,名为volumedeathhandler,并且为每个进行静音操作的进程创建一个实例。volumedeathhandler的实例保存了对应进程的静音计数,并在进程死亡时进行计数清零的操作。从这个名字来看可能是google希望这个类将来能够承担更多与音量相关的事情吧,不过眼下它只负责静音。我们将在后续的内容中对这个类进行深入讲解。

经过前面的介绍,我们不难得出audioservice、volumestreamstate与volumedeathhandler的关系,如图3-4所示。

图 3-4与静音相关的类

setstreammute()分析

同音量设置一样,静音控制也是相对于某一个流类型而言的。正如本节开头所提到的,静音控制涉及引用计数和客户端进程的死亡监控。所以相对于音量控制来说,静音控制有一定的复杂度。还好,静音控制对外入口只有一个函数,就是audiomanager.setstreammute()。其第二个参数state为true,表示静音,否则表示解除静音。

[audiomanager.java-->audiomanager.setstreammute()]

public void setstreammute(int streamtype, boolean state) {

audiomanager一如既往地充当着audioservice代理的一个角色,不过这次有一个很小却很重要的动作:audiomanager为audioservice传入了一个名为micallback的变量。查看一下micallback的定义:

private final ibinder micallback = new binder();

真是简单得不得了。全文搜索一下,我们发现micallback只用来作为audioservice的几个函数调用的参数。从audiomanager角度看它没有任何实际意义。其实,这在android的进程间交互通信中是一种常见且非常重要的技术。micallback这个简单binder对象可以充当bp端在bn端的一个唯一标识。而且audioservice拿到这个标识后,就可以通过deathrecipient机制获取bp端异常退出的回调。这是audioservice维持静音状态正常变迁的一个基石。

服务端把客户端传入的这个binder对象作为客户端的一个唯一标识的时候,往往会以这个标识为键创建一个hashtable,用来保存每个客户端的相关信息。这在android各个系统服务的实现中是一种很常见的用法。

另外,本例传入的micallback是直接从binder类实例化出来的,是一个很原始的ibinder对象。进一步讲,如果传递了一个通过aidl定义的ibinder对象,那么这个对象就有了交互能力,服务端可以通过它向客户端进行回调。在后面探讨audiofocus机制时会遇到这种情况。

volumedeathhandler分析

我们继续跟踪audioservice.setstreammute()的实现,记得注意第三个参数cb,它代表特定客户端的标识。

[audioservice.java-->audioservice.setstreammute()]

public void setstreammute(int streamtype, boolean state, ibinder cb) {

接下来是volumestreamstate的mute()函数。volumestreamstate的确是音量相关操作的核心类型。

[audioservice.java-->volumestreamstate.mute()]

public synchronized void mute(ibinder cb, boolean state) {

上述代码引入了静音控制的主角,volumedeathhandler,也许叫做mutehandler更合适一些。它其实只有两个成员变量,分别是micallback和mmutecount。其中micallback保存了客户端传进来的标识,mmutecount则保存了当前客户端执行静音操作的引用计数。另外,它继承自ibinder.deathrecipient,所以它拥有监听客户端生命状态的能力。而volumedeathhandler()的成员函数只有两个,分别是mute()和binderdied()。说到这里,再看看上面volumestreamstate.mute()的实现,读者能想象到volumedeathhandler的具体实现是什么样子的吗?

继续上面的脚步,看一下它的mute()函数。它的参数state的取值指定了进行静音还是取消静音。所以这个函数也就被分成两部分,分别是处理静音与取消静音两个操作。其实,这完全可以放在两个函数中完成。先看看静音操作是怎么实现的吧。

[audioservice.java-->volumedeathhandler.mute()part 1]

public void mute(boolean state) {

看明白了吗?这个函数的条件嵌套比较多,仔细归纳一下,就会发现这段代码的思路是非常清晰的。静音操作根据条件满足与否,完成三个任务:

无论在什么条件下,只要执行这个函数,静音操作的引用计数都会加1。

如果这是客户端第一次执行静音,则开始监控其生命状态,并且把自己加入volumestreamstate的mdeathhandlers列表中。这是这段代码中很精练的一个操作,只有在客户端执行过静音操作后才会对其生命状态感兴趣,才有保存其volumedeathhandler的必要。

更进一步的是,如果这是这个流类型第一次被静音,则设置流音量为0,这才是真正的静音动作。

不得不说,这段代码是非常精练的,不是说代码量少,而是它的行为非常干净,决不会做多余的操作,也不会保存多余的变量。

下面我们要看一下取消静音的操作。取消静音作为静音的逆操作,相信读者已经可以想象到它都做什么事情了吧?这里就不再对其进行说明了。

[audioservice.java-->volumedeathhandler.mute() part 2]

下面就剩下最后的binderdied()函数了。当客户端发生异常,没能取消其执行过的静音操作时,需要替它完成它应该做却没做的事情。

[audioservice.java-->volumedeathhandler.binderdied()]

public void binderdied() {

这个实现不难理解,读者可以自行分析一下为什么这么做可以消除意外退出的客户端遗留下来的影响。

3.2.4音量控制小结

音量控制是audioservice最重要的功能之一。经过上面的讨论,相信读者对audioservice的音量管理流程已经有了一定的理解。

总结一下我们在这一节里所学到的内容:

audioservice音量管理的核心是volumestreamstate。它保存了一个流类型所有的音量信息。

volumestreamstate保存了运行时的音量信息,而音量的生效则是在底层audioflinger完成的。所以进行音量设置需要做两件事情:更新volumestreamstate存储的音量值,设置音量到audio底层系统。

volumedeathhandler是volumestreamstate的一个内部类。它的实例对应在一个流类型上执行静音操作的一个客户端,是实现静音功能的核心对象。

继续阅读