Android應用程式開發以及背後的設計思想深度剖析(5)

weixin_34337265發表於2016-09-23

轉載於  http://www.uml.org.cn/mobiledev/201211063.asp#2

功耗控制

在嵌入式領域,功耗與運算量幾乎成正比。作業系統裡所需要的功能越來越複雜、安全性需求越來越高,則會需要更強大的處理能力支援。像在老式的實時作業系統裡,沒有程式概念,不需要虛擬記憶體支援,這時即便是寫一些簡單應用,所需要的運算量、記憶體都非常小,而一旦換用支援虛擬記憶體的系統,則所需要的硬體處理能力、電量都會成倍上漲,像一些功能性手機平臺,可以成為一臺不錯的手機,但執行起一個Linux作業系統都很困難。而隨著作業系統能力增強,則所能支援的硬體又得以提升,可以使用更大的螢幕、使用更大量記憶體、支援更多的無線晶片,這些功能增強的同時,也進一步加劇了電量的消耗。雖然現在晶片技術不斷提高生產工藝降低製程(就是晶片內部燒寫邏輯時的閘電路尺寸),幾乎都已經接近了物理上的極限(40奈米、28奈米、22奈米),但是出於設計更復雜晶片為目的的,隨著雙核、四核、以及越來越高的工作頻率,事實上,功耗問題不但沒有降低,反而進一步被加劇了。

面對這樣越來越大的功耗上的挑戰,Android在設計上,必須在考慮其他設計因素之前,更關注功耗控制問題。Android在設計上的一些特點,使系統所需要的功耗要高於傳統設計:Android是使用Java語言執行環境的,所有在虛擬機器之上執行的程式碼都需要更大的運算量,使用機器程式碼中需要一條指令的地方,在虛擬機器環境下執行則可能需要十幾條指令;與其他偽多工不同,Android是真實多工的,多工則意味著在同一時刻會有更多工在執行;Android是構建上Linux核心之上的系統,Linux核心在效能上表現奇佳,在功耗處理上則是短板,就拿PC環境來說,Linux的桌面環境在功耗控制上從來不如其他作業系統,MacOS或是Windows。

當然,有時沒有歷史包袱,也未必就是壞事,比如Linux核心在功耗管理上做得還不夠好,於是就不會在Linux核心環境裡死磕,Android可以通過新的設計來進行功耗控制上的提升。出於跟前面我們所說過的可減小對Linux核心依賴性、加強系統可移植性的設計需求,於是不可避免的,功耗控制將會盡可能多地被推到系統的上層。在我們前面對於安全性的分層中可以看到,Android相當於把整個作業系統都在使用者態重新設計了一次,SystemServer這個系統級程式相當於使用者態的一個Linux核心,於是將功耗控制更多地抽到使用者態來執行,也沒有什麼不合理的。

在Android的整體系統設計裡,功耗控制會先從應用程式著手,通過多工並行時減小不必要的開銷開始;在整個系統構架裡,唯一知道當前系統對功耗需求的是SystemServer,於是可以通過相應的安全介面,將功耗的控制提取出來,可由SystemServer來進行後續的處理。Android系統所面臨的執行環境需求裡,電源是極度有限的資源,於是功耗控制應該是暴力型的,儘可能有能力關閉不需要使用的電源輸出。當然暴力關電,則可能引起某些外設晶片不正常工作,於是在晶片驅動裡需要做小範圍修改。與其他功能部分的設計不同,既然我們功耗控制是通過與驅動打交道來實現,可能無法避免地需要驅動,但要讓修改儘可能小,以提供可移植性。

在這種修改方案裡,最需要解決的當然首先是多工處理。我們可以得到的就是我們的生命週期。所謂的生命週期,是不是僅僅只是提供更多一些程式設計上的回撥介面而已呢?不僅如此,我們的所謂生命週期是一種休眠狀態點,更多地起到休眠操作時我們有機會插入程式碼的作用。如果僅是提供程式設計功能,我們可以參考JAVA ME裡對於應用程式實現:

3064872-429588fc0cf8dfd6.png

JAVA ME框架裡對待應用程式只有三個狀態點,執行、暫停、關閉,對應提供三種回撥介面就可以驅動起這種程式設計模型。但我們的Android不是這樣處理的,Android在程式設計模型上,把帶顯示與不帶顯示的程式碼邏輯分別抽象成Activity與Service,每種不同邏輯實現都有其獨特的生命週期,以更好地融入到系統的電源管理框架裡。

像我們的與顯示相關的處理,Activity,它擁有6種不同狀態:

3064872-8451c9199ab2abfd.png

它的不同生命週期階段,取決於這一Activity是否處於互動狀態,是否處理可見狀態。如果加入這兩個限制條件,於是Activity的生命週期則是為這兩種狀態而設計的。onResume()與onResume()分別是進入互動與退出互動時的狀態點,在onResume()執行完之後,這時系統進入了互動狀態,也就是Activity的Running狀態,而此時如果由於Activity發生呼叫或是另一個Activity主動執行,彈出一個小對話方塊,使原來處於Running狀態的Activity被擋住,這時Activity就被視為不需要互動了,這時Activity進入不可見互狀態,觸發onPause()回撥。onStart()與onStop()則是對應於是否可見,在onStart()回撥之後,應用程式這裡就可以被顯示出來,但不會真正進入互動期,當Activity變得完全不可見之後,則會觸發onStop()。而Android的多工實現,還會造成程式會被殺死掉,於是也提供兩個onCreate()與onDestroy()兩種回撥方法來提供程式被建立之後與程式被殺死之前的兩種不同操作。

3064872-a828e5c32d041d99.png

