天天看点

《深入理解Android:卷III A》一一3.3音频外设的管理

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

这一节将探讨audioservice的另一个重要功能,那就是音频外设的管理。看过卷i第7章的读者应该对音频外设这个概念并不陌生。在智能机全面普及的时代,对有线耳机、蓝牙耳机等音频外设的支持已经是手机的标准,有些机型甚至支持hdmi、usb声卡等输出接口。再加上手机本身自带的扬声器与听筒,这样一来,一台手机上同时能进行音频输出的设备往往会有三四种甚至更多。如何协调这些设备的工作,使其符合用户的使用习惯、满足用户的需求变得非常重要。

卷i的第7章详细介绍过audiopolicy如何进行设备的路由切换,然而并没有讨论音频设备为什么出现在audiopolicy的设备候选列表中,这一节将以有线耳机为例讨论这个问题。

3.3.1 wiredaccessoryobserver 设备状态的监控

wiredaccessoryobserver简介

这要从wiredaccessoryobserver开始讲起,它是内核通知有线耳机插入事件所到达的第一个环节。

wiredaccessoryobserbver继承自ueventobserver。ueventobserver是android用来接收uevent的一个工具类。ueventobserver类维护着一个读取uevent的线程,注意这个线程是ueventobserver的一个静态成员,也就是说,一个进程只有一个。当调用ueventobserver的startobserving()函数开始监听时,会告诉这个线程ueventobserver关心什么样的uevent,当匹配的事件到来时,监听线程会通过回调ueventobserver的onuevent函数进行通知。读者可以看一下ueventobserver的源代码以了解其具体实现,这并不复杂。

wiredaccessoryobserver接收内核上报的和耳机/hdmi/usb相关的uevent事件,并将其翻译成设备的状态变化。由于每种外设都有自己的uevent与状态文件,因此wiredaccessoryobserver定义了一个内部类名为ueventinfo, 并且为自己感兴趣的每一个音频外设创建一个实例,其内部保存了对应外设的名字、uevent地址及状态文件的地址。每当有合适的uevent到来时,wiredaccessoryobserver就会查找匹配的ueventinfo实例,并且更新可用设备的状态列表,同时通知audioservice。

关于可用外设的状态列表,虽然称为列表,事实上,它只是一个整型的变量,名为mheadsetstate。在可用外设的状态列表中用一个二进制标志位表示某个外设的状态可用与否,这与audiopolicymanager的mavailableoutputdevices的用法是一样的。下面是各种外设的标志位的定义:

举个例子,如果mheadsetstate等于0x00000002,也就是bit_headset_no_mic,表示目前手机上插入一个不带麦克风的耳机。而如果mheadsetstate等于0x00000011,也就是headsets_with_mic | bit_hdmi_audio,则表示目前手机上同时插入一个带有麦克风的耳机及hdmi输出线。

wiredaccessoryobserver工作原理就这么简单,我们接下来将以有线耳机为例子对其进行详细讨论。

启动与初始化

虽然wiredaccessoryobserver不是一个服务,但是它拥有系统服务的待遇—在system_server中同系统服务一起被加载,如下所示:

只有一个构造函数,其实,构造函数中并没有做太多的初始化工作,而是注册了一个broadcastreceiver,监听action_boot_complete。其真正的初始化工作是在这个bootcompletedreceiver中完成的。

这里的init()函数的作用是为了在开机后对外设的状态进行初始化。

到这里wiredaccessoryobserver已经完成初始化了,已经对第一条uevent的到来准备就绪。

耳机插入或拔出时的处理

如果有外设被插入或拔出,wiredaccessoryobserver的onuevent()函数会被回调。参数event中保存了其详细的信息。

看到这里,读者是否觉得updatestate的实现有些笨拙了呢?如果以devname为键,将ueventinfo保存在hashtable中,无论对代码的整洁还是执行的效率都是有帮助的。

注意uei.computenewheadsetstate()这个函数,它的目的是通过uevent上报的状态值计算出新的可用外设列表。

