本節書摘來華章計算機出版社《深入了解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收到此通知之後的代碼路徑雖然與本節所讨論的内容不完全一樣,但其處理原則與有線耳機是一緻的,讀者可以自行分析學習。