這種設計的技巧在於,當Activity處於可互動狀況時,這是系統裡的全馬力執行的週期。而再向外走一個狀態期,只是處於可見但不可互動狀態時,我們就可以開始通過技巧降功耗了,比如此時介面不再重新整理、可以關閉一些所有與使用者互動相關的硬體。當Activity再進一步退出可見狀態時,可以進一步退出所有硬體裝置的使用,這時就可以全關電了。編寫應用程式時,當我們希望它有不一樣的表現時,我們可以去通過IoC去靈活地覆蓋並改進這些回撥介面,而假如這種標準的模型滿足我們的需求,我們就什麼都不需要用,自動地被這種框架所管理起來。

當然,這種模型也不符合所有的需求,比如對於很多應用程式來說,在後臺不可見狀態下,仍然需要做一些特定的操作。於是Android的應用程式模型裡,又增加了一個Service。對於一些暴力派的開發者,比較喜歡使用後臺執行緒來實現這種需求,但這種實現在Android並不科學,因為只通過Activity承載的後臺執行緒,有可能會被殺死掉,在有狀態更新需求時,後臺執行緒需要通過Activity重繪介面,實際上這樣也會破壞Android在功耗控制上的這種合理性設計。比較合適的做法,所有不帶介面、需要在後臺持續進行某些操作的實現,都需要使用Service來實現,而狀態顯示的改變應該是在onStart()裡完成的,狀態上的互動則需要放到onResume()方法裡,這樣的實現可以有效繞開程式被殺死的問題。並且在我們後面介紹AIDL的部分,還可以看到,這樣實現還可以加強後臺任務的可互動性,當我們進一步將Service通過AIDL轉換成Remote Service之後,則我們的實現會具備強大的可複用性,多個程式都可以訪問到。

Service也會有其生存週期,但Service的生存週期相對而言要簡單得多,因為它的生存週期只存在“是否正在被使用”的區別。當然,同樣出於Android的多工設計,“使用中”這個狀態之外,也會有程式是否存在的狀態。

3064872-e0ceb6d895bb9b83.png

於是,我們的Service也可被納入到這種程式碼活躍狀態的受控環境,當是不需要與後臺的Service發生互動,這時,我們可能只是通過一個startService()發出Intent,這時Service在執行完相應的處理請求則直接退出。而如果是一個AIDL方式丟擲的Remote Service,或是自己程式範圍內的Service,但使用bindService()進行了互動,這時,Service的執行狀態,只處於onBind()與OnUnbind()回撥方法之間。

當我們的應用程式的各種不同執行邏輯,都是處於一個可控狀態下時,這時,我們的功耗控制就可以被集中到一個系統程式的SystemServer來完成。這時,我們面臨一種設計上的選擇,是預設提供一種鬆散的電源控制,讓應用程式儘可能多自由地控制電源使用,還是提供一種嚴格邏輯,預設情況下實施嚴格的電源輸出管理,只允許應用程式出於特殊的需求來調高它的需求?當然,前一種方式靈活,但出於電源的有限性,這時Android系統裡使用了第二次邏輯,儘可能多地嚴格電源輸出控制。

在預設情況下,Android會嘗試讓系統儘可能多地進入到休眠狀態之中。在從使用者開始進行了最後一次互動之後,系統則會觸發一個計時器,計時器會在一定的時間間隔後超時,但每次使用者的互動操作都會重置這一計時器。如果使用者一直沒有進行第二次互動,計時器超時則觸發一些功耗控制的操作。比如第一步,會先變暗直至關閉系統的螢幕,如果在後續的一定時間內使用者繼續沒有任何操作,這時系統則會進一步嘗試將整個系統變成休眠狀態。

休眠部分的操作,基本上是Linux核心的功耗控制邏輯了。休眠操作的最後,會將記憶體控制器設成自重新整理模式,關掉CPU。到這種低功耗執行模式之下,這時系統功耗會降到最低,如果是不帶3G模組的晶片,待機電流應該處於1mA以下。但我們的系統是手機,一般2G、3G、或是4G是必須存在的,而且待機狀態時關掉這種不同網路制式下的Modem,也失去了手機存在的意義,於是,一般功耗上會加上一個移動Modem,專業術語是基帶(Baseband)的功耗,這時一般要控制在10 – 30mA的待機電流,100mW左右的待機功耗。如果這時,使用者按些某些用於喚醒的按鍵、或是基帶晶片上過來了一些簡訊或是電話之類的資訊,則系統會通過喚醒操作,回到休眠之前的狀態。

但是Linux核心的Suspend與Resume方案,是針對ACPI裡通用計算環境(我們的PC、筆記本、伺服器)的功耗控制方案,並不完全與手機的使用需求相符合。而Linux核心所缺失的,主要是UI控制上功耗管理,手機平臺上耗電最大的元器件,是螢幕與背光,是無法通過Linux核心的suspend/resume兩級模型來實現高效的電源管理。於是,Android系統,在原始的suspend與resume介面之外,再增加了兩級early_suspend與late_resume,用於UI互動時的提前休眠。

3064872-0c7e2e3ef899d985.png

我們的Android系統,在出現使用者操作超時的情況下,會先進入early_suspend狀態點,關閉一些UI互動相關的硬體裝置,比如螢幕、背光、觸控式螢幕、Sensor、攝像頭等。然後,在進一步沒有相應喚醒操作時,會進入suspend關閉系統裡的其他類的硬體。最後系統進入到記憶體自重新整理、CPU關電的狀態。如果在系統完全休眠的情況下,發生了某種喚醒事件,比如電話打進來、簡訊、或是使用者按了電源鍵,這時就會先進resume,將與UI互動不相關的硬體喚醒,再進入late_resume喚醒與UI互動相關的硬體。但如果裝置在進入early_suspend狀態但還沒有開始suspend操作之前發生了喚醒事件,這時就直接會走到late_resume,喚醒UI互動的硬體驅動,從而使用者又可以看到螢幕上的顯示,並且可以進行互動操作。