computenewheadsetstate()这个函数的扩展性并不是太好,只是目前够用而已,读者可以自行研究。

继续前面的脚步,现在到了update()函数。这个函数的目的是对前面传入的newstate进行全面检查,防止出现不正确的状态。这个函数的运算稍多些,为了方便分析,仅留下和有线耳机(h2w)相关的代码。

这个函数的意图比较很明显,只是其中一个判断条件让人一时摸不着头脑,(h2w_headset & (h2w_headset - 1)) != 0。按照注释中的说法,此函数不接受同时有两种耳机出现的情况,也就是h2w_headst == bit_headset | bit_headset_no_mic,直接做这个判断不就可以了吗?仔细琢磨就能发现写这个条件的人的聪明之处。直接判断仅限于只有两种可能的外设时才能起作用,超过两个就很难处理了。而谷歌的这个做法既快捷,又可以应对任意多种可能的外设。读者可以思考一下为什么。

另外,这段代码在执行mhandler.sendmessage()的调用之前先申请了一个电源锁。这是一个很细节但很重要的做法。当发送消息给一个handler时,必须考虑设备有可能在handler得以处理消息之前进入深睡眠状态的极端情况(对延时消息来说,可能就是常见情况了)。在这种情况下,cpu将会进入休眠状态,从而使得消息无法得到及时处理,影响程序执行的正确性。

可用外设列表更新完毕后发送了一条消息给mhandler。当消息生效时,直接调用setdevicesstate()函数,它会遍历所有supported_headset,然后对每个外设调用setdevicestate()。注意,这两个函数是devices与device的区别。setdevicestate()的目的就是要把指定外设的状态汇报给audioservice,我们看一下它的实现:

之后,程序的流程将会离开wiredheadsetobserver,再次前往audioservice。

总结一下wiredaccessoryobserver

对wiredheadsetobserver的分析就先告一段落,这里再简单回顾一下关于它的知识。

它是站在最前方的一个哨兵,时刻监听着和音频外设拔插相关的uevent事件。

它接收到uevent事件后,会翻译事件的内容为外设可用状态的变化。

它是为audioservice服务的,一旦有变化就立刻通知audioservice。

它虽然不是一个服务,但是它却运行在system_server中。

它不是唯一的音频外设状态监听者,它只负责监控有线连接的音频外设。其他的,如蓝牙耳机,在其他相关模块中维护。但是它们的本质是类似的,最终都要通知给audioservic。有兴趣的读者可以自行研究。

3.3.2audioservice的外设状态管理

最终还是要回到audioservice中来,它才是音频相关操作的主基地。

处理来自wiredaccessoryobserver的通知

audioservice会如何处理外设的可用状态变化呢?仔细想想,在开发播放器的时候一定接触过action_audio_becoming_noisy和action_headset_plug这两个广播吧。另外,更重要的是,这些变化需要让底层的audiopolicy知道。所以,笔者认为audioservice外设状态管理分为三个内容:

管理发送action_audio_becoming_noisy广播。

发送设备状态变化的广播,通知应用。

将其变化通知底层。

从wiredheadsetobserver调用的setwireddeviceconnectionstate()函数开始:

此函数负责两项工作:调用checksendbecomingnoisyintent()函数及发送set_wired_device_connection_state消息给maudiohandler。

checksendbecomingnoisyintent()函数的目的是判断当前状态的变化是否有必要发送becoming_noisy广播。这个广播用于警告所有媒体播放应用声音即将从手机外放中进行播放。在绝大部分情况下,收到这个广播的应用都应当立即暂停播放,以避免用户无意识地泄露自己的隐私或打扰到周围的其他人。另外,这个函数的返回值决定了set_wired_device_connection_state消息是否需要延时处理。其代码如下:

代码不长,有价值的内容不少。becoming_noisy广播发出的条件是最后一个安静外设被拔出,这个很好理解。而推迟msg_set_wired_device_connection_state消息的生效时间这种做法可能一时难以弄明白。不过暂时先不管它,等我们了解了外设连接状态变化的流程后再解释它的意义。

