《深入理解Android:卷III A》一一3.4AudioFocus机制的实现

    xiaoxiao2023-07-20  152

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

    3.4AudioFocus机制的实现

    AudioFocus是自Android 2.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使用方法。下面是一个播放器的部分代码:

    publicvoidplay(){ // 在开始播放前,先申请AudioFocus,注意传入的参数 int result = mAudioManager.requestAudioFocus( mAudioFocusListener,AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); // 只有成功申请到AudioFocus之后才能开始播放 if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) mMediaPlayer.start(); else // 申请失败,如果系统没有问题,这一定是正在通话过程中,所以,还是不要播放了 showMessageCannotStartPlaybackDuringACall(); } public void stop() { // 停止播放时,需要释放AudioFocus mAudioManager.abandonAudioFocus(mAudioFocusListener); }

    在开始播放前,应用首先要申请AudioFocus。申请AudioFocus时传入了3个参数。mAudioFocusListener:参数名为l。AudioFocus变化通知回调。当AudioFocus被其他AudioFocus使用者抢走或归还时将通过这个回调对象进行通知。AudioManager.STREAM_MUSIC: 参数名为streamType。这个参数表明了申请者即将使用哪种流类型进行播放。目前这个参数仅提供一项信息而已,对AudioFocus的机制没有任何影响,不过Android在后续的升级中可能会使用此参数。AudioManager.AUDIOFOCUS_GAIN: 参数名为durationHint。这个参数指明了申请者将拥有这个AudioFocus多长时间。例子中传入的AUDIOFOCUS_GAIN表明了申请者将长期占用这个AudioFocus。另外还有两个可能的取值,它们是AUDIOFOCUS_GAIN_TRANSIENT和AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK。这两个取值的含义都表明申请者将暂时占用AudioFocus,不同的是,后者还指示了即将被申请者抢走AudioFocus的使用者不需要暂停,只要降低一下音量就可以了(这就是“DUCK”的意思)。在停止播放时,需要调用abandonAudioFocus释放AudioFocus,会将其归还给之前被抢 走AudioFocus的那个使用者。接下来,我们看一下mAudioFocusListener是如何实现的。

    private onAudioFocusChangeListenermAudioFocusListener = newOnAudioFocusChangeListener(){ // 当AudioFocus发生变化时,这个函数将会被调用。其中参数focusChange指示发生了什么变化 publicvoidonAudioFocusChange(int focusChange){ switch(focusChange) { // AudioFocus被长期夺走,需要中止播放,并释放AudioFocus // 这种情况对应于抢走AudioFocus的申请者使用了AUDIOFOCUS_GAIN case AUDIOFOCUS_LOSS: stop(); break; // AudioFocus被临时夺走,不久就会被归还,只需要暂停,AudioFocus被归还后再恢 复播放 // 这对应于抢走AudioFocus的申请者使用了AUDIOFOCUS_GAIN_TRANSIENT 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.2AudioFocus实现原理简介看了前面的示例代码,可以推断出,AudioFocus的实现基础应该是一个栈。栈顶的使用者拥有AudioFocus。在申请AudioFocus成功时,申请者被放置在栈顶,同时,通知之前在栈顶的使用者,告诉它新的申请者抢走了AudioFocus。当释放AudioFocus时,使用者将从栈中被移除。如果这个使用者位于栈顶,则表明释放前它拥有AudioFocus,因此AudioFocus将被返还给新的栈顶。

    AudioFocus的工作原理就是如此。下面将分别对申请和释放这两个过程进行详细分析。3.4.3申请AudioFocus先看一下AudioFocus的申请过程。

    [AudioManager.java-->AudioManager.requestAudioFocus()] public int requestAudioFocus(OnAudioFocusChangeListener l, int streamType, int durationHint) { int status = AUDIOFOCUS_REQUEST_FAILED; // 注册AudioFocusListener registerAudioFocusListener(l); IAudioService service = getService(); try { // 调用AudioService的requestAudioFocus函数,参数很多,分别是什么意思呢 status = service.requestAudioFocus(streamType, durationHint, mICallBack, mAudioFocusDispatcher, getIdForAudioFocusListener(l), mContext.getPackageName()); } catch (RemoteException e) { } return status; }

    这段代码有两个关键的地方,分别是调用registerAudioFocusListener()和调用Audio-Service的requestAudioFocus()函数。这个函数的参数很多,随着分析的深入,这些参数的意思会逐渐明朗。下面先看一下registerAudioFocusListener做了什么。

    [AudioManager.java-->AudioManager.registerAudioFocusListener()] public void registerAudioFocusListener(OnAudioFocusChangeListener l) { synchronized(mFocusListenerLock) { if (mAudioFocusIdListenerMap.containsKey(getIdForAudioFocusListener(l))) { return; } mAudioFocusIdListenerMap.put(getIdForAudioFocusListener(l), l); } }

    原来,应用程序提供的AudioFocusChangeListener是被注册进AudioManager的一个Hashtable中而不是AudioService中。注意,这个Hashtable的KEY是getIdForAudioFocus-Listener()分配的一个字符串型的Id。这样看来,AudioManager这一侧一定有一个代理负责接受AudioService的回调并从这个Hashtable中通过Id将回调转发给相应的Listener。究竟是谁在做这个代理呢?就是稍候将作为参数传递给AudioService的mAudio-FocusDispatcher。

    [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.request-AudioFocus()时传入的参数很多。它们都是什么意义呢?为了方便后面探讨,我们先把AudioService.requestAudioFocus的参数意义搞清楚。mainStreamType: 这个参数目前没有被使用,这是为了便于以后功能扩展所预留的一个参数。focusChangeHint:这个参数指明了申请者持有AudioFocus的方式。cb:IBinder类型的一个参数,在探讨静音控制时曾经见过这个参数,就是AudioManager的mICallBack。应该能够联想到,AudioService又要做linkTo-Death了。fd:IAudioFocusDispatcher对象,我们刚刚分析过,它是AudioService回调回放实例的中介。clientId:参考AudioManager.requestAudioFocus()的实现,它是通过getget-IdForAudioFocusListener()函数获取的一个字符串,用于唯一标识一个Audio-FocusChangeListener。这个Id真的是唯一的吗?getIdForAudioFocusListener()返回的其实就是一个toString()。这样做是无法严格区分两个不同的AudioFocusListener实例的。callingPackageName: 回放实例所在的包名。接下来看看AudioService.requestAudioFocus()的工作原理。[AudioService-->AudioService.requestAudioFocus()]public int requestAudioFocus(int mainStreamType, int focusChangeHint, 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_REQUEST_FAILED; } /* 看到AudioFocusDeathHandler后,可以对比一下VolumeDeathHandler的工作,这里之所 以需要监控其生命状态,就是为了防止一个使用者还没有来得及调用abandonAudioFocus就崩 溃。所以读者一定知道VolumeDeathHandler的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.unlinkToDeath(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(mainStreamType, focusChangeHint, fd, cb, clientId, afdh, callingPackageName, Binder.getCallingUid())); // AudioFocus易主,我们需要通知RemoteView,但这不是我们目前的讨论范围 …… } // 告诉申请者它成功获得了AudioFocus return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;

    }代码比较长,但是思路还是比较清晰的。总结一下申请AudioFocus的工作内容:通过canReassignAudioFocus()判断当前是否在通话中,如果在通话过程中,则直接拒绝申请。具体如何让通话拥有最高优先级的问题可参考canReassignAudioFocus()的实现。对申请者进行linkToDeath。使得在申请者意外退出后可以代其完成abandon-AudioFocus操作。对于已经持有AudioFocus的情况,如果没有改变持有方式,则不作任何处理,直接返回申请成功。否则将其从栈顶删除,暂时将栈顶让给下一个回放实例。通过回调告知此时处于栈顶的回放实例,它的AudioFocus将被夺走。将申请者的信息加入栈顶,成为新的拥有AudioFocus的回放实例。申请AudioFocus的方式已经了解,那么释放AudioFocus是什么样的一个流程呢?读者可以自己先思考一下。3.4.4释放AudioFocus先看AudioManager的anbandonAudioFocus()函数,从这个函数中可以看出,向Audio-Service申请释放AudioFocus需要提供两个“证件”:mAudioFocusDispatcher和AudioFocus-ChangeListener的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(l)); } catch (RemoteException e) { ...... } return status; }

    AudioService的abandonAudioFocus并没有更多的内容,只是调用了removeFocus-StackEntry()函数而已。参考requestAudioFocus的实现过程,可以推断这个函数的工作有:从mFocusStack中删除拥有指定clientId的回放实例的信息。执行unlinkToDeath,取消监听其死亡通知。如果被删除的回放实例位于栈顶的位置,说明AudioFocus还给了另外一个回放实例,这时就要通过它的mFocusDispatcher回调,通知它重新获得了AudioFocus。removeFocusStackEntry()的工作就是如此,只是实现得不够简练。[`javascriptAudioService.java-->AudioService.removeFocusStackEntry()]private void removeFocusStackEntry(String clientToRemove, boolean signal) {

    if (!mFocusStack.empty() && mFocusStack.peek().mClientId.equals(clientToRemove)) { // 取消对生命状态的监控 FocusStackEntry fse = mFocusStack.pop(); fse.unlinkToDeath(); // 通知栈顶的回放实例,AudioFocus回来了 if (signal) { notifyTopOfAudioFocusStack(); // 通知RemoteControl, AudioFocus发生变化,这个不是我们讨论的主题 synchronized(mRCStack) { checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL); } } } else { // 从栈中寻找要删除的回放实例,然后将其从栈中删除 Iterator<FocusStackEntry> stackIterator = mFocusStack.iterator(); while(stackIterator.hasNext()) { FocusStackEntry fse = (FocusStackEntry)stackIterator.next(); if(fse.mClientId.equals(clientToRemove)) { stackIterator.remove(); fse.unlinkToDeath(); } } }

    }

    看完AudioFocus的申请与释放的实现代码,读者能否感受到它们的实现在细节上确实有些臃肿和重复。对比曾经分析过的静音控制相关的代码,实在差距不小。阅读Android源代码的时候,我们不仅仅是在学习某些功能的原理,同时也是博取其代码组织与书写的精妙之处,发现其不足的地方并引以为戒。 关于AudioFocus,读者是否有自己的想法改造其实现,让其更加精炼吗? **3.4.5AudioFocus小结** 这一节学习了AudioFocus机制的工作原理。AudioFocus机制有三部分内容:申请、释放与回调通知,这些内容都是围绕一个名为mFocusStack的栈完成的。 在对代码的分析过程中,可以看到AudioFocus基本上是自成一个小的系统,没有和外部服务,尤其是Audio底层打过交道,而且AudioFocus的回调通知只是告诉回放实例AudioFocus发生了变化,无法保证回放实例在回调中做什么。这说明了AudioFocus作为一个协调工具,是没有任何强制力的。希望在以后版本的Android中AudioFocus可以适当地增加一些约束能力使得这套机制可以发挥更大的作用。 即便如此,AudioFocus作为唯一的通用的音频交互策略,建议每一个涉及音频播放的应用都能参与这套机制,并且认真遵守其规则,这样才能保证Android音频“社会”的和谐。
    最新回复(0)