經過了這樣的修改,在沒有使用者操作的情況下,系統會不斷進入休眠模式省電,而使用者並不會感受到這種變化,在頻繁操作時,實際上休眠與喚醒只是快進快出的UI相關硬體的休眠與喚醒。但完全暴力型的休眠也會存在問題,比如我們有些應用程式,QQ需要保持登入,下載需要一直在後臺下載,這些都不符合Android的需求的,於是,我們還需要一種機制,讓某些特殊的應用程式,在萬不得已的情況下,我們還是可以得這些應用程式足夠的供電執行得下去。

於是Android在設計上,又提出了一套創新框架,wake_lock,在多加了early_suspend與late_resume之外,再加上可以提供功耗上的特殊控制。Wake_lock這套機制,跟我們C++裡使用的智慧指標(Smart pointer),借用智慧指標的思想來設計電源的使用和分配。我們也知道Smart Pointer都是引用,則它的引用計數會自動加1,取消引用則引用計數減1,使用了智慧指標的物件,當它的引用計數為0時,則該物件會被回收掉。同樣,我們的wake_lock也保持使用計數,只不過這種“智慧指標”的所使用的資源不再是記憶體,而是電量。應用程式會通過特定的WakeLock去訪問硬體,然後硬體會根據引用計數是否為0來決定是不是需要關閉這一硬體的供電。

Suspend與wake_lock這兩種新加入的機制,最後也是需要加放SystemServer這個程式裡,因為這是屬於系統級的服務,需要特權才能保證“沙盒”機制。於是,我們得到了Android裡的電源管理框架:

3064872-0093f24e93fecc4d.png

當然,這裡唯一不太好的地方,就是Android系統設計必須對Linux核心原有的電源管理機制進行改動,需要加入wake_lock機制的處理,也需要在原始的核心驅動之上加入新的early_suspend與late_resume兩個新的電源管理級別與wake_lock相配套。這部分的程式碼,則會造成Android系統所需要的驅動,與標準Linux核心的驅動並不完全匹配,同時這種簡單粗暴的方式,也會破壞掉核心原有的清晰簡要的風格。這方面也造成了Linux社群與Android社群之間曾一度吵得很凶,Linux核心拒絕Android提交的修改,而Android原始碼則不再使用標準的Linux核心原始碼,使用自己特殊的分支進行開發。

我們再來看Android系統對於功能介面的設計。

1.6 功能介面設計

我們實現一個系統,必須儘可能多地提供給應用程式儘可能多的開發介面,作為一個開源系統更應該如此。雖然我們前面提到了,我們需要有許可權控制機制來限制應用程式可訪問系統功能與硬體功能,但是這是許可權控制的角度,如果應用程式得到了授權,應該有理由來使用這一功能,一個能夠獲得所有許可權的應用程式,則理所當然應該享受系統裡所提供的一切功能。

對於一個標準的Java系統,無論是桌面環境裡使用的Java SE還是嵌入式環境裡使用的Java ME,都不存在任何問題,因為這時Java本就只是系統的一層“皮”,每個Java寫成的應用程式,只是一層底層系統上的二次封裝,實際上都是借用底層作業系統來完成訪問請求的。對於傳統的應用程式,一個main()進入死迴圈處理UI,也不存在這個問題,通過連結到系統裡的動態連結庫或是直接訪問裝置檔案,也可以實現。但這樣的方式,到了Android系統裡,就會面臨一個功能介面的插分問題。因為我們的Android,不再是一層作業系統之上的Java虛擬機器封裝,而是抽象出來的在使用者態運轉的作業系統,同時還會有“沙盒”模式,應用程式並不見得擁有所有許可權來訪問系統資源,則又不能影響它的正常執行。

於是,對於Android在功能介面設計上,會被劃分成兩個層次的,一種是以“受託管”環境下通過一個系統程式SystemServer來執行,另一種是被對映到應用程式的程式空間內來完成。而我們前面分析的使用Java程式語言,而Framework層功能只以API方式向上提供訪問介面,就變得非常有遠見。使用了Java語言,則我們更容易實現程式碼結構上的重構,如果我們的功耗介面有變動,則可以通過訪問介面的重構來隱藏掉這樣的差異性;只以Framework的API版本為標準來支援應用程式,則進一步提供封裝,在絕大部分情況下,雖然我們底層結構已經發生了巨大變動,應用程式卻完全不受影響,也不會知道有這樣的變化。

從這種設計思路,我們再去看Android的程式模型,我們就可以看到,通常意義上的Framework,實際上被拆分成兩部分:一部分被應用程式用Java實現的classes.dex所引用,這部分用來提供應用程式執行所必須的功能;另一部分,則是由我們的SystemServer程式來提供。

3064872-232db1ce0b91f536.png

在應用程式只需要完成基本的功能,比如只是使用Activity來處理圖形互動時,通過Activity來構建方便使用者使用的一些功能時,這時會通過自己程式空間內對映的功能來完成。而如果要使用一些特殊功能,像打電話、發簡訊,則需要通過一種跨程式通訊,將請求提交到SystemServer來完成。

這種由於特殊設計而得到的執行模型很重要,也是Android系統有別於其他系統很重要的一個區別。這樣的框架設計,使Android與傳統Linux上所面臨的易用性問題在設計角度就更容易解決。

比如顯示處理。我們傳統的嵌入式環境裡,要不就是簡單的Framebuffer直接執行,要麼會針對通用性使用一個DirectFB的顯示處理方案,但這種方案通用性很低,安全性極差。為了達到安全性,同時又能儘可能相容傳統桌面環境下的應用程式,大都會傳承桌面環境裡的一個Xorg的顯示系統,比如Meego,以及Meego的前身Maemo,都是使用了Xorg用來處理圖形。但Xorg有個很嚴重的效能問題:

3064872-cf092dda394f3f07.png