回到setwireddeviceconnectionstate (),调用checksendbecomingnoisyintent()函数后,它发送msg_set_wired_device_connection_state给maudiohandler,此消息生效后,maudiohandler调用onsetwireddeviceconnectionstate函数。

senddeviceconnectionintent(device, state, name);

}

在这个函数中,我们需要重点关注的是对handledeviceconnection()和senddevice-connectionintent两个函数的调用。它们分别用来通知audiopolicy与上层应用。

另外,还可以看到,在handledeviceconnection()函数上下有一对关于蓝牙耳机的操作。从其实现上可以看出,如果拔出普通耳机,系统将会强制使用蓝牙耳机进行输出。如果插入耳机则会取消这个设置。这种操作完全可以放在audiopolicymanager中实现。

很简单吧?如果读者对卷i第7章的内容比较熟悉,那么一定知道audiosystem.setdeviceconnectionstate()这个函数意味着什么。它将更新底层的audiopolicy中缓存的可用设备列表,同时,如果正在进行音频播放,那么这个函数还将触发音频设备的重新选择。

这一节提到“可用设备列表”的次数很多,很多地方都使用了这个概念。归纳一下,在本节所讨论的内容里,有三个地方有可用设备列表:

1)wiredaccessoryobserver: 目的是确认外设的状态变化是否合法,是否需要报告给audioservice。

2)audioservice: 它以一个hashtable的形式保存了一个可用设备列表,它为audioservice向应用及底层audiopolicymanager发送通知提供依据。

3)audiopolicymanager: 它保存的可用设备列表在audiopolicymanager需要重新选择音频输出设备时提供候选。

关于推迟处理外设状态

前面讨论checksendbecomingnoisyintent()函数的实现时提到了根据某些条件,有可能使msg_set_wired_device_connection_stat延迟生效1秒。在这种情况下应用会在1秒之后才能收到设备状态变化的广播,同时,audiopolicy也要在1秒之后才能更新可用设备列表并进行必要的设备切换。为什么要这么做呢?想想推迟的条件:

最后一个安静外设被移除,发送了becoming_noisy广播。

队列中尚有两个消息在等候处理:msg_set_wired_device_connection_state 和msg_set_a2dp_connection_state。

只要这两个条件有一个满足,就会发生1秒推迟。下面分别讨论。

关于第一个条件,当最后一个安静外设被移除后,手机上可用的音频输出设备就只剩下扬声器了(听筒不能算是常规的音频输出设备,它只有在通话过程中才会用到)。那么在msg_set_wired_device_connection_stat生效后,audiopolicymanager将会切换输出到扬声器,此时正在播放的音频就会被外放出来。

很多时候,这并不是用户所期望的,用户可能不希望他人知道自己在听什么,或者不希望在某些场合下扬声器发出的声音打扰到其他人。何况耳机被拔除有可能还是个意外。所以,正在进行音频播放的应用可能希望收到耳机等安静设备被拔出时的通知,并且在收到后暂停播放。

读者可能会有疑问,在senddeviceconnectionintent()中不是发送了状态通知的广播了吗?其实,这个状态通知广播用在其他情况下可以,但是用在上述情况中是有问题的。按照上面的讨论,执行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得到相关模块发来的通知,根据需要发送becoming_noisy消息给应用,并更新自己的可用设备列表。

audioservice将外设可用状态的变化通知audiopolicy。audiopolicy更新自己的可用设备列表,并重新选取音频输出设备。

audioservice将外设可用状态以广播的形式发送给应用等其他对此感兴趣的应用程序或系统模块。

蓝牙模块负责蓝牙耳机的连接/断开状态的监控并通知audioservice。audioservice收到此通知之后的代码路径虽然与本节所讨论的内容不完全一样,但其处理原则与有线耳机是一致的,读者可以自行分析学习。

继续阅读