# 第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 voidadjustSuggestedStreamVolume(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 intoldIndex = 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) {
??? intoldIndex = getIndex(device, false? /*lastAudible */);
??? index =getValidIndex(index);
??? // 在VolumeStreamState中保存設置的音量值,注意是用了一個HashMap
???mIndex.put(device, getValidIndex(index));
??? if(oldIndex != index) {
??????? // 保存到lastAudible
??????? 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的具體實現是什么樣子的么?
繼續上面的腳步,看一下它的mute()函數。它的參數state的取值指定了進行靜音還是取消靜音。所以這個函數也就分成了兩部分,分別處理靜音與取消靜音兩個操作。其實,這完全可以放在兩個函數中完成。先看看靜音操作是怎么做的吧。
```
[AudioService.java-->VolumeDeathHandler.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) {
???????????????????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的一個內部類。它的實例對應了在一個流類型上執行了靜音操作的一個客戶端,是實現靜音功能的核心對象。