使用Xorg處理顯示的,所有的應用程式實際上只是一個客戶端,通過Unix Socket,使用一種與傳統相容的X11的網路協議。使用者互動,應用程式會在自己的互動迴圈裡,通過X11發起建立視窗的請求,之後的互動,則會通過輸入裝置讀取輸入事件,再通過Xorg伺服器,轉回客戶端,而應用程式介面上的重繪操作,則還是會通過X11協議,走回到Xorg Server之後,再進行最後的繪製與輸出。雖然現在我們使用的經過模組化重新設計的XorgR7.7,已經儘可能通過硬體加速來完成這種操作,Xorg伺服器還是有可能會成為整個圖形互動的瓶頸,更重要的是複雜度太高,在這種構架裡修改一個bug都有點困難,更不要說改進。在嵌入式平臺上更是如此,效能本就不夠的系統環境,Xorg的缺陷暴露無移,比如使用Xorg的Meego更新過程遠比Android要困難,使用者互動體驗也比較差。

在Android裡,處理模型則跟傳統的Xorg構架很不一樣。從設計角度來講,繪製圖形介面與獲取輸入裝置過來的輸入事件,本來不需要像Xorg那樣的中控伺服器,尤其像Android執行環境這樣,並不存在多視窗問題(多視窗的系統需要有個伺服器決定哪個視窗處於前臺,哪個視窗處於互動狀態中)。而從實現的角度,如果能夠提供一種設計,將圖形處理與最終輸出分開,則更容易實現優化處理。基於圖形介面的互動,實際上將由三個不同的功能實體來完成:應用程式、負責將圖層進行疊加渲染的SurfaceFlinger、以及負責輸入事件管理和選擇合適的地址進行傳送的SystemServer。當然,我們的上層的應用程式不會看到內部的複雜邏輯,它只知道通過android.view這個包來訪問所有的圖形互動功能。

於是得到Android系統的圖形處理框架:

3064872-83a8c4ae666f88ea.png

我們的SurfaceFlinger,是Android裡的一種Native Service的實現,所以有原理上來說,只要有一個承載它的執行體(程式、執行緒皆可),就可以在系統裡執行。在實現過程裡,SurfaceFlinger作為一個執行緒在SystemServer這個程式空間裡完成也是可以的,只是出於穩定性的考慮,一般將它獨立成一個單獨的SurfaceFlinger的獨立程式。

這種設計,可以達到一個低耦合設計的優勢,這套圖形處理框架將變得更簡單,同時也不會將Xorg那樣需要大量的特殊核心介面與其適配,如果在別的作業系統核心之上進行移植,也不會有太大的依賴性。但這時會帶來嚴重的效能問題,因為圖層的處理和輸出是需要大量記憶體的(如果是24位真彩色輸出,即使是800x480的分辯率,每秒60楨的輸出頻率,也需要3*800*480*60 = 69120000,69M Byte/s),這種開銷對於嵌入式方案而言,是難以承受的。在程式間傳遞資料時,會先需要在一個程式執行上下文環境裡通過copy_from_user()把資料從使用者態拷貝到核心態,然後在另一個程式執行的上下文環境裡通過copy_to_user()把資料拷貝從核心態拷貝到另一個使用者態環境,這樣才能保證互不干擾。

而回過頭來看Linux核心,搞過Linux核心態開發的都知道,在Linux系統的程式之間減小記憶體拷貝的開銷,最直接的手段就是通過mmap()來完成記憶體對映,讓儲存資料的記憶體頁只會在核心態裡迴圈,這時就沒有記憶體拷拷貝的開銷了。使用了mmap()之後,記憶體頁是直接在核心態分配的記憶體,兩個程式都通過mmap()把這段區域對映到自己的使用者空間,然後可以一個程式直接操作記憶體,另一個程式就可以直接訪問到。在圖層處理上,最好這些在核心態申請的記憶體是連續記憶體,這時就可以直接通過LCD控制器的DMA直接輸出,Android於是提供了一種新的特殊驅動pmem,用來處理連續實體記憶體的分配與管理。同時,這種方式很裸,最好還在上層提供一次抽象,程式設計時則靈活度會更高,針對這種需求,就有了我們的Gralloc的HAL介面。加入了這兩種介面之後,Android在影像處理上便自成體系,不再受限於傳統實現了。

3064872-15b60c08b4118fd5.png

我們的圖層,是由應用程式在建立是通過Gralloc來申請圖層儲存空間,然後被包裝成上層的Surface類,在Activity實現裡Surface則是按需要進行重繪(呼叫view的draw()方法),並在繪製完成後通過post()將繪製完成的訊息傳送給SurfaceComposer遠端物件。而在SurfaceFlinger這段,則是將已經繪製完成的Surface通過其對應的模式,進行圖層的合成並輸出到螢幕。對於上層實現,貌似是一種很鬆散的互動,而對於底層實現,實際則是一種很高效的流水線操作。

這裡,值得一提的是Surface本身也包含了圖層處理加速的另一種技巧,就是double buffer技術。一個Surface會有兩個圖層buffer,一楨在後臺被繪製,另一楨在前臺進行輸出。當後臺繪製完成後,會通過一次Page Flipping操作,原來的後臺楨被換到前臺進行輸出,而繪製操作則繼續在後臺完成。這樣使用者總會看到繪製完整的影像,因為圖層總是繪製完成後才能輸出。而有了double buffer,使我們圖形輸出的效能也得到提升,我們輸出繪製與輸出使用獨立的迴圈,通過流水線加快了圖層處理,尤其在Android裡,可能有多個繪製的邏輯部分,效能得以進一步加速。在Android 4.1裡面,這種圖形處理得以進一步優化,使用了triple buffer(三重緩衝),加深了圖層處理的流水線操作能力。

