Android應用開發以及設計思想深度剖析
1. 支撐應用程式的Android系統
分析一個系統的構成,可以有多個出發點。從不同出發點,我們可從不同側面分析這個系統的設計,以及為什麼要這樣設計:
T 從系統結構出發,Android系統給我們的感覺就是一種簡潔的,分層式的構架實現,從這種分層式的構架實現角度,我們可以理解這個系統是如何被組織到一起。
T 從系統的執行態角度出發,我們又可以從Android的整個啟動過程裡做了哪些工作,系統是由哪些執行態的組成部分來構造起來的。
T 從原始碼的結構出發,我們可以通過Android的原始碼,來分析Android系統具體實現。設計最終都會落實到程式碼,通過程式碼,我們可以逆向地看出來這個系統是如何構建起來的。
這種不同的分析角度,通常也包含著不同的用意,一般是在Android開發裡從事不同的方向所導向的最方便的一種選擇。比如,作為底層移植者而言,大部分都是BSP(Board Support Package)工程師的工作,比較接近硬體底層,一般就會暴力一點,從Linux啟動開始分析,得到執行態的Android系統概念。作為高階的軟體設計師,看這個系統,可能更多的會是從系統結構出發,來了解這一系統設計上的技巧與原理。而更多的學習者或是愛好者,則會是直接從原始碼來著手,一行行程式碼跟,最後也會理解到Android的系統的構架。
我們前面粗淺地介紹了Android的程式設計,並非只是浪費篇張,而是作為我們這裡獨特的分析角度的一種鋪墊。我們的角度,由會先看應用程式,然後回過頭來再分析Android系統是如何支援應用程式執行的,如何通過精巧設計來完成基於功能來共享的Android系統的。這種角度可能更接近逆向工程,但這時我們也能瞭解到Android系統構架的各種組織部分,而且更容易理解為什麼Android系統,會是這樣看上去有點怪的樣子。
作為Android這樣的系統,或者是任何的智慧手持裝置上的作業系統,其實最大挑戰並非功能有多好、效能有多好,而首先面臨是安全性問題。桌面作業系統、伺服器作業系統,在設計上的本質都是要儘可能提供一種多人共享化的系統,而所謂的智慧手機作業系統,包含了個人的隱私資訊,需要在保護這些隱私資訊條件下來提供儘可能多的功能,這對安全性要求是一個很大的挑戰。特別像Android這樣,設計思路本就是使用者會隨時隨地地下載更多應用程式到系統裡,相當於是添加了更多功能到系統裡,這時就必須解決安全性問題。
以安全性角度出發,Android必然會引入一些特殊的設計,而這些設計帶來的一個直接後果就是會帶來一些連鎖性的設計上的難題:即要保持足夠的效能、同時又要控制功耗;即要功能豐富,同時又需要靈活的許可權控制;需要儘可能實現簡單,但同時又需要提供靈活性。所有這些後續問題的解決,就構成了我們的Android系統。
1.1 Android應用程式執行環境所帶來的需求
我們在實現類似於Android應用程式的支撐環境時,必然會遇到一些比較棘手的問題,當我們能夠解決這些問題時,我們就得到了我們想要的Android系統。這類問題有:
l 安全性:作為一個功能可靈活拓展的系統,都將面臨安全性的挑戰。特別是Android又是一個開源作業系統,它不能像iOS那樣保持封閉來保持一定的安全性,又不能過於嚴格地限制開發者自由來達到安全性目的的,而且由於是嵌入式裝置,還不能過於複雜,複雜則執行效率不夠。於是,安全性是Android這套執行機制最大的挑戰。
l 效能:如果Android系統僅僅只是能夠提供桌面上的Java Applet,或是老式手機上的JAVA ME那樣的效能,則Android系統則會毫無吸引力。而我們的安全性設計,總會帶來一定的效能上的開銷,這時可能會導致Android系統的表現還不如標準的Java環境。
l 跨程序互動:因為有了Intent與Activity,我們的系統可能隨時隨地都在互動,而且會是跨程序的互動,我們需要了解Android裡獨特的程序間通訊的方式。
l 功耗控制:所有使用電池供電的裝置,都天生地有強烈的功耗控制的需求。隨著處理器的能力加強,這時功耗會變得得更大,提供合理的功耗控制是一種天生地需求。
l 功能:如前面所說,我們提供了安全性系統,又不能限制應用程式在使用上的需求,我們應該儘可能多地提供系統裡有的硬體資源,系統能夠提供的軟體層功能。
l 可移植性:作為一個開源的野心勃勃的智慧手機作業系統,Android必須在可移植性上,甚至是跨平臺上都要表現良好。只有這樣,才會有更多廠商樂意生產Android裝置,開發者會提供更多應用程式,從而像今天這樣形成良性迴圈的Android生態環境。
我們再來針對這些應用程式所必須解決的問題,一個個地看Android是如何解決的。
1.2 安全性設計
安全性是軟體系統永恆的主題,其緊迫程度與功能的可拓展性成正比。越是可以靈活拓展的系統,越是需要一種強大的安全控制機制。世界上最安全的系統,就是一坨廢鐵,因為永遠不可能有新功能加入,於是絕對安全。如果我們可以在其上編寫程式,則需要提供一定程度的安全控制,這時程式有好有壞,也有可能出錯。如果我們的軟體,會通過網際網路這樣的渠道可以獲得,則這種安全上需求會更強烈,因為各種各樣的邪惡用意都有可能存在。大體上說,安全性控制會有四種需求:
l 應用程式絕對不能對系統造成破壞。作為一個系統,它的首要目標當然是共享給運行於其上的應用程式以各種系統級的功能。但如果這些應用程式,如果可以通過某種渠道對這個共享的系統造成破壞,這樣的系統去執行程式就沒有意義,因為這時系統過於脆弱。
l 應用程式之間,最好不能互相干擾。如果我們的應用程式,互相之間可以破壞對方的資料,則也不會有很好的可用性,因為這時單個的應用程式也還是脆弱的。
l 應用程式與系統,應用程式之間,應該提供共享的能力。在安全性機制下,我們也還是需要提供手段,讓應用程式與系統層之間、應用程式之間可以互動。
l 還需要許可權控制。我們可以通過許可權來保護系統,一些非法的程式碼在沒有許可權的情況就無法造成破壞。在給不同應用程式提供系統層功能、提供共享時,應用程式有許可權才能執行,沒有許可權則會拒絕應用程式的訪問。
解決這類安全性需求的辦法,有繁有簡,像Android這樣追求簡潔,當然會使更簡潔的方案,同時這套方案得非常可靠。於是Android的執行模型就參考了久經40年考驗的單核心作業系統的安全模型。
為了解釋得更清楚一點,我們先來從單核心作業系統開始說起。
在計算機開始出現的原始時期,我們的計算機是沒有所謂作業系統的。即使是我們的PC,也是由DOS開始,這樣的所謂作業系統,實際上也只是把一些常用的庫封閉起來,可以支援一個字元的操作介面,執行完一個任務會退回到操作介面,然後才能再執行下一個。這樣的作業系統效能不高,在做一些耗時操作則必須等待。
於是,大家又給CPU的中斷控制入手,一些固定的中斷源(比如時鐘中斷)會打斷CPU操作而強制讓CPU進入一段中斷處理程式碼,在這種中斷處理程式碼里加入一些程式碼跳轉執行的邏輯,就允許程式碼可以有多種執行序列。這樣的程式碼序列被抽象成任務,而這種修改過的中斷處理程式碼則被稱為排程器,這樣得到的作業系統就叫多工作業系統,因為這些作業系統上執行的程式碼像是有多個任務在並行一樣。
這種模型實現簡單,同時所謂的任務排程只是一次程式碼跳轉,開銷也小,實際上我們今天也在廣泛地用它,比如大部分的實時作業系統。但在這種模式裡有個很致命的缺陷,就是任務間的記憶體是共享的,這就跟我們想達到的安全性機制不符,應用程式會有可能互相破壞。這是目前大家在實時作業系統做開發的一個通病,90%的比較有歷史的實時系統裡,大量使用全域性變數(因為記憶體是可以共享訪問的),幾乎到了無法維護的程度了。大部分情況下,決定程式碼質量的,並非框架設計,而是寫程式碼的人。當系統允許犯使用全域性變數的錯誤,大家就會隔三差五的因為不小心使用到,而累積到最後,就會是一大坨無法再維護的全域性變數。
於是,在改進的作業系統裡,不但讓每個任務有獨立的程式碼執行序列,同時也給它們虛擬出來獨立的記憶體空間。這時,系統裡的每個任務執行的時候,它自己會以為整個世界裡只有它存在,於是很開心地執行,想怎麼玩就怎麼玩,就算把自己玩掛掉,對系統裡的其他執行的任務完全沒有影響,因為這時每個任務所用的記憶體都是虛擬出來互相獨立的空間。當然,此時還會有一些共享的需求,比如訪問硬體,訪問一些所有任務都共享的資料,還有可能需要多個任務之間進行通訊。這時,就再設立一種特權模式,在這種模式裡則使用同一份記憶體,來完成這種共享的需求。任務要使用這種模式,必須通過一層特殊的系統呼叫來進入。在Unix系列的作業系統裡,我們這裡的任務(task)被稱為程序,給每個程序分配的獨立的記憶體區域,被稱為使用者空間,而程序間特權模式下共享的那段空間,被稱為核心空間。
特權模式裡的程式碼經過精心設計,確保執行時不出錯,這時就完善了我們前面提到的安全性模型。
有了多工的支援後,特別是歷史上計算機曾經極度昂貴,這時大家覺得只一個人佔著使用,有了多工也很浪費,希望可以讓多人共享使用。這時,每個可以使用計算機的人,都分配一個標籤,可以在作業系統裡通過這個系統認可的標籤來執行任務。這時因為所執行的任務都互相獨立的,加入標籤之後,這些任務在執行時也以不同標籤為標識,這樣就可以根據標籤來進行許可權的判斷,比如使用者建立的檔案可以允許其他人操作,也可以不允許其他人操作。這種標籤通常只是一個整形值,被稱為使用者ID(User ID,簡稱uid)。而使用者ID在許可權上會有一定的共性,可以被組織成群組,比如所有人是一個群組、負責伺服器維護的是一個群組等等。於是可以在使用者ID基礎上進一步得一個群組ID(Group ID,簡稱gid)的概念,用於有組織的共享。每個世界都需要超人,有了多使用者的作業系統,都必須要有一個超人可以求我們於水火,於是在uid/gid體系裡,就把uid與gid都為0的使用者作為超人,可以完成系統內任何操作,這一使用者也一般稱為root。
基於排程器與記憶體空間獨立的設計,使我們得到了安全的多工支援;而基於uid/gid的許可權控制,使我們得到了多使用者支援。支援多使用者與多工的作業系統,則是Unix系統。我們的Linux核心也屬於Unix系統的一種變種。在4、5年前,我們談及程式碼時總是會說Unix/Linux系統的什麼什麼。除了效能上和功能上的細緻差異,Linux與其他Unix系統幾乎沒有區別(當然實現上差異很大)。只不過近年來Linux的表現實在太過威猛,以至於於我們每次不單獨把Linux提出來講,就顯得我們沒有表現出來滔滔江水般的崇拜之情。
等一下,難道我們這是在普及作業系統的基礎知識?
稍安勿躁,我們的這些作業系統知識在Android環境還是很有幫助的。Android系統是借用的Linux核心,於是這個原則性的東西是有效的。而更重要的是,Android的應用程式管理模型,與Unix系統程序模型有極大的相似性,更容易說明問題。
首先,我們的應用程式,不是需要一種安全的執行環境嗎?這時,Linux的程序就提供了良好的支援。假如所有的應用程式,都會以非root的uid許可權執行(這時就應用程式就不會有許可權去破壞系統級功能),又擁有各自獨立的程序空間,還可以通過系統呼叫來進行共享,這時,我們的安全性需求基本上就得到滿足了。當然,這時的Android系統,在構架上跟普通的嵌入式Linux方案沒有任何區別。
然後,我們再來看uid/gid。在傳統的伺服器環境裡,Linux系統裡的uid/gid是一把利器,可以讓成千上萬的使用者登入到上面,共享這臺機器上的服務,因為它本就是伺服器。即便是我們的PC(個人計算機),我們也有可能使用uid來使多個人可以安全地共享這臺機器。但是,放到嵌入式平臺,試想一下,咱們的手機,會不會也有多人共享使用?這是不可能的,手機是個私人性的裝置。於是,uid在手機上便是廢物了。
而傳統Linux執行環境裡的另外一個問題是共享,作為環境,是提供儘可能友好的環境,讓使用者可以共享這臺機器上的資源。比如檔案系統,每個使用者共享機器裡的一切檔案系統,除了唯一個私人目錄環境/home之外,所有使用者都可以共享一切檔案環境。而無論是PC還是伺服器,並不完全控制應用的訪問環境的,比如應用程式都是想上網就上網。這些在一個更私人化的嵌入式執行環境裡則會是災難性地,想像一下您的手機,您下載的應用程式,想上網就上網,想打電話就打電話,所有的隱私資訊,就洩露得一乾二淨了。
在純正Linux環境裡做嵌入式系統,我們就面臨了更精細的許可權控制的問題,需要通過許可權來限制使用系統裡的資源的共享。我們也可以使用很複雜的方案,Linux本身完全是可以提供軍用級安全能力的,有selinux這個幾乎可以控制一切的方案。但這樣的方案實現起來很複雜,而且我們需要考慮到傳統Linux環境是需要一個管理員的,如果我們提供一套智慧手機方案,同時還要為這臺手機配置一個Linux系統管理員,這就太搞笑了。
在Android的設計裡,既然面臨這樣的挑戰,於是系統就採取了一種簡單有效的方案,這種方案就是基於uid/gid的“沙盒”(Sandbox)。既然uid/gid在嵌入式系統裡是個廢物,於是我們可以進行廢物利用。應用程式在安裝到系統裡時,則給每個機程都分配一個獨立的uid,如果有共享的需求,則給需要共享的應用程式分配同樣的gid,應用程式在執行時,我們都使用這種uid/gid來執行應用程式,則可以根據uid/gid來控制應用程式的能力。應用程式被安裝到系統後,它就不再使用完整的根目錄,只給它一個特定目錄作為它的根目錄,每個應用程式的根目錄,不再是系統的/目錄,也是每個應用程式都不一樣的。讓每個應用程式使用不同根目錄環境,技術上倒不復雜,我們Linux上很多交叉編譯工具都是這樣方式執行的,這樣可以讓編譯環境完全與主機環境隔離,不會因為主機環境裡提供的庫檔案或是標頭檔案而造成編譯成功,但無法執行。
同時,當應用程式發生系統級請求時,都會根據uid/gid來決定它有什麼許可權,有怎麼樣的許可權。
這樣的模型就更加符合手機平臺的需求了,應用程式都以獨立的程序執行,這時應用程式無論寫得多糟糕多邪惡,它只能進行自殘,而不能破壞其他應用程式的執行環境。而它們的檔案系統都是互相隔離的,則這種檔案系統上有可能互相破壞的潛在風險也被解決掉了。這時,我們只需要通過一層庫檔案,或是在核心里加一層機制可以讓所有系統功能的使用都需要經過uid/gid進行許可權控制就可以了。
這時,造成的另一個問題是,如果是使用這樣的沙盒模型,則庫檔案與資源,每個程序的私有空間裡都需要一份。另外,加入許可權驗證到庫檔案裡,或是到核心裡,都不是軟體工程上的合理選擇:庫檔案的方式,需要引入一層許可權驗證到每個應用程式,在通用性上會帶來挑戰;而修改Linux核心,則是下下策,核心元件是最不應該改動的,有可能影響核心的正常工作,也可能造成Android對核心的依賴,如果哪天我們不想使用Linux核心了怎麼辦?
假如,我們把Android核心層的功能,抽出來做成一個獨立的程序,這些就迎刃而解了。這個(或是可有多個)核心程序,執行在裝置真實的根目錄(/)環境裡,也會有高於使用者態的許可權。這時,應用程式可以使用一個最小化的根目錄,只需要應用程式執行所需要最基本環境,而對系統級的請求,這些應用程式都可以發出請求到核心程序來完成。這時,我們就可以在核心程序裡進行許可權控制了。
這樣,是不是就完美了?首先這種設計沒有依賴性,如果我們哪天把Linux核心幹掉,換用FreeBSD或是iOS使用的Darwin核心,這種設計也將是有效的(也就是我們有可能在iOS系統裡相容Android應用程式)。而跟我們Unix的程序實現模型作對比的話,是不是有點熟悉?我們可以將應用程式視為程序,核心程序視為核心層,它們之類的通訊方式,則是使用者態發生的一層System Call層。是的,這時,我們相當於在使用者態環境又抽象出一套作業系統層。
但是,這跟我們前面介紹的Android環境好像是對不上號,我們的Android應用程式不是Java寫的嗎?這是安全性設計裡更高一級的方案,Java是一種基於虛擬機器解析執行的環境,也常被稱為託管環境(Hosted),因為需要執行的邏輯並不直接與機器打交道,於是更安全。可惜的是,我們的Android系統從2.3之後,就可以使用一種叫Native Activity的邏輯實體,可以使用C++程式碼來寫應用程式(主要用於編寫遊戲),這會一定程度上影響到這種託管機制帶來的安全性。但問題並不是很嚴重,我們在後面的內容會看到,實際上,Native Activity最終還是需要通過JNI呼叫,才能訪問到Android裡的系統功能,因為這部分是由Java來實現的。
我們再來看看,真實的使用Java的Android系統。
從Java誕生之日起,就給予開發者無限的期望,這種語言天生具備的各種特點,曾在一段時間裡被認為可以取代其他任何程式語言。
l 跨平臺。Java是一種翻譯型語言,則具備了“編寫一次,到處執行”的能力,天生可以跨平臺。
l 純面向物件。Java語言是一種高階語言,是完全面向物件的語言,不能使用指標、機器指令等底層技術,同時還帶自動垃圾回收機制,這樣可以使用程式碼更健壯,程式設計更容易。
l 重用性。由於純面向物件,Java語言的重用性很高,絕大部分原始碼幾乎不需要修改就可以直接使用。由於Java語言的易用性,很多開發的原型系統,都基於Java開發,幾乎所有的設計模式都在Java環境裡先實現。更因為Java的歷史悠久,這種程式設計資源的積累,使它的重用性優勢更加明顯。
l 安全的虛擬機器。Java是基於虛擬機器環境,虛擬機器環境實際上是一種通過程式模擬出來的機器執行環境,更安全。所謂執行的程式碼,只是程式所能理解的一種虛擬碼,而且程式碼程式碼執行的最壞情況,也就僅能破壞虛擬機器環境,完全影響不到執行的實際機器。Java虛擬機器這樣的執行環境,一般被稱為託管(Hosted)程式設計環境,可以進一步將執行程式碼的潛在破壞能力限制到一個程序範圍內,就是像PC上的虛擬機器,再怎麼威猛的病毒,最多也只是破壞了虛擬機器的執行程式,完全影響不到實際的機器,像.Net,Java等都有這樣的加強的健壯性。
l 效能。我們並不總是需要再編譯執行的,上次翻譯出來的程式碼,我們也可以緩衝起來,下次支援呼叫機器程式碼,這樣,Java的執行效率跟實際機器程式碼的效率相關不大。因為虛擬機器是軟體,我們可以在虛擬機器環境裡追蹤程式碼的執行歷史,在這種基礎上,可以更容易進行虛擬機器裡面程式碼的執行狀況分析,甚至加入自動化優化,甚至可以超過真實機器的執行效率。比如,在Java環境裡,我們執行過一次handler.post(msgTouch),當下次通過msgTouch這個Object作為引數來執行handler.post()方法時,我們完全不需要再執行一次,我們已經可以得到其執行的結果,我們只需要把結果操作再做一次。對於一些大運算量而重複性又比較高的程式碼,我們的執行效率會得到成倍地提升。這種技術是執行態的優化技術,叫JIT(Just In Time)優化,在實際機器上是很難實現的,而幾乎所有使用虛擬機器環境的程式語言都支援。
所有的Java語言在程式設計上的優勢,都使它可以成為Android的首選程式設計環境,這跟WebOS選擇JavaScript、WindowsPhone選擇.Net都是同樣的道理。比如安全性,如果我們上面描述的執行模型是純Java的,則其安全性得到進一步提升。
但是,純Java環境不要說在嵌入式平臺上,就是在PC環境裡,也是以緩慢淡定著稱的,Java的自優化能力與處理器能力、記憶體大小成正比。使用純Java寫嵌入式方案,最終達到的結果也就只會是奇慢無比的JAVA ME。另外,Java是使用商業授權的語言,無論是在以前它的建立者Sun公司,還是現在已經收購Sun公司的Oracle,對Java一貫會收取不低的商業授權費用,一旦基於純粹Java環境來構建系統,最後肯定會造成Android系統不再是免費的午餐。既然Android所需要的環境只是Java語言本身,原始的Java虛擬機器的授權又難以免費,這就迫使Android的開發者,開發出來另一套Java虛擬機器環境,也就是我們的Dalvik虛擬機器。
於是,我們基於多程序模型的系統構架,出於跨平臺、安全、程式設計的簡易性等多方面的原因,使我們得到的Android的設計方案成為下面的這個新的樣子:核心程序這部分的實現我們還沒分析到,但應用程式此時在引入Java環境之後,都變成了通過Dalvik虛擬機器所管理起來的更受限的環境,於是更安全。
而在Java環境裡,一個Java程式的主入口實際上還是傳統的main()入口的方式,而以main()方法作為主入口,則意味著程式設計時,特別是圖形介面程式設計,需要使用者更多地考慮如何實現,如何進行互動。整個Java環境,全都是由一個個的有一定生存週期的物件組合而成,任何一個物件裡存在的static屬性的main()方法,則可以在Java環境裡作為一個程式的主入口得到執行。如果使用標準Java程式設計,則我們的圖形介面程式設計將複雜得多,比如我們下面的使用Swing程式設計寫出來的,跟我們前面的Helloworld類似的例子:
importjavax.swing.JFrame;
importjavax.swing.JButton;
importjavax.swing.JOptionPane;
importjava.awt.event.ActionListener;
importjava.awt.event.ActionEvent;
publicclass Swing {
publicstaticvoid main(String[] args) {
JFrame frame = newJFrame("Hello Swing");
JButton button = newJButton("Click Me");
button.addActionListener(newActionListener() {
publicvoidactionPerformed(ActionEvent event) {
JOptionPane.showMessageDialog(null,
String.format("<html>Hello from <b>Java</b><br/>" +
"Button %s pressed", event.getActionCommand()));
}
});
frame.getContentPane().add(button);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}
}
這種方式裡,我們這個Helloworld的例子執行起來,會需要另外有視窗管理器來維護應用程式的狀態。通過main()進入之後,會有很大的實現上的自由度,過大的自由會導致最終編寫出來的應用程式,在程式碼實現角度就已經會是千奇百怪的,同時,也加大了開發者在程式設計上的負擔。
而Android的程式設計上,應該繞開這些Java標準化程式設計上的侷限性,應用程式的行為模式應該被規範化,同時也需要減小應用程式開發時的開銷。於是,我們的Android環境裡,Activity、Service、Content Provider、Broadcast Receiver,不光有共享功能的作用,實際上還起到了減少程式設計工作量的作用。大部分情況下,我們的通用行為,已經被基類所實現,程式設計時只是需要將基類的功能按需求進行拓展。而從完全性的角度,我們也限制了應用程式的自由度,不再是從一個main()方法進入來實現所有的邏輯,而是僅能拓展一些特定的回撥點,比如Activity的onStart(),onStop()等。從應用程式的角度來看,實際上程式設計是這個樣子的:
這種實現的技巧,對於Android系統來說,雖然應用程式會通過自己的classes.dex來實現一些各自不同的功能實現,但對於Android系統來說,這些不同實現都是土頭土腦的一個樣子,可以方便管理。有了這種方便管理之後,實際又能達到靈活的目的,比如我們去修改這種應用程式的管理框架時,應用程式則完全不需要改變,自然而然被管理起來。對於Android的應用程式,則通過這種抽象的工作,大規模地減小了工作量,如果我們很懶不願意浪費時間寫垃圾程式碼,我們都可以借用Android內部已經提供的實現,而所謂的借用實際上是什麼都不用幹,由系統來自動完成。而這些具體的生命週期,我們在後面的內容裡再討論它們的意義。
引入了Java對Android來說,帶來的好處是明顯的,不說可移植性。就是對於安全性設計而言,就已經進一步強化了沙盒式的開發模型。但也還有懸而未決的問題,比如效能,比如說功耗控制。我們先來看Android的效能解決之道。
1.3 跨程序通訊
從Android的“沙盒”模型,我們可以知道,這種多程序模型,必須需要一種某種通過在多個程序之間進行訊息互通的機制。與其他系統不同的是,Android對於這種多程序之間通訊的要求會更高。在Android世界裡,出於功能共享的需求,程序間的互動隨時都在進行,所以必然需要一種效能很高的解決方案。由於Android是使用Java語言作為執行環境的,這樣的情況下,通訊機制就需要跟Java環境結合,可以提供面向物件的訪問方式,並且最好能夠與自動化垃圾回收整合到一起。
出於這些設計上的考慮,Android最終在選擇跨程序通訊方式時使用了一種叫Binder的通訊機制,絕大部分系統的執行,以及系統與應用程式的互動,都是通過Binder通訊機制來完成的。但Android系統裡並不只有Binder,實際上,在與已有的解決方案進行整合的過程中,Android也可能會使用其他的跨程序通訊機制,比如程序管理上會使用Signal,在處理3G Modem時會使用Socket通訊,以及為了使用BlueZ這套比較成熟的藍芽解決方案,也被迫使用Dbus。
任何一種計算環境,都會有實現的功能部件之間進行互動的需求。如果是統一的地址空間,這時解決方式可以簡單粗暴,比如通過讀寫全域性變數等來完成。如果是各部件之間的地址空間互相獨立,這就會是多程序的作業系統環境,我們就需要某種在兩個程序空間之間傳遞訊息的能力,這就是跨程序通訊。一般在多程序作業系統裡,這都被稱為程序間通訊(Inter-Process Communication,IPC)。
在傳統的作業系統環境裡,IPC程序並非必須的選項,只是一種支援多程序環境設計的一種輔助手段。而由於這種非強制性的原因,IPC機制在長期的作業系統發展歷史被約定俗成的固化下來,會包括訊號(Signal)、訊號量(Semaphore)、管道(PIPE)、共享記憶體(Share Memory)、訊息佇列(Message Queue)、套接字(Socket)等。
在這些IPC機制裡,對系統唯一的必須選項是訊號。要是研究過系統的程序排程方向的,都知道訊號不光是程序之間提供簡單訊息傳遞的一種機制,同時也被用於程序管理。比如在Linux的程序管理框架裡,會通過SIGCHLD(Signal No. 20)的訊號來管理父子程序,通過SIGKILL(Signal No. 9)來關閉程序,以及SIGSEGV(Signal No. 11)來觸發段錯誤。所以對於程序管理而言,訊號同時也是一種程序管理機制,像SIGKILL,在核心進行程序排程時會立即處理,不進入到使用者態,也無法進行被遮蔽。既然構建於Linux核心之上,Android必然會使用到訊號。
這些常用IPC機制構造出一種靈活的支援環境,可以讓多程序軟體的設計者可以靈活地選擇。但問題是過於靈活也是毛病,這樣的靈活機制也不能對上層互動訊息作任何假設,只能作為程序間互動的一種最裸的手段,在上層傳輸還需要進行封裝。每個多程序軟體在設計裡會自己設計一套內部通訊的方案,在與其他軟體有互動時再吵架協商出一套通用的互動方案,最後才能組合出一套整個作業系統級別的通訊機制。比如我們的Linux桌面環境裡,Firefox有自己的一套IPC機制、Gnome有自己的一套通過Gconf構建的IPC機制,OpenOffice又有另一套。這些軟體剛開始只關注自己的實現和改進時,這種IPC不統一的缺陷還不是那麼的嚴重,到後來需要協同工作時,由於IPC不統一造成的無法共同協作的問題就進一步嚴重起來,於是又通過一番痛苦的標準化過程,又形成了Linux環境裡統一的IPC機制—Dbus。
前車之鑑,後事之師,既然以前的Linux解決方案會因為IPC機制不統一造成了缺陷,於是就不能重蹈覆轍了。於是Android權衡了設計上的需求,在效能、面向物件、以程序為單位、可以自動進行垃圾回收的多方位的需求,於是選用了一種叫Binder的通訊機制。Binder源自於Palm公司開源出來的一套IPC機制OpenBinder,正如名字上所看到的,Binder比OpenBinder拼寫簡化了一些,也是OpenBinder的一種簡化版。
OpenBinder本用於構建傳統的作業系統BeOS的系統級訊息傳遞機制的,在BeOS退出歷史舞臺之後,又被Palm收購用於Palm的程式設計環境。但對於Android來說,OpenBinder的設計過於複雜,它本質是非常接近微軟的COM通訊機制全功能級IPC,在Binder體系裡,可用於包裝系統裡的一切物件,同時也具備像CORBA那樣的可以繫結到多種語言支援環境裡,甚至它裡面還有shell環境支援!這對Android來講就有點殺雞用牛刀了。於是Android簡化了OpenBinder,只借用其核心驅動部分,上層則重新進行封裝,於是得到我們常說的Binder。從學習角度而言,我們只需要理解與分析Binder,也不應該被一般誤導性的說明去研究OpenBinder,OpenBinder絕大部分功能是在Android裡碰不到的,而Android裡的Binder實現與應用的完整原始碼,總共也沒幾行,分析起來更容易一些。
Binder構建於Linux核心裡的一個叫binder的驅動之上,系統裡所有涉及Binder通訊的部分,都通過與/dev/binder的裝置驅動互動來得到資訊互通的功能。而binder本身只是一種借用記憶體作後端的“偽驅動”,並不對應到硬體,而只是作用於一段記憶體區域。通過這個binder驅動,最終在Android系統裡得到了程序間通訊所需要的特點:
T 高效能:基於Binder的通訊全都使用ioctl來進行通訊,做過實時系統的人都會知道,ioctl則會繞開檔案系統緩衝,達到實時互動的目的。同時,基於Binder傳遞的資料,binder可以靈活地選用不同的機制傳遞資料,兩次複製(用於傳遞小資料量),一次複製(用於ServiceManager),零複製(通過IMemory物件共享,資料只在核心空間裡存在一份,使用者態進行記憶體對映),於是在效率上靈活性都可以很高。
T 面向對象:與傳統的僅提供底層通訊能力的IPC不同,Android系統是一種面向物件式開發的系統,於是需要更高層的具備面向物件的抽象能力的IPC機制。使用底層IPC加以封裝也不是不可以,像Mozilla這種設計也可以解決問題,但Android是作為作業系統開發的,沒必要產生這樣的開銷。而使用Binder之後,就得到了天然的面向物件的IPC,在設計上與實現上都得到了簡化。使用Binder進行通訊異常簡單,只需要通過直接或是間接的方式繼承IBinder基類即可。
T 繫結到Dalvik虛擬機器:一般的系統設計裡使用IPC,都是先將就底層,再設計更面向高層的IPC介面。還拿Mozilla來作例子的話,Mozilla裡的IPC先是提供C/C++程式設計介面,然後再繫結到其他高階語言,像Java、JavaScript、Python。而Android裡的Binder則不同,更多地是面向Java的,直接通過JNI繫結到Java層的Dalvik虛擬機器,先滿足Java層的程式設計需求,然後再考慮到C/C++。使用C/C++語言來對Binder進行程式設計,更像是在Java底層的hack層,無論Binder怎麼被擴充套件,其服務物件都是Java層的,從這個意義上來說,Android的Binder,也是面向Java的。
T 自動垃圾回收:在傳統的IPC機制裡,垃圾回收或者說記憶體回收,都是IPC程式設計框架之外的,IPC本身只負責通訊,並不管理記憶體。而Android裡使用Binder也不一樣,因為它是面向物件式的,於是更容易使用基於物件引用計數的記憶體管理。在使用Binder時,我們可能會經常遇到sp<>,wp<>這樣的模板類,這種模板則是直接服務於垃圾回收的,以Java語言的Soft Reference、Weak Reference來管理物件的生存週期。
T 簡單靈活:與傳統IPC相比,或是標準OpenBinder實現相比,Binder都具備了實現更簡單靈活的特點。Binder在訊息傳遞機制之上,附加了一定的記憶體管理功能,大大簡化了程式設計,同時在實現上又非常簡單,大家可以看到frameworks/base/libs/binder下的實現,也沒有幾行程式碼,而對於驅動來說,也僅有一個drivers/stage/android/binder.c一個檔案而已。這種簡單實現也給上層使用上提供了更多靈活性。
T 面向程序:除了Signal之外,傳統IPC機制幾乎沒有辦法獲取訪問者(也就是程序)相關的資訊,而只以核心態的檔案描述符為單位進行通訊。但Binder在設計裡,就是以程序為單位的,所有的資訊在傳遞過程裡都以PID作為分發的基礎,這時也為Android的多程序模型提供了便捷性。在維護程序之間通訊狀態時,Binder底層本身是可以得到程序是否已經出錯掛掉等資訊。Binder這種特性也間接提供了一定的程序排程的能力,處於Binder通訊過程裡的程序,在沒有Binder通訊發生時,實際一直會處於休眠狀態,並不會盲目執行。
T 安全:我們前面分析過基於程序空間的“沙盒”模型,它是基於uid/gid為基礎的單程序模型。如果我們的Binder提供的傳遞機制也是以程序為單位進行通訊,這時這種程序間通訊的模型也可以得以強化,程序執行時的uid/gid也會在Binder通訊時被用於許可權判斷。
當然,Android系統並非依賴Binder處理全部邏輯,在特殊性況下也會使用到其他的IPC機制,比如我們前面提到的RIL與BlueZ。在與程序管理模型相適配時,Android也會使用到訊號,關閉應用程式是通過簡單的SIGKILL,還會在某些程式碼除錯部分的實現裡使用SIGUSR1、SIGUSR2等。但對於Android整個系統的核心實現而言,都會通過Binder來組織系統內部的訊息傳遞與處理。應用程式之間的萬能資訊Intent,實際上底層是使用的Binder,而應用程式要訪問到系統裡的功能,則會是使用Binder封裝出來的Remote Service。整個系統的基本互動圖如下所示:
在binder之上,framework裡會把binder通訊的基本操作封裝到libbinder庫(frameworks/base/libs/binder)實現裡,當然libbinder則會通過JNI繫結到Dalvik虛擬機器的執行環境。應用程式App1與App2之間進行通訊,只會通過Binder之上再包裝出來的Intent來完成具體的互動。同時,在Android的系統實現的,我們提供的系統級功能,大部分會是Java實現的Remote Service,有一小部分是使用的C/C++實現的Native Service,而所有的這些Service,會通過一個叫ServiceManager的程序來進行管理,不論是Java實現的Service還是NativeService,都將使用addService()註冊到ServiceManager裡。當任何一個應用程式需要訪問系統級功能時,由會通過呼叫ServiceManager的getService方法取回一個系統級Service的例項,然後再與這些Service進行互動。
圖中我們可以看到,實線代表互動,虛線代表基於Binder的遠端互動,從上圖中我們也可以看出,實際上,系統裡基本上都不會有多個功能實現間的直接呼叫,所有的可執行部分,都只是通過libbinder封裝庫來與binder裝置進行通訊,而通訊的結果就是實現了虛線所示的跨程序間的呼叫。當然,在程式碼裡是看出來這些貓膩的,我們程式碼上可能大部分都貌似是直接呼叫的,比如WIFI訪問:
mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
intwifiApState = mWifiManager.getWifiApState();
if (isChecked&& ((wifiApState == WifiManager.WIFI_AP_STATE_ENABLING) ||
(wifiApState == WifiManager.WIFI_AP_STATE_ENABLED))) {
mWifiManager.setWifiApEnabled(null, false);
}
我們在程式碼呼叫上根本看不到有Binder存在,就像是在當前程序裡呼叫了某個方法一樣。在這程式碼示例裡,我們只是通過getSystemService()取回一個Context.WIFI_SERVICE的例項,然後就通過這一例項來訪問gitWifiApState()方法,但在底層實現上,getSystemService()與getWifiApState()都是執行另一個程序空間裡的程式碼。這就是Android在實現上的厲害之處,雖然是簡單的封裝,但使我們的程式碼具備了強大跨程序的功能。
而這些所謂的Service,跟我們在應用程式程式設計裡看到的Service基本上是一樣的,唯一的區別是會通過addService()載入到ServiceManager的管理框架裡,於是所有的程序都可以共享這種Service。在addService()成功之後,應用程式或是系統的其他部分,不需要再通過Intent來呼叫Service,而是可以直接通過getService()取回被呼叫的例項,然後直接進行跨程序呼叫。當然,Service概念的引入也給系統設計帶來了方便,這些Service,即可以以獨立程序的方式執行,也可以以執行緒方式被託管到另一個程序。在Android世界裡,這樣的技巧被反覆使用,一般情況下,都會將系統級的Service以執行緒方式執行在SystemServer這個系統程序裡,但如果有功能上或是穩定性上的考慮,則有可能以獨立的程序來執行Service,比如MediaServer、StageFlinger就都是這樣的例子。但對於呼叫它們的上層來說,都是透明的,無論哪種方式都沒有區別。
對於我們應用程式,我們最關心的可能還是Intent。其實,如何回到前面我們提及過的onSavedInstance這種序列化物件,我們就可以瞭解到Intent這種神奇機制是如何實現的了。我們前面說明了,其實onSavedInstance的型別是Bundle,是一個用於恢復Activity上下文現場的用於序列化處理的特殊物件,而這種所謂序列化,就是將其轉換成字典型別的結構,以key對應value的方式進行儲存與讀取。Bundle類可以處理一些Java原始支援的資料型別,像String,Int等,可以使只使用這些資料型別的物件可以被序列化(字典化),於是可以將這樣的Bundle物件儲存起來或是在跨程序間傳遞。同時,Bundle裡可以把另一個Bundle物件作為其屬性變數,於是通過Bundle實際上可以描述所有物件,而且是以一種程序無關機器無關的狀態描述,這種結構天生就可以跨程序。
像我們的這例子裡,我們可以通過Bundle來描述Bundle_Master物件,但因為Bundle_Master裡又包含另一個物件,於是使用Bundle_Key4來描述。這些在程式設計上則不是直接使用Bundle物件進行程序間的資料傳遞,因為需要使用Binder來傳遞物件,於是我們又會得到一個Parcel類,把字典結構的Bundle物件,轉換成基於Binder IPC進位制的Parcel物件。Parcel物件,實際上也沒有什麼特殊性,只是提供一種跨程序互動時的簡便性罷了,它內部使用一個Bundle來儲存中間結果,同時會通過Binder來標識訪問的活躍狀態,然後提供writeToParcel()與readFromParcel()兩個讀寫的介面方法也提供中間資料的讀寫。在使用Parcel進行封裝時,可以通過實現Parcelable介面來實現。最後得到的Parcel物件如下所示:
如上所示,實現了一個Parcel介面之後,我們得到的某個Parcelable的類,內部除了自己定義的屬性與方法之外,還需要提供一個static final的CREATOR物件。Static final表明,所有這些Parcelable的物件(比如我們例子裡的Intent),都會共享同一CREATOR物件來初始化物件,或是物件的陣列。此時便提供了物件初始化方法的跨程序性。進一步需要提供兩個介面方法,readFromParcel()與writeToParcel()兩個方法,則此時物件就可以通過這兩個介面來進行不同上下文環境裡的讀寫,也就是物件本身可被“自動”序列化了。當然,對於Bundle內儲存的中間資料,有可能也需要單個的讀寫,所以也會提供readValue()與writeValue()方法來進行內部屬性的讀寫,嚴格地說是Getter/Setter。我們這裡是以Intent為例子說明的,這我們就可以看到Intent的起源,其實Intent也只是通過Binder來拓展出來的Bundle序列化物件而已。
在我們Intent物件裡包含的資訊裡,絕大部分是Java的基本型別,比如Action,Data都是String,於是大部分情況下我們都是直接Parcelable的介面操作即可完成共享。而唯一需要進一步使用Parcel的,是Extras屬性,通過Extras可以傳遞極其複雜的物件為引數,於是Extras便成為Intent的屬性裡唯一會被Parcel進一步包裝的部分。
通過Binder可以很高效而且安全實現資料的傳遞,於是我們整個Android世界便毫無顧忌地使用Intent在多程序的環境裡執行,Intent的傳送與處理,實際上一直處理多程序的互動環境裡,使用者本質上並沒有程序內與跨程序的概念。
對於應用程式之間是如此,對於應用程式與系統之間的互動也是如此,我們也需要達到一種簡單高效,同時讓應用程式看不到本地與遠端變化的效果。Android對於Java環境裡的系統級程式設計,也提供了良好的封裝,使Java環境裡編寫系統級服務也很簡單。
使用Binder之後,所有的跨程序的訪問,都可以通過一個Binder物件為介面,遮蔽掉呼叫端與實現端的細節:
比如,我們這個應用環境裡,程序1訪問程序2的ClassOther時,我們並非直接訪問另一程序的物件,而在程序1的地址空間裡,會有一個objectRemote的遠端引用,這一遠端引用通過一個IBinder介面會引用到程序2的ClassRemote的例項。當在程序1裡訪問程序2的某個方法時,則直接會通過這一引用呼叫其ClassRemote裡實現的具體的方法。
這種命名方式跟我們的Android對不上號,這是因為Android裡使用了一種叫Proxy的設計模式。Proxy設計模式,可以將設計與實現分離開。先定義介面類,呼叫端會通過一個Proxy介面來進行呼叫,而實現端則只通過介面類的定義來向外暴露其實現。
如圖所示,呼叫方會通過介面找到Proxy,而通過Proxy才會具體地找到合適的Stub實現來進行通訊。通過這樣的抽象,可以使程式碼的互相合作時的耦合度被大大降低,Proxy實現部分可以根據呼叫的具體需要來向不同的實現發出呼叫請求,同時實現介面的部分也會因此而靈活,對同一介面可以有多個實現,也可以對介面之外的呼叫進行靈活地拓充。
對應到Android環境,我們就需要能夠將Binder的實現,進一步通過Proxy設計模式包裝起來,從而能夠提高實現上的靈活性。於是Android裡應用程式與系統的互動模型,就會變成這樣:
首先繼承自Binder的,不再是某一個物件,而是被拆分成Proxy與Stub兩個部分,Proxy部分被重新命名為xxxManager(與現實生活相符,我們總是不直接找某個人,而是找他的經驗來解決問題),而Stub部分則是通過繼承自Binder來得到一個Stub物件,這個Stub物件會作為一個xxxService的屬性,從而對於某些功能xxx的存在週期,將通過Service的生命週期來進行管理。通過這樣的程式碼重構的Proxy訪問模式,使我們的系統的靈活性得以大大提高,這也是為什麼我們在系統裡可以見到大量的xxxManager.java,xxxService.java的原因,Manager供應用程式使用,屬於Proxy執行在應用程式程序空間,Service提供實現,一般執行在某個系統程序空間裡。
通過這種Proxy方式簡化之後,可能還是會有個程式碼重複性的問題,比如我們的Manager怎麼寫、Service怎麼寫,總是會不停地重複寫這些互動的程式碼,但對於通訊過程而言,本質上從Manager的資訊,怎麼到Service端,基本上是比較固化的。於是,Android裡提供了另一套機制,來簡化設計,減小需要重複的程式碼量。
AIDL跟傳統的IDL有類似之處,但區別也很大,更加簡單,只用於Java環境,不能用於網路環境裡。AIDL提供的功能,就是將這些重複性的通訊程式碼(Proxy與Stub的基本實現),固化到AIDL工具自動生成的程式碼裡,這部分程式碼使用者看不到,也不用去改寫它。
這時,在Android裡,寫一個系統級的Service,就不再有明顯的Proxy類、Service類,而變成呼叫端直接呼叫,實現端只提供Stub的具體實現即可。當然,我們系統環境裡的Proxy介面會複雜一些,需要考慮許可權、共享、電源控制等多種需求,所以我們還是可以見到大量的Manager類的實現。
我們在Android系統程式設計裡研究得深入一點,也會發現Remote Service,也就是我們AIDL機制的妙用,很簡單地就可以提供一些方法或是屬性給另一個程序。Android系統,其實說白了,也就是大量這樣的基於AIDL的Remote Service的實現。
當我們遇到效能問題時,我們還可以在Binder之上,“黑”掉正常的Java程式碼,全部使用C/C++來響應應用程式的請求,這就是所謂的Native Service。當然,我們就不能再使用AIDL了,AIDL是專用於Java環境裡跨程序呼叫所用的,必須自己手動來實現所有的Proxy介面與Stub介面。從這個意義上來說,Android也是Java作業系統,因為使用C/C++反而是比較受限的程式設計環境。
於是,我們的跨程序通訊的問題,幾乎可以得到完美解決了。可以再強調一下的是,在Android世界裡,如果是應用程式程式設計,可能只會與Activity打交道,因為大部分情況下,我們只是把介面畫出來進行互動。而學習Android底層,需要做Android移植,甚至需要進行Android定製化的改進,對Binder、Remote Service、Native Service的深入理解與靈活使用則是關鍵點。不瞭解這些特點的Android系統工程,都將在效能、記憶體使用上遇到難以避免的麻煩,因為系統本身都依賴於這種機制而存在。由於這樣的重要性,我們在後面會專門以這三個主題展開分析,說明在實踐運用時,我們可以怎麼靈活使用這三種功能部件。