這種顯示處理上的靈活性,在Android系統裡也具備非常重要的意義,可以讓整個系統在功能設計上可以變得更加靈活。我們提供了一種“零拷貝”圖層處理技術之後,最終上層都可以通過一個特殊的可以跨程式的Surface物件來進行非同步的繪製處理(如果我們不是直接操作控制元件,而是通過“打洞”方式來操作圖形介面上的某個區域,則屬於SurfaceView提供的,當然,這時也只是操作Surface的某一部分)。我們的Surface的繪製與post()非同步進行的,於是多個執行體可以並行處理圖層,而使用者只會看到通過post()傳送的圖層繪製完成的同步事件之後的完整圖層,圖層質量與流暢性反而可以更佳。比如,我們的VOIP應用程式,可以會涉及多個功能實體的互動,Camera、多媒體編解碼、應用程式、SurfaceFlinger。

3064872-6ca54a9da5cbe347.png

應用程式、多媒體編解碼與Camera都只會通過一個Surface物件來在後臺楨上進行互動介面的繪製,像前攝像頭出來的回顯,從網路解碼出來的遠端的視訊,然後應用程式的操作控制元件,都將重繪後臺圖層。而如果這一應用程式處於Activity的可互動狀態(見前面的生命週期的部分),就會通過找到同一Surface物件,將這一Surface物件的前臺楨(也就是繪製完成但還沒有輸出的圖層)輸出。輸出完則對這一Surface物件的前後兩楨圖層進行對調,於是這樣的流水線則可以很完美的執行下去。

Android並非是最高效的方案,而只是一種通過物件導向方式完全重新設計的嵌入式解決方案,高效是其設計的一部分要素。如果單從效率角度出發,無程式概念的實時作業系統最高效,排程開銷也小,沒有虛址切換時的開銷。作為Android系統,通過目前我們看到的功能性介面的設計,至少得到了以良好的構架為基礎同時又兼顧效能的一種設計。

當然,我們前面所總結的,對於Android系統的種種特性,最終得到的一種印象是每種設計都是萬能膠,同一種設計收穫了多種的好處。那是不是這種方式最好,大家都應該遵循這種設計上的思路與技巧?That depends,要看情況。像Android這樣要完整地實現一整套這種在嵌入式環境裡執行的,物件導向式的,而且是基於沙盒模式的系統,要麼會得到效率不高的解決方案,要麼會兼顧效能而得到大量黑客式的介面。Android最終也就是這麼一套黑客式的系統,這個系統一環套一環,作為系統核心部分的設計,都彼此過分依賴,拆都拆不開,對它進行拆分、精減或是定製,其實都很困難。但Android,其系統的核心就是Framework,而所謂的Framework,從軟體工程學意義上來說,這樣的構架卻是可以接受的。所謂的Framework,對上提供統一介面,保持系統演進時的靈活性;對下則提供抽象,封裝掉底層實現的細節。Android的整個系統層構架,則很好的完成了這樣的抽象,出於這樣的角度,我們來看看Android的可移植性設計。

1.7 可移植性

單純從可移植性角度來說,Linux核心是目前世界上可移植性最強的作業系統核心,沒有之一。目前,只要處理器晶片能夠提供基本的運算能力(可以支撐多程式在排程上的開銷),只要能夠提供C語言的編譯器(準確地說是Gnu C編譯工具鏈),就可以執行Linux核心。Linux核心在設計上保持了傳統Unix的特點,大部分使用了C語言開發,極少部分機器相關的程式碼使用匯編,這種結構使其可移植性很強。在Linux核心發展到2.6版本之後,這種強大的可移植性得到進一步提升,通過驅動模型與驅動框架的引入和不斷加強,使Linux核心裡絕大部分原始碼幾乎都沒有硬體平臺上的依賴性。於是,Linux核心幾乎能夠執行在所有的硬體平臺之上,常見有的X86、ARM,不那麼常見但可能也會在不知道不覺地使用到的有MIPS、PowerPC、Alpha,另外還有一些我們聽都沒有聽過的,像Blackfin,Cris、SuperH、Xtensa,Linux核心都支援,平臺支援可參考linux核心原始碼的arch目錄。甚至,出於Linux核心的可移植性,Linux一般也被作為晶片驗證的工具,晶片從FPGA設計到最終出廠前,都會通過Linux核心來檢測這一晶片是否可以執行,是否存在晶片設計上的錯誤。

得益於Linux核心,構建於其上的作業系統,多多少少可繼承這樣的可移植性。但Android又完成應用程式執行環境的二次抽象,在使用者態幾乎又構造出一層新的作業系統,於是它的可移植性多多少少會受此影響,而且,像我們前面所分析出來的,Android的核心層構建本身,也因為效能上的考慮,耦合性也有點強,於是在可移植性也會面臨挑戰。“窮山惡水出刁民”,正因為挑戰大,於是Android反倒通過各種技巧來加強系統本身的可移植性,反而做得遠比其他系統要好得多。Android在可移植性上的特點有:

按需要定製可移植性。與傳統嵌入式Linux作業系統不同,Android在設計上有明確的設計思想與目標,不會為了使用更多開源軟體而提供更高相容性的編譯環境,而是列出功能需求,按功能需求來定製所需要的開源軟體。有些開源軟體能夠提供更復雜的功能,但在Android環境裡,只會選擇其驗證過的必需功能,像藍芽,BlueZ本身可以提供更復雜的藍芽控制,但Android只選擇了BlueZ的基本功能,更多功能是由Android自己來實現,於是減小了依賴性,也降低了移植時的風險性。

儘可能跨平臺。與以前的系統相比,Android在跨平臺上得益於Java語言的使用,使其跨平臺能力更強,在開發上幾乎可以使用任何Java環境可以執行的作業系統裡。而在原始碼級別,它也能夠在MacOSX與Linux環境裡進行編譯,這也是一個大的突破。

硬體抽象層。Android在系統設計的最初,便規劃了硬體抽象層,通過對硬體訪問介面的抽象,使硬體的訪問介面相對穩定,而具體的實現則可在底層換用不同硬體訪問介面時靈活地加以實現,不要說應用程式,就是Framework都不會意識到這種變動。這是以前的嵌入式Linux作業系統所沒有的一種優點。硬體抽象層的使用,使Android並不一定需要侷限於Linux核心之上,如果將底層偷換成別的介面,也不會有太大的工作量。

實現介面統一的規範化。Android在構架上,都是奉行一種統一化的思路,先定義好API,然後會有Framework層的實現,然後再到硬體抽象層上的變動。API可在同一版本上擴充,Framework也在逐步加強,而硬體抽象層本身可提供的能力也越來越強,但這一切都在有組織有紀律的環境下進行,變動在任何一次版本更新上來看,都是增量的小範圍變動,而不會像普通的Linux環境那樣時刻都在變,時刻都有不相容的風險。從可移植性角度來說,這種規範化提供的好處,便是大幅降低了移植時的工作量。

儘可能簡單。簡單明瞭是Android系統構成上的一大特色,這種特色在可移植性上也是如此。像編譯環境,Android在交叉編譯環境上,是通過固化編譯選項來達到簡編譯過程的上的,最終,Android原始碼的編譯工程,會是一個個由Android.mk來構造的可編譯環境,這當然會降低靈活性,但直接導致了這套框架在跨平臺上表現非常出色。再比如硬體抽象層,同樣的抽象在現代嵌入式作業系統上可能都有,但是大都會遠比Android的HAL層要複雜,簡單于是容易理解和開發,在跨平臺性方面也會表現更好。

我們傳統的嵌入式Linux環境,幾乎都會遵從一種約定俗成的傳統,就是專注於如何將開源軟體精減,然後儘可能將PC上的執行環境照搬到嵌入式。在這種思路引導下開發出來的系統,可移植性本身是沒什麼問題的,只是不是跟X86繫結的原始碼,鐵定是可以移植。但是,這樣構建出來的系統,一般都在結構上過於複雜,會有過多的依賴性,應用程式介面並不統一,升級也困難。所有這樣的系統,最後反倒是影響到了系統的可移植性。比如開源嵌入式Linux解決方案,maemo,就是一個很好的例子:

3064872-e97a2723d657452c.png

對於Maemo的整體框架而言,我們似乎也可以看到類似於Android的層次化結構,但注意看這種系統組成時,我們就不難發現,這樣的層次化結構是假的,在Maemo環境裡,實際上就是一個小型化的Linux桌面環境,有Xorg,有gtk,有一大堆的依賴庫,程式設計環境幾乎與傳統Linux沒任何區別。這種所謂的軟體上的構架,到了Maemo的後繼者Meego,也是如此,只不過把gtk的圖形介面換成了Qt的,然後再在Qt庫環境裡包裝出所謂的UX,換湯不換藥,這時,Meego還是擁有一顆PC的心。

一般這種系統的交叉編譯環境,還必須構建在一套比較複雜的編譯環境之上,通過在編譯環境裡模擬出一個Linux執行環境,然後才能編譯儘可能多的原始碼。這樣的交叉編譯環境有Open Embedded,ScratchBox等。雖然有不同的交叉編譯實現上的思路,但並沒有解決可移植性問題,它們必須在Linux作業系統裡執行,而且使用上的複雜程度,不是經驗豐富的Linux工作者還沒辦法靈活使用。即便是比較易用的ScratchBox,也會有如下令人眼花繚亂的結構。

3064872-3098d6963027e264.png

針對這樣的現狀,Android在解決可移植性問題時的思路就要簡單得多。既然原來的嘗試不成功,PC被精減到嵌入式環境裡效果並不好,這時就可以換一種思路,一種“返璞歸真”的思路,直接從最底層來簡化設計,簡化交叉編譯。這樣做法的一個最重要前提條件,就是Android本身是完整重新設計與實現的,是一個自包含的系統,所有的編譯環境,都可以從原始碼裡把系統編譯出來。

在系統結構上,Android在設計上便拋棄了傳統的大肆搜刮開原始碼的做法,由自己的設計來定位需要使用的開原始碼,如果沒有合適的開原始碼,則會提供一個簡單實現來實現這一部分的功能。於是,得到我們經常見到的Android的四層結構:

3064872-4f82bfdd247fc83e.png

從這樣簡化過的四層結構裡,最底層的Linux核心層,這些都與其他嵌入式Linux解決方案是共通的特性,都是一樣的。其他三層就與其他作業系統大相徑庭了:應用程式層是一種基於“沙盒”模式的,以功能共享為最終目的的統一開發層,並非只是用於開發,同時還會通過API來規範這些應用程式的行為;Framework層,這是Android真正的核心層,而從程式設計環境上來說,這一層算是Java層,任何底層功能或硬體介面的訪問,都會通過JNI訪問到更低層次來實現;給Framework提供支撐的就是Library層,也就是使用的一個自己實現的,或是第三方的庫環境,這一層以C/C++編寫的可以直接在機器上執行的ELF檔案為主。

有了這種更簡化的層次關係,使Android最後得到的原始碼相對來說更加固定,應用程式這層我們只會編譯Java,Framework層的編譯要麼是Java要麼是JNI,而Library層則會是C/C++的編譯。在比較固定的編譯目標基礎上,編譯環境所需要解決的問題則會比較少,於是更容易通過一些簡化過的編譯環境來實現。Android使用了最基本的編譯環境GnuMake,然後再在其上使用最基本的Gnu工具鏈(不帶library與動態連結支援)來編譯原始碼,最後再通過簡化過的連結器來完成動態連結。得到的結果是幾乎不需要編譯主機上的環境支援,從而可以在多種作業系統環境裡執行,Android的編譯工程檔案還比較簡單,更容易編寫,Android.mk這種Android裡的編譯工程檔案,遠比天書般的autoconf工具要簡單,比傳統的Makefile也更容易理解。

3064872-4ac437bcd15e02eb.png

Android的編譯系統,由build目錄裡一系列.mk編譯指令碼和其他指令碼組成,以build/main.mk作為主入口。main.mk檔案會改到配置項,然後再通過配置項迴圈地去編譯出所需要的LOCAL_MODULE。而這些LOCAL_MODULE的編譯目標,是由Android.mk來定義的,而到底如何編譯目標物件,則是由簡單的include $(BUILD_*)這樣的編譯巨集選項來提供,比如include $(BUILD_SHARED_LIBRARY)則會編譯生成動態連結庫.so檔案。這樣的編譯系統拋棄autoconf的靈活性,換回了跨平臺、編寫簡單。

當然,這樣的編譯工具,也不是沒有代價的,使用簡易化的Android編譯環境則意味著Android放棄了一些現有絕大部分程式碼的可移植性,Linux環境裡的一些常用庫移植到Android環境則需要大量移植工作,像萬能播放器核心ffmpeg,目前幾乎只有商用應用程式才肯花精力將它移植到Android系統裡。

當然,光有自己程式碼結構,光有更簡易的編譯環境,並不解決所有問題,我們的Android系統,最後還必須通過訪問系統呼叫、讀寫驅動的裝置檔案來完成底層的操作。不然我們的Android系統就只會是一個光桿司令,什麼都不是,沒有任何功能。既然我們擁有了相關簡化的系統結構,我們在內部自己按自己的想法去訪問硬體也是可以,只是這樣在升級與維護起來的程式碼會變得很大,比如我們通過”/dev/input/event0”來讀觸控式螢幕,”/dev/input/event1”來讀重力感應器,如果換個平臺,這些裝置名字變了怎麼辦?或者有些私有化平臺,都完全不使用這樣的標準化裝置檔案命名時怎麼辦?難道針對每個平臺準備一份原始碼來做這些修改?於是就有了內部訪問介面統一化的問題,Android在對於底層的裝置檔案的訪問上,又完成了一層抽象,也就是我們的硬體抽象層。

硬體抽象層,準確地說,是介於Framework層與Linux核心層之間的一個層次,Framework通過硬體抽象層的統一介面來向下訪問,在Linux核心上硬體訪問介面上的差異性,則通過硬體抽象層來進行遮蔽。硬體抽象層,英文是Hardware Abstraction Layer,簡稱HAL,於是也就是我們常見到的AndroidHAL層。

3064872-83be4d47b82e1e5c.png

提供硬體抽象層之後,這時Framework層與底層Linux核心之間的耦合性,就完全消除掉了。如果要將Android部署到其他作業系統、或是作業系統核心之上,只需要將HAL層的相應介面實現換成其他平臺上的訪問機制即可,而Framework只會使用HAL層的統一介面向下訪問,完全不知道底層變動資訊。

在Android的世界裡,硬體抽象其實是包含多種含義的,可能會有多種的硬體訪問實現,比如RIL、BlueZ這樣的儘可能相容已有解決方案的廣義上的HAL,Framework會通過Socket來訪問傳統方式實現的daemon,也有Gralloc、Camera這樣先定義Framework向下訪問的介面,然後由硬體向上提供介面實現的狹義上的HAL。我們的Android上的HAL實現,更多地專指狹義上的HAL,由原始碼裡的hardware目錄提供。

狹義上的HAL,在實現上也會分成兩種:一種基於函式直接訪問的方式來實現,這種方式比較簡單粗暴,所以在命名上,被稱為libhardware_legacy實現,由目錄hardware/libhardware_legacy裡實現的一個個動態連結庫來實現;另一種則使用了物件導向的技巧,上層通過一個hardware_module_t來訪問,而具體的某一類HAL的實現,則會將這一hardware_module_t按需求進行擴充,這種方式被稱為libhardware實現,由hardware/libhardware提供hardware_modult_t的訪問介面,硬體訪問的實現部分,也是動態連結庫檔案,但會在執行時根據板卡的具體配置,進行動態載入。從長遠看,libhardware_legacy結構的HAL實現是一種中間方案,在Android發展過程裡有可能會被libhardware方式的實現所取代,比如Android 4.0裡,Audio、Camera完成了libhardware_legacy到libhardware的轉變。

3064872-66a0054e01140423.png

從設計的角度來看,libhardware_legacy雖然解決了與Linux核心的耦合問題,但是直接函式介面的訪問,終究還是靈活性不夠。以libhardware_legacy實現HAL的動態連結庫,必須被直接連結到Framework的實現裡,通過JNI進行直接訪問,不具備動態性,不能支援同一份Android可執行環境支援不同硬體平臺。從設計角度來說,任何抽象都是由另一次間接呼叫來實現,於是,我們在硬體訪問介面裡再加入一層抽象,這就是libhardware.so。Framework並不直接訪問具體的動態連結庫實現,而是通過libhardware.so裡實現的通用介面來向下訪問,並且也只會呼叫到預定義好的一些訪問介面,而HAL實現的,是以Stub方式來提供到系統裡一些功能訪問的具體實現。如果不好理解的,可以認為是系統提供標頭檔案,也就是我們右圖中的<>,這些只是介面類定義。而實現上,並不是直接通過標頭檔案來實現,而是通過實現一個具有一定特性的hardware_module_t的資料結構,來向上提供具體的函式呼叫介面,與標頭檔案裡所需要的介面相對應。為什麼叫它Stub呢?是因為在libhardware這種模式裡,把介面定義與具體的實現抽離開來,雖然不一定會使用面嚮物件語言來實現(一般是通過C來實現的),但提供了Interface+Stub這樣物件導向式的實現,所以libhardware有時也被稱為stub模式實現的HAL。

使用libhardware之後,HAL層實現上的可複用性與執行時的靈活性則被大大增強了。在libhardware框架下,Framework都不再直接呼叫HAL層,而是通過hw_get_module()方法來在/system/lib/hw和/system/vendor/lib/hw這兩個目錄裡迴圈尋找合適的.so實現,比如針對sensor,會有sensor.default.so,sensor.goldfish.so,sensor.xxx.so,會有不同種實現,以用於載入合適的實現。雖然這些只會在開機時執行一次,但通過簡單方式至少也實現了用同一份二進位制程式碼提供多種硬體平臺的支援。使用libhardware實現的HAL,當我們的實現的HAL有問題時,我們可以刪掉有問題的HAL,此時啟動時會使用一個xxx.default.so的純軟體實現的不進行任何硬體訪問的.so檔案,讓我們還是可以繞開啟動時的HAL載入錯誤。

Android這種強化可移植性的設計,最終使Android的移植移植過程變得相對比較簡單。如果是做Android的手機或是平板的開發,也許我們需要做的只是提供板塊相關的配置檔案,從而可以改變一些編譯時的配置引數。如果我們的硬體平臺跟Android原始碼時使用的標準平臺(比如Google的“親兒子”手機Nexus系列的產品,或是pandaboard這樣作為參考設計的產品),對於移植過程而言,我們可能什麼都不需要做,直接可以編譯執行,然後再做產品化的微調;如果我們使用的硬體平臺跟某些產商提供的開源專案的硬體結構一樣,比如Qualcomm提供的codeaurora.org,TI的Omapedia,還有各大廠商都湧躍參與的linaro.org專案等等,這時需要完成的移植工作也會類似的很小;如果我們的提供的硬體平臺跟Android這些已有的開源資源很不一樣,這時,我們需要完成的移植工作也不會很大,只需要根據特定的硬體平臺實現HAL,這一過程所需要的工作量遠小於其他平臺的移植過程。

Android的移植過程,基本上分為:

Bootloader與Linux核心的移植

Repo環境(Android原始碼基於repo管理,最後使用repo)

交叉編譯器、Bionic C庫與Dalvik虛擬機器的移植(如果不是ARM、X86和MIPS這三種基本構架)

提供板卡支援所需要的配置

實現所需要使用的HAL

Android產品化,完成介面或是功能上的定製

這些移植過程的步驟如下圖所示:

3064872-4d36135998839c51.png

對於我們做Android移植與系統級開發而言,可能我們所需要花的程式碼並不是那麼大。像Bootloader與Linux核心的移植,這一般都在進行Android系統移植時早就會就緒的,比如我們去選購某個產商的主晶片時(Application Processor,術語為AP),這些Android之前的支援大都已經就緒。而作為產業的霸主,我們除非極其特殊的情況,我們也不需要接觸交叉編譯器和Dalvik虛擬機器的移植。所以一般情況下,我們的Android移植是從建立repo原始碼管理環境開始,然後再進行板卡相關的配置,然後實現HAL。而Android的產品化這個步驟,Framework的細微調整與編寫自己平臺上特殊的應用程式,嚴格意義上來說,Framework也不屬於Android移植工作範圍內的,我們一般把它定位於Android產品化或是Android定製化這個步驟裡。Android移植相對來說非常簡單,而真正完成Android產品化則會是一個比較耗時耗人力的過程。

所謂的板卡的配置檔案,一般是放在一個專門的目錄裡,在2.3以前,是發在vendor目錄下,從2.3開始,vendor目錄只存放二進位制程式碼,配置檔案移到了device目錄。在這一目錄裡,會以“產商名/裝置名”的命名方式來規範配置的目錄結構。比如是產商名是ti,裝置名是panda,則會以“device/ti/panda”來存放這些配置檔案,再在這個目錄裡放置平臺相關的配置項。配置檔案,則是會幾個關鍵檔案構成:

} vendorsetup.sh,使用add_lunch_combo將配置項匯入編譯環境

} AndroidProducts.mk,這是會被編譯系統掃描的檔案,通過在這一檔案裡再匯入具體的編譯配置檔案,比如ti_panda.mk

} ti_panda.mk,在這一檔案裡定義具體的產品名,裝置名這些關鍵變數,這些變數是在Android編譯過程裡起關鍵配置作用的變數。一般說來,這個檔案不會很複雜,主要依賴匯入一些其他的配置檔案來完成所有的配置,比如語言配置等。而裝置特殊的設定,則一般是使用同目錄下的device.mk檔案來進行定製化的設定。

} device.mk,在這一檔案會使用一些更加複雜一些配置,包含一些需要編譯的子工程,設定某些特殊的編譯引數,以及進行系統某些特性的定製化,比如需要自定義怎樣的顯示效果、配置檔案等

} BoardConfig.mk,在這一檔案則是板子相關的一些定製項,以巨集的方式傳入到編譯過程裡,比如BOARD_SYSTEMIMAGE_PARTITION_SIZE來控制system分割槽的大小, TARGET_CPU_SMP來控制是否需要使用SMP(對稱多處理器)支援等。一般,對於同一個板卡環境,這些引數可以照抄,勿須修改。

所有的這些配置檔案,並不是必須的,只不過是建議性的,在這一點上也常會透露出產商在開源文化的素質。畢竟是開源的方案,如果都使用約定俗成的解決方案,則大家都會不用看也知道怎麼改。但一些在開源做得不好的廠商,對這些的配置環境都喜歡自己搞一套東西出來,要顯得自己與眾不同,所以對於配置檔案的寫法與移植過程,也需要具體情況具體對待。

當我們完成了這些配置上的工作後,可以先將這些配置上傳到repo的伺服器管理起來,剩下的移植工作就是實現所需要的HAL了。在Android移植過程裡,很多HAL的實現,是可以大量複用的,可以找一個類似的配置複製過來,然後再進行細微調整,比如使用ALSA ASoC框架的音訊支援,基本功能都是通用的,只需要在Audio Path和HiJack功能上進行微調即可。

相關文章