轉(zhuǎn)載于? http://www.uml.org.cn/mobiledev/201211063.asp#2
緊接連載三件豌,我們接下從性能的角度分別分析Android系統(tǒng)為應(yīng)用程序提供的支撐。
1.4 性能
Android使用Java作為編程語(yǔ)言菜秦,這一直被認(rèn)為是一局雄心萬(wàn)丈,但兇險(xiǎn)異常的險(xiǎn)棋七咧。Java的好處是多吴超,前面我們只是列舉了一小部分庄呈,但另一種普遍的現(xiàn)象是,Java在圖形編程上的應(yīng)用環(huán)境并不是那么多剖张。除了出于Java編程的目的切诀,我們是否使用過Java編寫的應(yīng)用程序?我們的傳統(tǒng)手機(jī)一般都支持Java ME版本搔弄,有多少人用過幅虑?我們是否見過Java寫就的很流暢的應(yīng)用程序?是否有過流行的Java操作系統(tǒng)顾犹?答應(yīng)應(yīng)該是幾乎為零倒庵。通過這些疑問褒墨,我們就可以多少了解,Android這個(gè)Java移動(dòng)操作系統(tǒng)難度了擎宝,還不用說(shuō)郁妈,我們前面看到各種設(shè)計(jì)上的考量。
首先绍申,得給Java正名噩咪,并非Java有多么嚴(yán)重的性能問題,事實(shí)上极阅,Java本是一種高效的運(yùn)行環(huán)境胃碾。Android在設(shè)計(jì)上的兩個(gè)選擇,一個(gè)Linux內(nèi)核筋搏,一個(gè)Java編程語(yǔ)言书在,都是神奇的萬(wàn)能膠,從服務(wù)器拆又、桌面系統(tǒng)儒旬、嵌入式平臺(tái),它們都在整個(gè)計(jì)算機(jī)產(chǎn)業(yè)里表現(xiàn)了強(qiáng)大的實(shí)力帖族,在各個(gè)領(lǐng)域里都霸主級(jí)的神器(當(dāng)然栈源,兩者在桌面領(lǐng)域表現(xiàn)都不是很好,都受到了Windows的強(qiáng)大壓力)竖般。從Java可以充當(dāng)服務(wù)器的編程語(yǔ)言甚垦,我們就可以知道Java本身的性能是夠強(qiáng)大的。標(biāo)準(zhǔn)的Java虛擬機(jī)還有個(gè)特點(diǎn)涣雕,性能幾乎會(huì)與內(nèi)存與處理器能力成正比艰亮,內(nèi)存越大CPU能力越強(qiáng),當(dāng)JIT的威力完全發(fā)揮出來(lái)時(shí)挣郭,Java語(yǔ)言的性能會(huì)比C/C++寫出來(lái)的代碼的性能還要好迄埃。
但是,這不是嵌入式環(huán)境的特色兑障,嵌入式設(shè)備不僅是CPU與內(nèi)存有限侄非,而且還不能濫用,需要盡可能節(jié)省電源的使用流译。這種特殊的需求下逞怨,還需要使用Java虛擬機(jī),就得到一個(gè)通用性極佳福澡,但幾乎無(wú)用的JAVA ME環(huán)境叠赦,因?yàn)镴AVA ME的應(yīng)用程序與本地代碼的性能差異實(shí)在太大了。在iPhone引發(fā)的“后PC時(shí)代”之前革砸,大家沒得選擇除秀,每臺(tái)手機(jī)上幾乎都支持窥翩,但使用率很低。而我們現(xiàn)在看到的Android系統(tǒng)鳞仙,性能上是絕對(duì)不成問題寇蚊,即便是面對(duì)iPhone這樣大量使用加速引擎的嵌入式設(shè)備,也沒有明確差異棍好,這里面竅門在哪里呢仗岸?
我們前面也說(shuō)明了,Java在嵌入式環(huán)境里有性能問題借笙,運(yùn)行時(shí)過低的執(zhí)行效率會(huì)引發(fā)使用體驗(yàn)問題扒怖,同時(shí)還有版權(quán)問題,幾乎沒有免費(fèi)的嵌入式Java解決方案业稼。如果正向去解盗痒,困難重重,那我們是否可以嘗試逆向的解法呢低散?
在計(jì)算機(jī)工程領(lǐng)域俯邓,逆向思維從來(lái)都是重要武器之一,當(dāng)我們?cè)谠O(shè)計(jì)熔号、算法上遇到問題時(shí)稽鞭,正向解決過于痛苦時(shí),可以逆向思考一下引镊,往往會(huì)有奇效朦蕴。比如我們做算法優(yōu)化,拼了命使用各種手法提高性能弟头,收效也不高吩抓,這時(shí)我們也可以考慮一下降低其他部分的性能。成功案例在Android里就有赴恨,在Android環(huán)境里疹娶,大量使用Thumb2這樣非優(yōu)化指令,只優(yōu)化性能需求最強(qiáng)的代碼嘱支,這時(shí)系統(tǒng)整體的性能反而比全局優(yōu)化性能更好蚓胸。我們做安全機(jī)制,試驗(yàn)各種理論模型除师,最終都失敗于復(fù)雜帶來(lái)的開銷,而我們反向思考一下扔枫,借用進(jìn)程模型來(lái)重新設(shè)計(jì)汛聚,這時(shí)我們就得到了“沙盒”這種模型,反而簡(jiǎn)便快捷短荐。只要思路開闊倚舀,在計(jì)算機(jī)科學(xué)里條條大路通羅馬叹哭,應(yīng)該是沒有解不掉的問題的。
回到Android的Java問題痕貌,如果我們正向地順著傳統(tǒng)Java系統(tǒng)的設(shè)計(jì)思路會(huì)走入痛苦之境风罩,何不逆向朝著反Java的思路去嘗試呢?這時(shí)還帶來(lái)了邊際收益舵稠,Java語(yǔ)言的版權(quán)問題也繞開了超升。于是,天才的Android設(shè)計(jì)者們哺徊,得到了基于Dalvik虛擬機(jī)方案:
基于寄存器式訪問的Dalvik VM
Java語(yǔ)言天生是用于提升跨平臺(tái)能力的室琢,于是在設(shè)計(jì)與優(yōu)化時(shí),考慮得更多是如何兼容更多的環(huán)境落追,于它可以運(yùn)行在服務(wù)器級(jí)別的環(huán)境里盈滴,也在運(yùn)行在嵌入式環(huán)境。這種可伸縮的能力轿钠,便源自于它使用棧式的虛擬機(jī)實(shí)現(xiàn)巢钓。所謂的棧式虛擬機(jī),就是在Java執(zhí)行環(huán)境在邊解釋邊執(zhí)行的時(shí)候疗垛,盡可能使用棧來(lái)保存操作數(shù)與結(jié)果鲁豪,這樣可設(shè)計(jì)更精練的虛擬指令的翻譯器,性能很高点待,但麻煩就在于需要過多地訪問內(nèi)存爱榕,因?yàn)闂<磧?nèi)存。
比如花履,我們寫一個(gè)最簡(jiǎn)單的Java方法芽世,讀兩個(gè)操作數(shù),先加诡壁,然后乘以2济瓢,最后返回:
public int test01( int i1, int i2 ) {
int i3 = i1 + i2;
return i3 * 2;
}
這樣的代碼,使用Java編譯器生成的.class文件里妹卿,最后就會(huì)有這樣的偽代碼(虛擬機(jī)解析執(zhí)行的代碼):
0000: iload_1 // 01
0001: iload_2 // 02
0002: iadd
0003: istore_3 // 03
0004: iload_3 // 03
0005: iconst_2 // #+02
0006: imul
0007: ireturn
出于棧式虛擬機(jī)的特點(diǎn)旺矾,這樣的偽指令追成的結(jié)果會(huì)操作到棧。比如iload_1, iload_2夺克,iadd箕宙,istore_3,這四行铺纽,就是分別將參數(shù)1柬帕,2讀取到棧里,再調(diào)用偽指令add將棧進(jìn)行加操作,然后將結(jié)果寫入棧頂陷寝,也就是操作數(shù)3锅很。這種操作模式,會(huì)使我們的虛擬機(jī)在偽指令解析器上實(shí)現(xiàn)起來(lái)簡(jiǎn)單凤跑,因?yàn)楹?jiǎn)單而可以實(shí)現(xiàn)得高效爆安,因?yàn)閭沃噶畋厝缓苌伲ㄊ聦?shí)上,通用指令用8位的opcode就可以表達(dá)完)仔引∪硬郑可以再在這種設(shè)計(jì)基礎(chǔ)上,針對(duì)不同平臺(tái)進(jìn)行具體的優(yōu)化肤寝,由于是純軟件的算法当辐,通用性非常好,無(wú)論是32位還是64位機(jī)器鲤看,都可以很靈活地針對(duì)處理器特點(diǎn)進(jìn)行針對(duì)性的實(shí)現(xiàn)與優(yōu)化缘揪。而且這種虛擬機(jī)實(shí)現(xiàn),相當(dāng)于所有操作都會(huì)在棧上有記錄义桂,對(duì)每個(gè)偽指令(opcode)都相當(dāng)于一次函數(shù)調(diào)用找筝,這樣實(shí)現(xiàn)JIT就更加容易。
但問題是內(nèi)存的訪問過多慷吊,在嵌入式設(shè)備上則會(huì)造成性能問題袖裕。嵌入式平臺(tái)上,出于成本與電池供電的因素溉瓶,內(nèi)存是很有限的急鳄,Android剛開始時(shí)使用的所謂頂級(jí)硬件配置,也才不過192MB堰酿。雖然現(xiàn)在我們現(xiàn)在的手機(jī)動(dòng)不動(dòng)就上G疾宏,還有2G的怪獸機(jī),但我們也不太可能使用太高的內(nèi)存總線頻率触创,頻率高了則功耗也就會(huì)更高坎藐,而內(nèi)存總線的功耗限制也使內(nèi)存的訪問速度并不會(huì)太高,與PC環(huán)境還是有很大差異的哼绑。所以岩馍,直到今天,標(biāo)準(zhǔn)Java虛擬的ARM版本也沒有實(shí)現(xiàn)抖韩,只是使用JAVA ME框架里的一種叫KVM的一種嵌入式版本上的特殊虛擬機(jī)蛀恩。
基于Java虛擬機(jī)的實(shí)現(xiàn),于是這時(shí)就可以使用逆向思維進(jìn)行設(shè)計(jì)帽蝶,棧式虛擬機(jī)的對(duì)立面就是寄存器式的赦肋。于是Android在系統(tǒng)設(shè)計(jì)便使用了Dan Bornstein開發(fā)的块攒,基于寄存器式結(jié)構(gòu)的虛擬機(jī)励稳,命名源自于芬蘭的一個(gè)小鎮(zhèn)Dalvik佃乘,也就是Dalivk虛擬機(jī)。雖然Dalvik虛擬機(jī)在實(shí)現(xiàn)上有很多技巧的部分驹尼,有些甚至還是黑客式的實(shí)現(xiàn)趣避,但其核心思想就是寄存器式的實(shí)現(xiàn)。
所謂的寄存器式新翎,就是在虛擬機(jī)執(zhí)行過程中程帕,不再依賴于棧的訪問,而轉(zhuǎn)而盡可能直接使用寄存器進(jìn)行操作地啰。這跟傳統(tǒng)的編程意義上的基于寄存器式的系統(tǒng)構(gòu)架還是有概念上的區(qū)別愁拭,即便是我們的標(biāo)準(zhǔn)的棧式的標(biāo)準(zhǔn)Java虛擬機(jī),在RISC體系里亏吝,我們也會(huì)在代碼執(zhí)行被優(yōu)化成大量使用寄存器岭埠。而這里所指的寄存器,是指執(zhí)行過程里的一種算法上的思路蔚鸥,就是不依賴于棧式的內(nèi)存訪問惜论,而通過偽指令(opcode)里的虛擬寄存器來(lái)進(jìn)行翻譯,這種虛擬寄存器會(huì)在運(yùn)行態(tài)被轉(zhuǎn)義成空閑的寄存器止喷,進(jìn)行直接數(shù)操作馆类。如果這里的解釋不夠清晰,大家可以把棧式看成是正常的函數(shù)式訪問弹谁,幾乎所有的語(yǔ)言都基于棧來(lái)實(shí)現(xiàn)函數(shù)調(diào)用乾巧,此時(shí)虛擬機(jī)里每個(gè)偽指令(opcode),都類似于基于棧進(jìn)行了函數(shù)調(diào)用预愤。而基于寄存器式沟于,則可以被看成是宏,宏在執(zhí)行之前就會(huì)被編譯器翻譯成直接執(zhí)行的一段代碼鳖粟,這時(shí)將不再有過多的壓棧出棧操作社裆,而是會(huì)盡可能使用立即數(shù)進(jìn)行運(yùn)算。
我們上面標(biāo)準(zhǔn)虛擬機(jī)里編譯出來(lái)的代碼向图,經(jīng)過dx工具轉(zhuǎn)出來(lái)的.dex文件泳秀,格式就會(huì)跟上面很不一樣:
代碼編譯偽指令
9000 0203 0000 add-int v0, v2, v3
da00 0002 0002 mul-int/lit8 v0, v0, #int 2 // #02
0f00 0004 return v0
對(duì)著代碼,我們可以看到(或者可以猜測(cè))榄攀,在Dalvik的偽指令體系里嗜傅,指令長(zhǎng)度變成了16bit,同時(shí)檩赢,根本去除了棧操作吕嘀,而通過使用00, 02, 03這樣的虛擬寄存器來(lái)進(jìn)行立即數(shù)操作,操作完則直接將結(jié)果返回。大家如果對(duì)Dalvik的偽指令體系感興趣偶房,可以參考Dalvik的指令說(shuō)明:http://source.android.com/tech/dalvik/dalvik-bytecode.html
像Dalvik這樣虛擬機(jī)實(shí)現(xiàn)里趁曼,當(dāng)我們進(jìn)行執(zhí)行的時(shí)候,我們就可以通過將00棕洋,02挡闰,03這樣的虛擬寄存器,找一個(gè)空閑的真實(shí)的寄存器換上去掰盘,執(zhí)行時(shí)會(huì)將立即數(shù)通過這些寄存器進(jìn)行運(yùn)算摄悯,而不再使用頻繁的棧進(jìn)行存取操作。這時(shí)得到的代碼大小愧捕、執(zhí)行性能都得到了提升奢驯。
如果寄存器式的虛擬機(jī)實(shí)現(xiàn)這么好,為什么不大家都使用這種方式呢次绘?也不是沒有過嘗試瘪阁,寄存器試的虛擬機(jī)實(shí)現(xiàn)一直是學(xué)術(shù)研究上的一個(gè)熱點(diǎn),只是在Dalvik虛擬機(jī)之前断盛,沒有成功過罗洗。寄存器式,在實(shí)際應(yīng)用中钢猛,未必會(huì)比棧式更高效伙菜,而且如果是通用的Java虛擬機(jī),需要運(yùn)行在各種不同平臺(tái)上命迈,寄存器式實(shí)現(xiàn)還有著天生的缺陷贩绕。比如說(shuō)性能,我們也看到在Dalvik的這種偽指令體系里壶愤,使用16位的opcode用于實(shí)現(xiàn)更多支持淑倾,沒有棧訪問,則不得不靠增加opcode來(lái)彌補(bǔ)征椒,再加需要進(jìn)行虛擬寄存器的換算娇哆,這時(shí)解析器(Interpreter)在優(yōu)化時(shí)就遠(yuǎn)比簡(jiǎn)單的8位解析器要復(fù)雜得多,復(fù)雜則優(yōu)化起來(lái)更困難勃救。從上面的函數(shù)與宏的對(duì)比里碍讨,我們也可以看到寄存器實(shí)現(xiàn)上的毛病,代碼的重復(fù)量會(huì)變大蒙秒,原來(lái)不停操作棧的8bit代碼會(huì)變成更長(zhǎng)的加寄存器操作的代碼勃黍,理論上這種代碼會(huì)使代碼體系變大。之所以在前面我們看到.dex代碼反而更精減晕讲,只不過是Dalvik虛擬機(jī)進(jìn)行了犧牲通用性代碼固化覆获,這解決了問題马澈,但會(huì)影響到偽代碼的可移植性。在棧式虛擬機(jī)里弄息,都使用8bit指令反復(fù)操作棧痊班,于是理論上16bit、32bit疑枯、64bit辩块,都可以有針對(duì)性的優(yōu)化蛔六,而寄存器式則不可能荆永,像我們的Dalvik虛擬機(jī),我們可以看到它的偽代碼會(huì)使用32位里每個(gè)bit国章,這樣的方式不可能通用具钥,16bit、32bit液兽、64bit的處理器體系里骂删,都需要重新設(shè)計(jì)一整套新的指令體系,完全沒有通用性四啰。最后宁玫,所有的操作都不再經(jīng)過棧,則在運(yùn)行態(tài)要得到正確的運(yùn)算操作的歷史就很難柑晒,于是JIT則幾乎成了不可能完成的任務(wù)欧瘪,于是Dalvik虛擬機(jī)剛開始則宣稱JIT是沒有必要,雖然從2.2開始加入了JIT匙赞,但這種JIT也只是統(tǒng)計(jì)意義上的佛掖,并不是完整意義上的JIT運(yùn)算加速。所有這些因素涌庭,都導(dǎo)致在標(biāo)準(zhǔn)Java虛擬機(jī)里芥被,很難做出像Dalvik這樣的寄存器式的虛擬機(jī)。
幸運(yùn)的是坐榆,我們上面說(shuō)的這些限制條件拴魄,對(duì)Android來(lái)說(shuō),都不存在席镀。嵌入式環(huán)境里的CPU與內(nèi)存都是有限的資源匹中,我們不太可能通過全面的JIT提升性能,而嵌入式環(huán)境以寄存器訪問基礎(chǔ)的RISC構(gòu)架為主愉昆,從理論上來(lái)說(shuō)职员,寄存器式的虛擬機(jī)將擁有更高的性能。如果是嵌入式平臺(tái)跛溉,則基本上都是32位的處理器焊切,而出于功耗上的限制扮授,這種狀況將持續(xù)很長(zhǎng)一段時(shí)間,于是代碼通用性的需求不是那么高专肪。如果我們放棄全面支持Java執(zhí)行環(huán)境的兼容性刹勃,進(jìn)一步通過固化設(shè)計(jì)來(lái)提升性,這時(shí)我們就可以得到一個(gè)有商用價(jià)值的寄存器式的虛擬機(jī)嚎尤,于是荔仁,我們就得到了Dalvik。
Dalvik虛擬機(jī)的性能上是不是比傳統(tǒng)的棧式實(shí)現(xiàn)有更高性能芽死,一直是一個(gè)有爭(zhēng)議的話題乏梁,特別是后面當(dāng)Dalvik也從2.2之后也不得不開始進(jìn)行JIT嘗試之后。我們可以想像关贵,基于前面提到的寄存器式的虛擬機(jī)的實(shí)現(xiàn)原理遇骑,Dalvik虛擬機(jī)通過JIT進(jìn)行性能提升會(huì)遇到困難。在Dalvik引入JIT后揖曾,性能得到了好幾倍的提升落萎,但Dalvik上的這種JIT,并非完整的JIT炭剪,如果是棧式的虛擬機(jī)實(shí)現(xiàn)练链,這方面的提升會(huì)要更強(qiáng)大。但Dalvik實(shí)現(xiàn)本身對(duì)于Android來(lái)講是意義非凡的奴拦,在Java授權(quán)上繞開了限制媒鼓,更何況在Android誕生時(shí),嵌入式上的硬件條件極度受限粱坤,是不太可能通過棧式虛擬機(jī)方式來(lái)實(shí)現(xiàn)出一個(gè)性能足夠的嵌入式產(chǎn)品的隶糕。而當(dāng)Android系統(tǒng)通過Dalvik虛擬機(jī)成功殺出一條血路,讓大家都認(rèn)可這套系統(tǒng)之后站玄,圍繞Android來(lái)進(jìn)行虛擬機(jī)提速也就變得更現(xiàn)實(shí)了枚驻,比如現(xiàn)在也有使用更好的虛擬機(jī)來(lái)改進(jìn)Android的嘗試,比如標(biāo)準(zhǔn)棧式虛擬機(jī)株旷,使用改進(jìn)版的Java語(yǔ)言的變種再登,像Scalar、Groovy等晾剖。
我們?cè)倏纯达笔福诩拇嫫魇降奶摂M機(jī)之外,Android在性能設(shè)計(jì)上的其他一些特點(diǎn)齿尽。
以Android API為基礎(chǔ)沽损,不再以Java標(biāo)準(zhǔn)為基礎(chǔ)
當(dāng)我們對(duì)外提供的是一個(gè)系統(tǒng),一種平臺(tái)之時(shí)循头,就必須要考慮到系統(tǒng)的可持續(xù)升級(jí)的能力绵估,同時(shí)又需要保持這種升級(jí)之后的向后兼容性炎疆。使用Java語(yǔ)言作為編程基礎(chǔ),使我們的Android環(huán)境国裳,得到了另一項(xiàng)好處形入,那就是可兼容性的提升。
在傳統(tǒng)的嵌入式Linux方案里缝左,受限于有限的CPU亿遂,有限的內(nèi)存,大家還沒有能力去實(shí)施一套像Android這樣使用中間語(yǔ)言的操作系統(tǒng)渺杉。使用C語(yǔ)言還需要加各種各樣的加速技巧才能讓系統(tǒng)可以運(yùn)行蛇数,基至有時(shí)還需要通過硬件來(lái)進(jìn)行加速,再在這種平臺(tái)上運(yùn)行虛擬機(jī)環(huán)境少办,則很不靠譜苞慢。這樣的開發(fā),當(dāng)然沒有升級(jí)性可言英妓,連二次開發(fā)的能力都很有限,更不用說(shuō)對(duì)話接口了绍赛,所謂的升級(jí)蔓纠,僅僅是增加點(diǎn)功能,修改掉一些Bug吗蚌,再加入再多Bug腿倚。而使用機(jī)器可以直接執(zhí)行的代碼,就算是能夠提供升級(jí)和二次開發(fā)的能力蚯妇,也會(huì)有嚴(yán)重問題敷燎。這樣的寫出來(lái)的代碼,在不同體系架構(gòu)的機(jī)器(比如ARM箩言、X86硬贯、MIPS、PowerPC)上陨收,都需要重新編譯一次饭豹。更嚴(yán)重的是,我們的C或者C++务漩,都是通過參數(shù)壓棧拄衰,再進(jìn)行指令跳轉(zhuǎn)來(lái)進(jìn)行函數(shù)調(diào)用的,如果升級(jí)造成了函數(shù)參數(shù)變動(dòng)饵骨,則還必須修改所開發(fā)的源代碼翘悉,不然會(huì)直接崩潰掉。而比較幸運(yùn)的是居触,所有的嵌入式Linux方案妖混,在Android之前包吝,都沒有流行開,比較成功的最多也不過自己陪自己玩源葫,份額很小诗越,大部分則都是紅顏薄命,出生時(shí)是demo息堂,消亡時(shí)也是demo嚷狞。不然,這樣的產(chǎn)品荣堰,將來(lái)維護(hù)起來(lái)也會(huì)是個(gè)很吐血的過程床未。
而Android所使用的Java,從一開始就是被定位于“一次編寫振坚,到處運(yùn)行”的薇搁,不用說(shuō)它極強(qiáng)大的跨平臺(tái)能力,就是其升級(jí)性與兼容性渡八,也都是Java語(yǔ)言的制勝法寶之一啃洋。Java編譯生成的結(jié)果,是.class的偽代碼屎鳍,是需要由虛擬器來(lái)解析執(zhí)行的宏娄,我們可以提供不同體系構(gòu)架里實(shí)現(xiàn)的Java虛擬機(jī),甚至可以是不同產(chǎn)商設(shè)計(jì)生產(chǎn)的Java虛擬機(jī)逮壁,而這些不同虛擬機(jī)孵坚,都可以執(zhí)行已經(jīng)編譯過的.class文件,完全不需要重新編譯窥淆。Java是一種高級(jí)語(yǔ)言卖宠,具有極大的重用性,除非是極端的無(wú)法兼容接口變動(dòng)忧饭,都可以通過重載來(lái)獲得更高可升級(jí)能力扛伍。最后,Java在歷史曾應(yīng)用于多種用途的運(yùn)行環(huán)境眷昆,于是定義針對(duì)不同場(chǎng)合的API標(biāo)準(zhǔn)蜒秤,這些標(biāo)準(zhǔn)一般被稱為JSR(Java Specification Request),特別是嵌入式平臺(tái)亚斋,針對(duì)帶不帶屏幕作媚、屏幕大小,運(yùn)算能力帅刊,都定義了詳細(xì)而復(fù)雜的標(biāo)準(zhǔn)纸泡,符合了這些標(biāo)準(zhǔn)的虛擬機(jī)應(yīng)該會(huì)提供某種能力,從而保證符合同一標(biāo)準(zhǔn)的應(yīng)用程序得以正常執(zhí)行赖瞒。我們的JAVA ME女揭,就是這樣的產(chǎn)物蚤假,在比較長(zhǎng)周期內(nèi),因?yàn)闆]有可選編程方案吧兔,JAVA ME在諸多領(lǐng)域里都成為了工業(yè)標(biāo)準(zhǔn)磷仰,但性能不佳,實(shí)用性較差境蔼。
JAVA ME之所以性能會(huì)不佳的一個(gè)重要原因灶平,是它只是一種規(guī)范,作為規(guī)范的東西箍土,則需要考慮到不同平臺(tái)資源上的不同逢享,不容易追求極致,而且在長(zhǎng)期的開發(fā)與使用的歷史里吴藻,一些歷史上的接口瞒爬,也成為了進(jìn)一步提升的負(fù)擔(dān)。Android則不一樣沟堡,它是一個(gè)新生事物侧但,它不需要遵守任何標(biāo)準(zhǔn),即使它能夠提供JAVA ME兼容弦叶,它能得到資源回報(bào)也不會(huì)大俊犯,而且會(huì)帶來(lái)JAVA ME的授權(quán)費(fèi)用。于是伤哺,Android在設(shè)計(jì)上就采取了另一次的反Java設(shè)計(jì),不兼容任何Java標(biāo)準(zhǔn)者祖,而只以Android的API作為其兼容性的基礎(chǔ)立莉,這樣就沒有了歷史包袱,可以輕裝上陣進(jìn)行開發(fā)七问,也大大減小了維護(hù)的工作量蜓耻。作為一個(gè)Java寫的操作系統(tǒng),但又不兼容任何Java標(biāo)準(zhǔn)械巡,這貌似是比較諷刺的刹淌,但我們?cè)谡麄€(gè)行業(yè)內(nèi)四顧一下,大家應(yīng)該都會(huì)發(fā)現(xiàn)這樣一種特色讥耗,所有不支持JAVA ME標(biāo)準(zhǔn)的系統(tǒng)有勾,都發(fā)展得很好,而支持JAVA ME標(biāo)準(zhǔn)古程,則多被時(shí)代所淘汰蔼卡。這不能說(shuō)JAVA ME有多大的缺陷,或是晦氣太重挣磨,只不過靠支持JAVA ME來(lái)提供有限開發(fā)能力的系統(tǒng)雇逞,的確也會(huì)受限于可開發(fā)能力荤懂,無(wú)法走得太遠(yuǎn)罷了。
這樣會(huì)不會(huì)導(dǎo)致Java寫的代碼在Android環(huán)境里運(yùn)行不起來(lái)呢塘砸?理論上來(lái)說(shuō)不會(huì)节仿。如果是一些Java寫的通用算法,因?yàn)橹簧婕罢Z(yǔ)言本身掉蔬,不存在問題廊宪。如果是代碼里涉及一些基本的IO、網(wǎng)絡(luò)等通用操作眉踱,Android也使用了Apache組織的Harmony的Java IO庫(kù)實(shí)現(xiàn)挤忙,也不會(huì)有不兼容性。唯一不能兼容的是一些Java規(guī)范里的特殊代碼谈喳,像圖形接口册烈、窗口、Swing等方面的代碼婿禽。而我們?cè)贏ndroid系統(tǒng)里編程赏僧,最好也可以把算法與界面層代碼分離,這樣可以增加代碼復(fù)用性扭倾,也可以保證在UI編程上淀零,保持跟Android系統(tǒng)的兼容性。
Android的版本有兩層作用膛壹,一是描述系統(tǒng)某一個(gè)階段性的軟硬件功能驾中,另外就是用于界定API的規(guī)范。描述功能的作用模聋,更多地用于宣傳肩民,用于說(shuō)該版本的Android是個(gè)什么東西,也就是我們常見的食物版本號(hào)链方,像éclair(2.0持痰,2.1),F(xiàn)royo(2.2)祟蚀, Gingerbread(2.3)工窍,Icecream Sandswich(4.0),Jelly Bean(4.1)前酿,大家都可以通過這些美味的版本號(hào)患雏,了解Android這個(gè)版本有什么功能,有趣而易于宣傳薪者。而對(duì)于這樣的版本號(hào)纵苛,實(shí)際上也意味著API接口上的升級(jí),會(huì)增加或是改變一些接口打瘪。
所謂的API版本幼苛,是位于應(yīng)用程序?qū)优cFramework層之間的一層接口層丰泊,如下所示:
應(yīng)用程序只通過AndroidAPI來(lái)對(duì)下進(jìn)行訪問蝎亚,而我們每一個(gè)版本的Android系統(tǒng)滨巴,都會(huì)通過Framework來(lái)對(duì)上實(shí)現(xiàn)一套完整的API接口蝉仇,提供給應(yīng)用程序訪問惠遏。只要上下兩層這種調(diào)用與被調(diào)用的需求能夠在一定范圍內(nèi)合拍机打,應(yīng)用程序所需要的最低API版本低于Framework所提供的版本蓬坡,這時(shí)應(yīng)用程序就可以正常執(zhí)行猿棉。從這個(gè)意義來(lái)說(shuō),API的版本屑咳,更大程度算是Android Framework實(shí)現(xiàn)上的版本萨赁,而Android系統(tǒng),我們也可以看成就是Android的Framework層兆龙。
這種機(jī)制在Android發(fā)展過程中一直實(shí)施得很好杖爽,直到Android 2.3,都保持了向前發(fā)展紫皇,同時(shí)也保持向后兼容慰安。Android 2.3也是Android歷史上的一個(gè)里程碑,一臺(tái)智能手機(jī)所應(yīng)該實(shí)現(xiàn)的功能聪铺,Android2.3都基本上完成了化焕。但這時(shí)又出現(xiàn)了平板(pad)的系統(tǒng)需求,從2.3又發(fā)展出一個(gè)跟手機(jī)平臺(tái)不兼容的3.0铃剔,然后這兩個(gè)版本再到4.0進(jìn)行融合撒桨。在2.3到4..0,因?yàn)檫\(yùn)行機(jī)制都有大的變動(dòng)键兜,于是這樣的兼容性遇到了一定的挑戰(zhàn)元莫,現(xiàn)在還是無(wú)法實(shí)現(xiàn)100%的從4.0到2.3的兼容。
只兼容自己API蝶押,是Android系統(tǒng)自信的一種體現(xiàn),同時(shí)火欧,也給它帶來(lái)另一個(gè)好處棋电,那就是可以大量使用JNI加速。
大量使用JNI加速
JNI苇侵,全稱是Java本地化接口層(Java Native Interface)赶盔,就是通過給Java虛擬機(jī)加動(dòng)態(tài)鏈接庫(kù)插件的方式,將一些Java環(huán)境原本不支持功能加入到系統(tǒng)中榆浓。
我們前面說(shuō)到過Dalvik虛擬機(jī)在JIT實(shí)現(xiàn)上有缺陷于未,這點(diǎn)在Android設(shè)計(jì)人員的演示說(shuō)明里,被狡猾地掩蓋了。他們說(shuō)烘浦,在Android編程里抖坪,JIT不是必須的,Android在2.2之前都不提供JIT支持闷叉,理由是應(yīng)用程序不可能太復(fù)雜擦俐,同時(shí)Android本身是沒有必要使用JIT,因?yàn)橄到y(tǒng)里大部分功能是通過JNI來(lái)調(diào)用機(jī)器代碼(Native代碼)來(lái)實(shí)現(xiàn)的握侧。這點(diǎn)也體現(xiàn)了Android設(shè)計(jì)人員蚯瞧,作為技術(shù)狂熱者的可愛之處,類似這樣的錯(cuò)誤還不少品擎,比如Android剛開始的設(shè)計(jì)初衷是要改變大家編程的習(xí)慣埋合,要通過Android應(yīng)用程序在概念上的封裝,去除掉進(jìn)程萄传、線程這樣底層的概念甚颂;甚至他們還定義了一套工具,希望大家可以像玩積木一樣盲再,在圖形界面里拖拉一下西设,在完全沒有編程背景的情況下也可以編程;等等答朋。這些錯(cuò)誤當(dāng)然也被Android強(qiáng)大的開源社區(qū)所改正了贷揽。但對(duì)于Android系統(tǒng)性能是由大量JNI來(lái)推進(jìn)的,這點(diǎn)診斷倒沒有錯(cuò)梦碗,Android發(fā)展也一直順著這個(gè)方向在走禽绪。
Android系統(tǒng)實(shí)現(xiàn)里,大量使用JNI進(jìn)行優(yōu)化洪规,這也是一個(gè)很大的反Java舉動(dòng)印屁,在Java的世界里,為了保持在各個(gè)環(huán)境的兼容性斩例,除了Java虛擬機(jī)這個(gè)必須與底層操作系統(tǒng)打交道的執(zhí)行實(shí)體雄人,以及一些無(wú)法繞開底層限制的IO接口,Java環(huán)境的所有代碼念赶,都盡可能使用Java語(yǔ)言來(lái)編寫础钠。通過這種方式,可以有效減小平臺(tái)間差異所引發(fā)的不兼容叉谜。在Java虛擬機(jī)的開發(fā)文檔里旗吁,有詳盡的JNI編程說(shuō)明,同時(shí)也強(qiáng)烈建議停局,這樣的編程接口是需要避免使用的很钓,使用了JNI香府,則會(huì)跟底層打上交道,這時(shí)就需要每個(gè)體系構(gòu)架码倦,每個(gè)不同操作系統(tǒng)都提供實(shí)現(xiàn)企孩,并每種情況下都需要編譯一次,維護(hù)的代價(jià)會(huì)過高叹洲。
但Android則是另一個(gè)情況柠硕,它只是借用Java編程語(yǔ)言,應(yīng)用程序使用的只是Android API接口运提,不存在平臺(tái)差異性問題蝗柔。比如我們要把一個(gè)Android環(huán)境運(yùn)行在不那么流行的MIPS平臺(tái)之上,我們需要的JNI民泵,是Android源代碼在MIPS構(gòu)架上實(shí)現(xiàn)并編譯出來(lái)的結(jié)果癣丧,只要API兼容,則就不存在平臺(tái)差異性栈妆。對(duì)于系統(tǒng)構(gòu)架層次來(lái)說(shuō)胁编,使用JNI造成的差異性,在Framework層里鳞尔,就已經(jīng)被屏蔽掉了:
如上圖所示嬉橙,Android應(yīng)用程序,只知道有Framework層寥假,通過API接口與Framework通信市框。而我們底層,在Library層里糕韧,我們就可以使用大量的JNI枫振,我們只需要在Framework向上的部分保持統(tǒng)一的接口就可以了。雖然我們的Library層在每個(gè)平臺(tái)上都需要重新編譯(有些部分可能還需要重新實(shí)現(xiàn))萤彩,但這是平臺(tái)產(chǎn)商或是硬件產(chǎn)商的工作粪滤,應(yīng)用程序開發(fā)者只需要針對(duì)API版本寫代碼,而不需要關(guān)心這種差異性雀扶。于是杖小,我們即得到高效的性能(與純用機(jī)器實(shí)現(xiàn)的軟件系統(tǒng)沒有多少性能差異),以使用到Java的一些高級(jí)特性愚墓。
單進(jìn)程虛擬機(jī)
使用單進(jìn)程虛擬機(jī)窍侧,是Android整個(gè)設(shè)計(jì)方案里反Java的又一表現(xiàn)。我們前面提到了转绷,如果在Android系統(tǒng)里,要構(gòu)建一種安全無(wú)憂的應(yīng)用程序加載環(huán)境硼啤,這時(shí)议经,我們需要的是一種以進(jìn)程為單位的“沙盒(Sandbox)”模型斧账。在實(shí)現(xiàn)這種模型時(shí),我們可以有多種選擇煞肾,如果是遵循Java原則的話咧织,我們的設(shè)計(jì)應(yīng)該是這個(gè)樣子的:
我們運(yùn)行起Java環(huán)境,然后再在這個(gè)環(huán)境里構(gòu)建應(yīng)用程序的基于進(jìn)程的“小牢房”籍救。按照傳統(tǒng)的計(jì)算機(jī)理論习绢,或是從資源有效的角度考慮,特別是如果需要使用棧式的標(biāo)準(zhǔn)Java虛擬機(jī)蝙昙,這些都是沒話說(shuō)的闪萄,只有這種構(gòu)建方式才最優(yōu)。
Java虛擬機(jī)奇颠,之所以被稱為虛擬機(jī)败去,是因?yàn)樗嫱ㄟ^一個(gè)用戶態(tài)的虛擬機(jī)進(jìn)程虛擬出了一個(gè)虛擬的計(jì)算機(jī)環(huán)境,是真正意義上的虛擬機(jī)烈拒。在這個(gè)環(huán)境里圆裕,執(zhí)行.class寫出來(lái)的偽代碼,這個(gè)世界里的一切都是由對(duì)象構(gòu)成的荆几,支持進(jìn)程吓妆,信號(hào),Stream形式訪問的文件等一切本該是實(shí)際操作系統(tǒng)所支持的功能吨铸。這樣就抽象出來(lái)一個(gè)跟任何平臺(tái)無(wú)關(guān)的Java世界行拢。如果在這個(gè)Java虛擬世界時(shí)打造“沙盒”模式,則只能使用同一個(gè)Java虛擬機(jī)環(huán)境(這不光是進(jìn)程焊傅,因?yàn)镴ava虛擬機(jī)內(nèi)部還可以再創(chuàng)建出進(jìn)程)剂陡,這樣就可以通過統(tǒng)一的垃圾收集器進(jìn)行有效的對(duì)象管理,同時(shí)狐胎,多進(jìn)程則內(nèi)存有可能需要在多個(gè)進(jìn)程空間里進(jìn)行復(fù)制鸭栖,在使用同一個(gè)Java虛擬機(jī)實(shí)例里,才有可能通過對(duì)象引用減小復(fù)制握巢,不使用統(tǒng)一的Java虛擬機(jī)環(huán)境管理晕鹊,則可復(fù)用性就會(huì)很低。
但這種方案暴浦,存在一些不容易解決的問題溅话。這種單Java虛擬機(jī)環(huán)境的假設(shè),是建立在標(biāo)準(zhǔn)Java虛擬機(jī)之上的歌焦,但如前面所說(shuō)飞几,這樣的選擇困難重重,于是Android是使用Dalvik虛擬機(jī)独撇。這種單虛擬機(jī)實(shí)例設(shè)計(jì)屑墨,需要一個(gè)極其強(qiáng)大穩(wěn)定的虛擬機(jī)實(shí)現(xiàn)躁锁,而我們的Dalvik虛擬機(jī)未必可以實(shí)現(xiàn)得如此功能復(fù)雜同時(shí)又能保證穩(wěn)定性(簡(jiǎn)單穩(wěn)定容易,復(fù)雜穩(wěn)定則難)卵史。Android必須要使用大量的JNI開發(fā)战转,于是會(huì)進(jìn)一步破壞虛擬機(jī)的穩(wěn)定性,如果系統(tǒng)里只有一個(gè)虛擬機(jī)實(shí)例以躯,則這個(gè)實(shí)例將會(huì)非常脆弱槐秧。當(dāng)在Java環(huán)境里的進(jìn)程,有惡意代碼或是實(shí)現(xiàn)不當(dāng)忧设,有可能破壞虛擬機(jī)環(huán)境刁标,這時(shí),我們只能靠重啟虛擬機(jī)來(lái)完成恢復(fù)见转,這時(shí)會(huì)影響到虛擬機(jī)里運(yùn)行的其他進(jìn)程命雀,失去了“沙盒”的意義。最后斩箫,虛擬機(jī)必須給預(yù)足夠的權(quán)限運(yùn)行吏砂,才能保證核心進(jìn)程可訪問硬件資源,則權(quán)限控制有可能被某個(gè)惡意應(yīng)用程序破壞乘客,從而失去對(duì)系統(tǒng)資源的保護(hù)狐血。
Android既然使用是非標(biāo)準(zhǔn)的Dalvik虛擬機(jī),我們就可以繼續(xù)在反Java的道路上嘗試得更遠(yuǎn)易核,于是匈织,我們得到的是Android里的單進(jìn)程虛擬機(jī)模型。在基于Dalvik虛擬機(jī)的方案里牡直,虛擬機(jī)的作用退回到了解析器的階段缀匕,并不再是一個(gè)完整的虛擬機(jī),而只是進(jìn)程中一個(gè)用于解析.dex偽代碼的解析執(zhí)行工具:
在這種沙盒模式里碰逸,每個(gè)進(jìn)程都會(huì)執(zhí)行起一個(gè)Dalvik虛擬機(jī)實(shí)例乡小,應(yīng)用程序在編程上,只能在這個(gè)受限的饵史,以進(jìn)程為單位的虛擬機(jī)實(shí)例里執(zhí)行满钟,任何出錯(cuò),也只影響到這個(gè)應(yīng)用程序的宿主進(jìn)程本身胳喷,對(duì)系統(tǒng)湃番,對(duì)其他進(jìn)程都沒有嚴(yán)重影響。這種單進(jìn)程的虛擬機(jī)吭露,只有當(dāng)這個(gè)應(yīng)用程序被調(diào)用到時(shí)才予以創(chuàng)建吠撮,也不存在什么需要重啟的問題,出了錯(cuò)讲竿,殺掉出錯(cuò)進(jìn)程纬向,再創(chuàng)建一個(gè)新的進(jìn)程即可择浊。基于uid/gid的權(quán)限控制逾条,在虛擬機(jī)之外的實(shí)現(xiàn),應(yīng)用程序完全不可能通過Java代碼來(lái)破壞這種操作系統(tǒng)級(jí)別的權(quán)限控制投剥,于是保護(hù)了系統(tǒng)师脂。這時(shí),我們的系統(tǒng)設(shè)計(jì)上反有了更高的靈活度江锨,我們可以放心大膽地使用JNI開發(fā)吃警,同時(shí)核心進(jìn)程也有可能是直接通過C/C++寫出來(lái)的本地化代碼來(lái)實(shí)現(xiàn),再通過JNI提供給Dalvik環(huán)境啄育。而由于這時(shí)酌心,由于我們降低了虛擬機(jī)在設(shè)計(jì)上的復(fù)雜程序,這時(shí)我們的執(zhí)行性能必然會(huì)更好挑豌,更容易被優(yōu)化安券。
當(dāng)然,這種單進(jìn)程虛擬機(jī)設(shè)計(jì)氓英,在運(yùn)行上也會(huì)帶來(lái)一些問題侯勉,比如以進(jìn)程以單位進(jìn)行GC,數(shù)據(jù)必然在每個(gè)進(jìn)程里都進(jìn)行復(fù)制铝阐,而進(jìn)程創(chuàng)建也是有開銷的址貌,造成程序啟動(dòng)緩慢,在跨進(jìn)程的Intent調(diào)用時(shí)徘键,嚴(yán)重影響用戶體驗(yàn)练对。Java環(huán)境里的GC是標(biāo)準(zhǔn)的,這方面的開銷倒是沒法繞開吹害,所以Android應(yīng)用程序編程優(yōu)化里的重要一招就是減小對(duì)象使用螟凭,繞開GC。但數(shù)據(jù)復(fù)制造成的冗余赠制,以及進(jìn)程創(chuàng)建的開銷則可以進(jìn)行精減赂摆,我們來(lái)看看Android如何解決這樣的問題。
盡可能共享內(nèi)存
在幾乎所有的Unix進(jìn)程管理模型里钟些,都使用延時(shí)分配來(lái)處理代碼的加載烟号,從而達(dá)到減小內(nèi)存使用的作用,Linux內(nèi)核也不例外政恍。所謂的進(jìn)程汪拥,在Linux內(nèi)核里只是帶mm(虛存映射)的task_struct而已,而所謂的進(jìn)程創(chuàng)建篙耗,就是通過fork()系統(tǒng)調(diào)用來(lái)創(chuàng)建一個(gè)進(jìn)程迫筑,而在新創(chuàng)建的進(jìn)程里使用execve()系列的系統(tǒng)調(diào)用來(lái)執(zhí)行新的代碼宪赶。這兩個(gè)步驟是分兩步進(jìn)行,父進(jìn)程調(diào)用fork()脯燃,子進(jìn)程里調(diào)用execve():
上圖的實(shí)線代表了函數(shù)調(diào)用搂妻,虛線代碼內(nèi)存引用。在創(chuàng)建一個(gè)進(jìn)程執(zhí)行某些代碼時(shí)辕棚,一個(gè)進(jìn)程會(huì)調(diào)用fork()欲主,這個(gè)fork()會(huì)通過libc,通過系統(tǒng)調(diào)用逝嚎,轉(zhuǎn)入內(nèi)核實(shí)現(xiàn)的sys_fork()扁瓢。然后在sys_fork()實(shí)現(xiàn)里,這時(shí)就會(huì)創(chuàng)建新的task_struct补君,也就是新的進(jìn)程空間引几,形成父子進(jìn)程關(guān)系。但是挽铁,這時(shí)伟桅,兩個(gè)進(jìn)程使用同一個(gè)進(jìn)程空間。當(dāng)被創(chuàng)建的子進(jìn)程里屿储,自己主動(dòng)地調(diào)用了execve()系列的函數(shù)之后贿讹,這時(shí)才會(huì)去通過內(nèi)核的sys_execve()去嘗試解析和加載所要執(zhí)行的文件,比如a.out文件够掠,驗(yàn)證權(quán)限并加載成功之后民褂,這時(shí)才會(huì)建立起新的虛存映射(mm),但此時(shí)雖然子進(jìn)程有了自己獨(dú)立的進(jìn)程空間疯潭,并不會(huì)分配實(shí)際的物理內(nèi)存赊堪。于是有了自己的進(jìn)程空間,當(dāng)下次執(zhí)行到時(shí)竖哩,才會(huì)通過一次缺頁(yè)中斷加載a.out的代碼段哭廉,數(shù)據(jù)段,而此時(shí)相叁,libc.so因?yàn)閮蓚€(gè)進(jìn)程都需要使用遵绰,于是會(huì)直接通過一次內(nèi)存映射來(lái)完成。
通過Linux的進(jìn)程創(chuàng)建增淹,我們可以看到椿访,進(jìn)程之間雖然有獨(dú)立的空間,但進(jìn)程之間會(huì)大量地通過頁(yè)面映射來(lái)實(shí)現(xiàn)內(nèi)存頁(yè)的共享虑润,從而減小內(nèi)存的使用成玫。雖然在代碼執(zhí)行過程中都會(huì)形成它自己的進(jìn)程空間,有各自獨(dú)立的內(nèi)存類,但對(duì)于可執(zhí)行文件哭当、動(dòng)態(tài)鏈接庫(kù)等這些靜態(tài)資源猪腕,則在進(jìn)程之間會(huì)通過頁(yè)面映射進(jìn)行共享進(jìn)行共享。于是钦勘,可以得到的解決思路陋葡,就是如何加強(qiáng)頁(yè)面的共享。
加強(qiáng)共享的簡(jiǎn)單一點(diǎn)的思路彻采,就是人為地將所有可能使用到的動(dòng)態(tài)鏈接庫(kù).so文件脖岛,dalvik虛擬機(jī)的執(zhí)行文件,都通過強(qiáng)制讀一次颊亮,于是物理內(nèi)存里便有了存放這些文件內(nèi)容的內(nèi)存頁(yè),其他部分則可以通過mmap()來(lái)借用這些被預(yù)加載過的內(nèi)存頁(yè)陨溅。于是终惑,當(dāng)我們的用戶態(tài)進(jìn)程被執(zhí)行時(shí),雖然還是同樣的執(zhí)行流程门扇,但因?yàn)閮?nèi)存里面有了所需要的虛擬機(jī)環(huán)境的物理頁(yè)雹有,這時(shí)缺頁(yè)中斷則只是進(jìn)行一次頁(yè)面映射,不需要讀文件臼寄,非嘲赞龋快就返回了,同時(shí)由于頁(yè)面映射只是對(duì)內(nèi)存頁(yè)的引用吉拳,這種共享也減小實(shí)際物理頁(yè)的使用质帅。我們將上面的fork()處理人為地改進(jìn)一下,就可以使用如下的模式:
這時(shí)留攒,對(duì)于任一應(yīng)用程序煤惩,在dalvik開始執(zhí)行前,它所需要的物理頁(yè)就都已經(jīng)存在了炼邀,對(duì)于非系統(tǒng)進(jìn)程的應(yīng)用程序而言魄揉,它所需要使用的Framework提供的功能、動(dòng)態(tài)鏈接庫(kù)拭宁,都不會(huì)從文件系統(tǒng)里再次讀取洛退,而只需要通過page_fault觸發(fā)一次頁(yè)面映射,這時(shí)就可以大大提供加載時(shí)的性能杰标。然后便是開始執(zhí)行dalvik虛擬兵怯,解析.dex文件來(lái)執(zhí)行應(yīng)用程序的獨(dú)特實(shí)現(xiàn),當(dāng)然在旱,每個(gè)classes.dex文件的內(nèi)容則是需要各自獨(dú)立地進(jìn)行加載摇零。我們可以從.dex文件的解析入手,進(jìn)一步加強(qiáng)內(nèi)存使用驻仅。
Android環(huán)境里谅畅,會(huì)使用dx工具,將.class文件翻譯成.dex文件噪服,.dex文件與.class文件毡泻,不光是偽指令不同,它們的文件格式也完全不同粘优,從而達(dá)到加強(qiáng)共享的目的仇味。標(biāo)準(zhǔn)的Java一般使用.jar文件來(lái)包裝一個(gè)軟件包,在這個(gè)軟件里會(huì)是以目錄結(jié)構(gòu)組織的.class文件雹顺,比如org/lianlab/hello/Hello.class這樣的形式丹墨。這種格式需要在運(yùn)行時(shí)進(jìn)行解壓,需要進(jìn)行目錄結(jié)構(gòu)的檢索嬉愧,還會(huì)因?yàn)?class文件里分散的定義贩挣,無(wú)法高效地加載。而在.dex文件里没酣,所有.class文件里實(shí)現(xiàn)的內(nèi)容王财,會(huì)合并到一個(gè).dex文件里,然后把每個(gè).class文件里的信息提取出來(lái)裕便,放到同一個(gè)段位里绒净,以便通過內(nèi)存映射的方式加速文件的操作與加載。
這時(shí)偿衰,我們的各個(gè)不同的.class文件里內(nèi)容被檢索并合并到同一個(gè)文件里挂疆,這里得到的.dex文件,有特定情況下會(huì)比壓縮過的.jar文件還要小哎垦,因?yàn)榇藭r(shí)可以合并不同.class文件里的重復(fù)定義囱嫩。這樣,在可以通過內(nèi)存映射來(lái)加速的基礎(chǔ)上漏设,也從側(cè)面降低了內(nèi)存的使用墨闲,比如用于.class的文件系統(tǒng)開銷得到減小,用于加載單個(gè).class文件的開銷也得以減小郑口,于是得到了加速的目的鸳碧。
這還不是全部,需要知道犬性,我們的dalvik不光是一個(gè)可執(zhí)行的ELF文件而已瞻离,還是Java語(yǔ)言的一個(gè)解析器,這時(shí)勢(shì)必需要一些額外的.class文件(當(dāng)然乒裆,在Android環(huán)境里套利,因?yàn)槭褂昧伺cJava虛擬機(jī)不兼容的Dalvik虛擬機(jī),這樣的.class文件也會(huì)被翻譯成.dex文件)里提供的內(nèi)容,這些額外的文件主要就是Framework的實(shí)現(xiàn)部分肉迫,還有由Harmony提供的一些Java語(yǔ)言的基本類验辞。還不止于此,作為一個(gè)系統(tǒng)環(huán)境喊衫,一些特定的圖標(biāo)跌造,UI的一些控件資源文件,也都會(huì)在執(zhí)行過程里不斷被用到族购,最好我們也能實(shí)現(xiàn)這部分的預(yù)先加載壳贪。出于這樣的目的,我們又會(huì)面臨前面的兩難選擇寝杖,改內(nèi)核的page_fault處理违施,還是自己設(shè)計(jì)。出于設(shè)計(jì)上的可移植性角度考慮瑟幕,還是改設(shè)計(jì)吧醉拓。這時(shí),就可以得到Android里的第一個(gè)系統(tǒng)進(jìn)程設(shè)計(jì)收苏,Zygote。
我們這時(shí)對(duì)于Zygote的需求是愤兵,能夠?qū)崿F(xiàn)動(dòng)態(tài)鏈接庫(kù)鹿霸、Dalvik執(zhí)行進(jìn)程的共享,同時(shí)它最好能實(shí)現(xiàn)一些Java環(huán)境里的庫(kù)文件的預(yù)加載秆乳,以及一些資源文件的加載懦鼠。出于這樣的目的,我們得到了Zygote實(shí)現(xiàn)的雛形:
這時(shí)屹堰,Zygote基本上可以滿足我們的需求肛冶,可以加載我們運(yùn)行一個(gè)應(yīng)用程序進(jìn)程除了classes.dex之外的所有資源,而我們前面也看到.dex這種文件格式本身也被優(yōu)化過扯键,于是對(duì)于頁(yè)面共享上的優(yōu)化基本上得以完成了睦袖。我們之后的操作完全可以依賴于zygote進(jìn)程,以后的設(shè)計(jì)里荣刑,我們就把所有的需要特權(quán)的服務(wù)都在zygote進(jìn)程里實(shí)現(xiàn)就好了馅笙。
有了zygote進(jìn)程則我們解決掉了共享的問題,但如果把所有的功能部分都放在Zygote進(jìn)程里厉亏,則過猶不及董习,這樣的做法反而更不合適。Zygote則創(chuàng)建應(yīng)用程序進(jìn)程并共享應(yīng)用程序程序所需要的頁(yè)爱只,而并非所有的內(nèi)存頁(yè)皿淋,我們的系統(tǒng)進(jìn)程執(zhí)行的絕大部分內(nèi)容是應(yīng)用程序所不需要的,所以沒必要共享。共享之后還會(huì)帶來(lái)潛在問題窝趣,影響應(yīng)用程序的可用進(jìn)程空間疯暑,另外惡意應(yīng)用程序則可以取得我們系統(tǒng)進(jìn)程的實(shí)現(xiàn)細(xì)節(jié),反而使我們的辛辛苦苦構(gòu)建的“沙盒”失效了高帖。
Zygote缰儿,英文愿意是“孵化器”的意思,既然是這種名字散址,我們就可以在設(shè)計(jì)上盡可能保持其簡(jiǎn)單性乖阵,只做孵化這么最簡(jiǎn)單的工作,更符合我們目前的需求预麸。但是還有一個(gè)實(shí)現(xiàn)上的小細(xì)節(jié)瞪浸,我們是不是期望zygote通過fork()創(chuàng)建進(jìn)程之后,每個(gè)應(yīng)用程序自己去調(diào)用exec()來(lái)加載dalvik虛擬機(jī)呢吏祸?這樣實(shí)現(xiàn)也不合理对蒲,實(shí)現(xiàn)上很丑陋,還不安全贡翘,一旦惡意應(yīng)用程序不停地調(diào)用到zygote創(chuàng)建進(jìn)程蹈矮,這時(shí)系統(tǒng)還是會(huì)由于創(chuàng)建進(jìn)程造成的開銷而耗盡內(nèi)存,這時(shí)系統(tǒng)也還是很脆弱的鸣驱。這些應(yīng)該是由系統(tǒng)進(jìn)程來(lái)完成的泛鸟,這個(gè)系統(tǒng)進(jìn)程應(yīng)該也需要兼職負(fù)責(zé)Intent的分發(fā)。當(dāng)有Intent發(fā)送到某個(gè)應(yīng)用程序踊东,而這個(gè)應(yīng)用程序并沒有被運(yùn)行起來(lái)時(shí)北滥,這時(shí),這個(gè)系統(tǒng)進(jìn)程應(yīng)該發(fā)一個(gè)請(qǐng)求到Zygote創(chuàng)建虛擬機(jī)進(jìn)程闸翅,然后再通過系統(tǒng)進(jìn)程來(lái)驅(qū)動(dòng)應(yīng)用程序具體做怎么樣的操作再芋,這時(shí),我們的Android的系統(tǒng)構(gòu)架就基本上就緒了。在Android環(huán)境里,系統(tǒng)進(jìn)程就是我們的System Server纹坐,它是我們系統(tǒng)里,通過init腳本創(chuàng)建的第一個(gè)Dalvik進(jìn)程联喘,也就是說(shuō)Android系統(tǒng),本就是構(gòu)建在Dalvik虛擬機(jī)之上的辙纬。
在SystemServer里豁遭,會(huì)實(shí)現(xiàn)ActivityManager,來(lái)實(shí)現(xiàn)對(duì)Activity贺拣、Service等應(yīng)用程序執(zhí)行實(shí)體的管理蓖谢,分發(fā)Intent捂蕴,并維護(hù)這些實(shí)體生命周期(比如Activity的棧式管理)。最終闪幽,在Android系統(tǒng)里啥辨,最終會(huì)有3個(gè)進(jìn)程,一個(gè)只負(fù)責(zé)進(jìn)程創(chuàng)建以提供頁(yè)面共享盯腌,一個(gè)用戶應(yīng)用程序進(jìn)程溉知,和我們實(shí)現(xiàn)一些系統(tǒng)級(jí)權(quán)限才能完成的特殊功能的SystemServer進(jìn)程。在這3種進(jìn)程的交互之下腕够,我們的系統(tǒng)會(huì)堅(jiān)固级乍,我們不會(huì)盲目地創(chuàng)建進(jìn)程,因?yàn)閼?yīng)用程序完全不知道有進(jìn)程這回事帚湘,它只會(huì)像調(diào)用函數(shù)那樣玫荣,調(diào)用一個(gè)個(gè)實(shí)現(xiàn)具體功能的Activity,我們?cè)谕瓿蓛?nèi)存頁(yè)共享難題的同時(shí)大诸,也完成Android系統(tǒng)設(shè)計(jì)的整體思路捅厂。
這時(shí)對(duì)于應(yīng)用程序處理上,還剩下最后一個(gè)問題资柔,如果加快應(yīng)用程序的加載焙贷。
應(yīng)用程序進(jìn)程“永不退出”
雖然我們擁有了內(nèi)存頁(yè)的預(yù)加載實(shí)現(xiàn),但這還是無(wú)法保證Android應(yīng)用程序執(zhí)行上的高效性的贿堰。根據(jù)到現(xiàn)在為此我們分析到的Android應(yīng)用程序支持盈厘,我們?cè)谶@方面必將面臨挑戰(zhàn)。像Activity之間進(jìn)行跳轉(zhuǎn)官边,我們?nèi)绻幚硖D(zhuǎn)出的Activity所依附的那個(gè)進(jìn)程呢?直接殺死掉外遇,這時(shí)注簿,當(dāng)我們從被調(diào)用Activity返回時(shí)怎么辦?
這也會(huì)是個(gè)比較復(fù)雜的問題跳仿。一是前一個(gè)進(jìn)程的狀態(tài)如何處理诡渴,二是我們又如何對(duì)待上一個(gè)已經(jīng)暫時(shí)退出執(zhí)行的進(jìn)程。
我們老式的應(yīng)用程序是不存在這樣的問題的菲语,因?yàn)樗痪邆淇邕M(jìn)程交互的能力妄辩,唯一的有可能進(jìn)行跨進(jìn)程交互的方式是在應(yīng)用程序之間進(jìn)行復(fù)制/粘貼操作。而對(duì)于進(jìn)程內(nèi)部的界面之間的切換山上,實(shí)際上只會(huì)發(fā)生在同一個(gè)While循環(huán)里面眼耀,一旦退出某一個(gè)界面,則相應(yīng)的代碼都不會(huì)被執(zhí)行到佩憾,直到處理完成再返回原始界面:
而這種界面模型哮伟,在Android世界里干花,只是一個(gè)UI線程所需要完成的工作,跟界面交互倒并不相關(guān)楞黄。我們的Android 在界面上進(jìn)行交互池凄,實(shí)際上是在Activity之間進(jìn)行切換,而每個(gè)進(jìn)程內(nèi)部再維護(hù)一套上述的UI循環(huán)體:
在這樣的運(yùn)行模式下鬼廓,如果我們退出了某一個(gè)界面的執(zhí)行肿仑,則沒有必要再維持其運(yùn)行,我們可以通過特殊的設(shè)計(jì)使其退出執(zhí)行碎税。但這種調(diào)用是無(wú)論處理完尤慰,還是中途取消,我們還是會(huì)回到上一個(gè)界面蚣录,如果要達(dá)到一體化看上去像同一個(gè)應(yīng)用程序的效果割择,這里我們需要恢復(fù)上一個(gè)界面的狀態(tài)。比如我們例子里萎河,我們打了聯(lián)系列表選擇了某個(gè)聯(lián)系人荔泳,然后通過Gallery設(shè)置大頭貼,再返回到聯(lián)系人列表時(shí)虐杯,一定要回到我們正在編譯聯(lián)系人的界面里玛歌。如果這時(shí)承載聯(lián)系人列表的進(jìn)程已經(jīng)退出了話,我們將要使整個(gè)操作重做一次擎椰,很低效支子。
所以綜合考慮,最好的方式居然會(huì)是偷懶达舒,對(duì)上個(gè)進(jìn)程完全不處理值朋,而需要提供一種暫停機(jī)制,可以讓不處理活躍交互狀態(tài)的進(jìn)程進(jìn)入暫停巩搏。當(dāng)我們返回時(shí)則直接可以到上次調(diào)用前的那個(gè)界面昨登,這時(shí)對(duì)用戶來(lái)說(shuō)很友好,在多個(gè)進(jìn)程間協(xié)作在用戶看來(lái)會(huì)是在同一個(gè)應(yīng)用程序進(jìn)行贯底,這才是Android設(shè)計(jì)的初衷丰辣。
因?yàn)獒槍?duì)需要暫停的處理,所以我們的應(yīng)用程序各個(gè)實(shí)體便有了生命周期禽捆,這種生命周期會(huì)隨著Android系統(tǒng)變得復(fù)雜而加入更多的生命周期的回調(diào)點(diǎn)笙什。但對(duì)于偷懶處理,則會(huì)有后遺癥胚想,如果應(yīng)用程序一直不退出琐凭,則對(duì)系統(tǒng)會(huì)是一個(gè)災(zāi)難。系統(tǒng)會(huì)因?yàn)閼?yīng)用程序不斷增加而耗盡資源浊服,最后會(huì)崩潰掉淘正。
不光Android會(huì)有這樣的問題的摆马,Linux也會(huì)有。我們一直都說(shuō)Linux內(nèi)核強(qiáng)勁安全鸿吆,但這也是相對(duì)的囤采,如果我們系統(tǒng)里有了一些流氓程序,也有可能通過耗盡資源的方式影響系統(tǒng)運(yùn)行惩淳。大家可以寫一些簡(jiǎn)單的例子做到這點(diǎn)蕉毯,比如:
while(1)
{
char * buf = malloc (30 * 1000);
memset (buf, ‘a(chǎn)’, 30*1000);
if (!fork() )
fork();
}
這時(shí)會(huì)發(fā)現(xiàn)系統(tǒng)還是會(huì)受到影響,但Linux的健壯性表現(xiàn)在思犁,雖然系統(tǒng)會(huì)暫時(shí)因?yàn)橘Y源不足而變得響應(yīng)遲緩代虾,但還是可以保證系統(tǒng)不會(huì)崩潰。為了進(jìn)程數(shù)過多而影響系統(tǒng)運(yùn)行激蹲,Linux內(nèi)核里有一種OOM Killer(Out Of Memory Killer)機(jī)制棉磨,系統(tǒng)里通過一種叫notifier的機(jī)制(顧名思義,跟我們的Listener設(shè)計(jì)模式類似的實(shí)現(xiàn))監(jiān)聽目前系統(tǒng)里內(nèi)存使用率学辱,當(dāng)內(nèi)存使用達(dá)到比率時(shí)乘瓤,就開始?xì)⒌粢恍┻M(jìn)程,回收內(nèi)存策泣,這里系統(tǒng)就可以回到正常執(zhí)行衙傀。當(dāng)然,在真正發(fā)生Out Of Memory錯(cuò)誤也會(huì)提前觸發(fā)這種殺死進(jìn)程的操作萨咕。
一旦發(fā)生OOM事件统抬,這時(shí)系統(tǒng)會(huì)通過一定規(guī)則殺死掉某種類型的進(jìn)程來(lái)回收內(nèi)存,所謂槍打出頭鳥危队,被殺的進(jìn)程應(yīng)該是能夠提供更多內(nèi)存回收機(jī)會(huì)的聪建,比如進(jìn)程空間很大、內(nèi)存共享性很小的茫陆。這種機(jī)制并不完全滿足Android需要金麸,如果剛好這個(gè)“出頭鳥”就是產(chǎn)生調(diào)用的進(jìn)程,或是系統(tǒng)進(jìn)程盅弛,這時(shí)反而會(huì)影響到Android系統(tǒng)的正常運(yùn)行。
這時(shí)叔锐,Android修改了Linux內(nèi)核里標(biāo)準(zhǔn)的OOM Killer挪鹏,取而代之是一個(gè)叫LowMemKiller的驅(qū)動(dòng),觸發(fā)Out Of Memory事件的不再是Linux內(nèi)核里的Notifier愉烙,而由Android系統(tǒng)進(jìn)程來(lái)驅(qū)動(dòng)讨盒。像我們前面說(shuō)明的,在Android里負(fù)責(zé)管理進(jìn)程生成與Activity調(diào)用棧的會(huì)是這個(gè)系統(tǒng)進(jìn)程步责,這樣在遇到系統(tǒng)內(nèi)存不夠(可以直接通過查詢空閑內(nèi)存來(lái)得到)時(shí)返顺,就觸發(fā)Low Memory Killer驅(qū)動(dòng)來(lái)殺死進(jìn)程來(lái)釋放內(nèi)存禀苦。
這種設(shè)計(jì),從我們感性認(rèn)識(shí)里也可以看到遂鹊,用adb shell free登錄到設(shè)備上查看空閑內(nèi)存振乏,這時(shí)都會(huì)發(fā)現(xiàn)的內(nèi)存的剩余量很低。因?yàn)樵贏ndroid設(shè)備里秉扑,系統(tǒng)里空閑內(nèi)存數(shù)量不低到一定的程度慧邮,是不會(huì)去回收內(nèi)存的,Android在內(nèi)存使用上舟陆,是“月光族”误澳。Android通過這種方式,讓盡可能多的應(yīng)用程序駐留在內(nèi)存里秦躯,從而達(dá)到一個(gè)加速執(zhí)行的目的忆谓。在這種模型時(shí),內(nèi)存相當(dāng)于一個(gè)我們TCP協(xié)議棧里的一個(gè)窗口踱承,盡可能多地進(jìn)行緩沖倡缠,而落到窗口之外的則會(huì)被舍棄。
理論上來(lái)說(shuō)勾扭,這是一種物盡其用毡琉,勤儉執(zhí)家的做法,這樣使Android系統(tǒng)保持運(yùn)行流暢妙色,而且從側(cè)面也刺激了Android設(shè)備使用更大內(nèi)存桅滋,因?yàn)閮?nèi)存越多則內(nèi)存池越大,可同時(shí)運(yùn)行的任務(wù)越多身辨,越流暢丐谋。唯一不足之處,一些試圖縮減Android內(nèi)存的廠商就顯得很無(wú)辜煌珊,精減內(nèi)存則有可能影響Android的使用體驗(yàn)号俐。
我們經(jīng)常會(huì)見到系統(tǒng)間的對(duì)比,說(shuō)Android是真實(shí)的多任務(wù)操作系統(tǒng)定庵,而其他手機(jī)操作平臺(tái)只是偽多任務(wù)的吏饿。這是實(shí)話,但這不是被Android作為優(yōu)點(diǎn)來(lái)設(shè)計(jì)的蔬浙,而只是整個(gè)系統(tǒng)設(shè)計(jì)迫使Android系統(tǒng)不得不使用這種設(shè)計(jì)猪落,來(lái)維持系統(tǒng)的流暢度。至于多任務(wù)畴博,這也是無(wú)心插柳柳成蔭的運(yùn)氣吧笨忌。
Pre-runtime運(yùn)算
在Android系統(tǒng)里,無(wú)論我們今天可以得到的硬件平臺(tái)是多么強(qiáng)大俱病,我們還是有降低系統(tǒng)里的運(yùn)算量的需求官疲。作為一個(gè)開源的手機(jī)解決方案袱结,我們不能假設(shè)系統(tǒng)具備多么強(qiáng)勁的運(yùn)算能力,出于成本的考慮途凫,也會(huì)有產(chǎn)商生產(chǎn)一些更廉價(jià)的低端設(shè)備垢夹。而即便是在一些高端硬件平臺(tái)之上,我們也不能浪費(fèi)手機(jī)上的運(yùn)算能力颖榜,因?yàn)槲覀兪芟抻谟邢薜碾姵毓╇娔芰ε锒>退闶菍?lái)這些限制都不存在,我們最好也還是減少不必要的損耗掩完,將計(jì)算能力花到最需要使用它們的地方噪漾。于是,我們?cè)谇懊嬲劦降母鞣N設(shè)計(jì)技巧之外且蓬,又增加了降低運(yùn)算量的需求欣硼。
這些技巧,貌似更高深恶阴,但實(shí)際上在Android之前的嵌入式Linux開發(fā)過程里诈胜,大家也被迫干過很多次了。主要的思路時(shí)冯事,所有跟運(yùn)行環(huán)境無(wú)關(guān)運(yùn)算操作焦匈,我們都在編譯時(shí)解決掉,與運(yùn)行環(huán)境相關(guān)的部分昵仅,則盡可能使用固化設(shè)計(jì)缓熟,在安裝時(shí)或是系統(tǒng)啟動(dòng)時(shí)做一次。
與運(yùn)算環(huán)境無(wú)關(guān)的操作摔笤,在我們以前嵌入式開發(fā)里够滑,Codec會(huì)用到,比如一些碼表吕世,實(shí)際上每次算出來(lái)都是同樣或是類似的結(jié)構(gòu)彰触,于是我們可以直接在編譯時(shí)就把這張表算出來(lái),在運(yùn)行時(shí)則直接使用命辖。在Android里况毅,因?yàn)榇罅渴褂昧薠ML文件,而XML在運(yùn)行時(shí)解析很消耗內(nèi)存尔艇,也會(huì)占用大量?jī)?nèi)存空間尔许,于是就把它在編譯時(shí)解析出來(lái),在應(yīng)用程序可能使用的內(nèi)存段位里找一個(gè)空閑位置放進(jìn)去漓帚,然后再將這個(gè)內(nèi)存偏移地址寫到R.java文件里母债。在執(zhí)行時(shí)午磁,就是直接將二進(jìn)制的解析好的xml樹形結(jié)構(gòu)映射到內(nèi)存R.java所指向的位置尝抖,這時(shí)應(yīng)用程序的代碼在執(zhí)行時(shí)就可以直接使用了毡们。
在Android系統(tǒng)里使用的另一項(xiàng)編譯態(tài)運(yùn)算是prelink。我們Linux內(nèi)核之睥系統(tǒng)環(huán)境昧辽,一般都會(huì)使用Gnu編譯器的動(dòng)態(tài)鏈接功能衙熔,從而可以讓大量代碼通過動(dòng)態(tài)鏈接庫(kù)的方式進(jìn)行共享。在動(dòng)態(tài)鏈接處理里搅荞,一般會(huì)先把代碼編譯成位置無(wú)關(guān)代碼(Position Independent Code红氯,PIC),然后在鏈接階段將共用代碼編譯成.so動(dòng)態(tài)鏈接庫(kù)咕痛,而將可執(zhí)行代碼鏈接到這樣的.so文件痢甘。而在動(dòng)態(tài)鏈接處理里,無(wú)論是.so庫(kù)文件還是可執(zhí)行文件茉贡,在.text段位里會(huì)有PLT(Procedure Linkage Table)塞栅,在.data段位里會(huì)有GOT(Global Offset Table)。這樣腔丧,在代碼執(zhí)行時(shí)放椰,這兩個(gè)文件都會(huì)被映射到同一進(jìn)程空間,可執(zhí)行程序執(zhí)行到動(dòng)態(tài)鏈接庫(kù)里的代碼愉粤,會(huì)通過PLT砾医,找到GOT里定位到的動(dòng)態(tài)鏈接庫(kù)里代碼具體實(shí)現(xiàn)的位置,然后實(shí)現(xiàn)跳轉(zhuǎn)衣厘。
通過這樣的方式如蚜,我們就可以實(shí)現(xiàn)代碼的共享,如上圖中头滔,我們的可執(zhí)行文件a.out怖亭,是可以與其他可執(zhí)行程序共享libxxx.so里實(shí)現(xiàn)的func_from_dso()的。在動(dòng)態(tài)鏈接的設(shè)計(jì)里坤检,PLT與GOT分開是因?yàn)?text段位一般只會(huì)被映射到只讀字段兴猩,避免代碼被非法偷換,而.data段位映射后是可以被修改的早歇,所以一般PLT表保持不動(dòng)倾芝,而GOT會(huì)根據(jù).so文件被映射到進(jìn)程空間的偏移位置再進(jìn)行轉(zhuǎn)換,這樣就實(shí)現(xiàn)了靈活的目的箭跳。同時(shí)晨另,.so文件內(nèi)部也是這樣的設(shè)計(jì),也就是動(dòng)態(tài)鏈接庫(kù)本身可以再次使用這樣的代碼共享技術(shù)鏈接到其他的動(dòng)態(tài)鏈接庫(kù)谱姓,在運(yùn)行時(shí)這些庫(kù)都必須被映射到同一進(jìn)程空間里借尿。所以,實(shí)際上,我們的進(jìn)程空間可能使用到大量的動(dòng)態(tài)鏈接庫(kù)路翻。
動(dòng)態(tài)鏈接在運(yùn)行時(shí)還進(jìn)行一些運(yùn)行態(tài)處理狈癞,像GOT表是需要根據(jù)進(jìn)程上下文換算成正確的虛擬地址上的依稀,另外茂契,還需要驗(yàn)證這些動(dòng)態(tài)鏈接代碼的合法性蝶桶,并且可能需要處理鏈接時(shí)的一些符號(hào)沖突問題。出于加快動(dòng)態(tài)連接庫(kù)的調(diào)用過程掉冶,PLT本身也會(huì)通過Hash表來(lái)進(jìn)行索引以加快執(zhí)行效率真竖。但是動(dòng)態(tài)鏈接庫(kù)文件有可能很大,里面實(shí)現(xiàn)的函數(shù)很多很復(fù)雜厌小,還有可能可執(zhí)行程序使用了大量的動(dòng)態(tài)鏈接庫(kù)恢共,所有這些情況會(huì)導(dǎo)致使用了動(dòng)態(tài)鏈接的應(yīng)用程序,在啟動(dòng)時(shí)都會(huì)很慢璧亚。在一些大型應(yīng)用程序里旁振,這樣的開銷有可能需要花好幾秒才能完全。于是有了prelink的需求涨岁。Prelink就是用一個(gè)交叉編譯的完整環(huán)境拐袜,模擬一次完整地運(yùn)行過程,把參與運(yùn)行的可執(zhí)行程序與動(dòng)態(tài)鏈接所需要使用的地址空間都算出來(lái)一個(gè)合理的位置梢薪,然后再就這個(gè)值寫入到ELF文件里的特殊段位里蹬铺。在執(zhí)行時(shí),就可以不再需要(即便需要秉撇,也只是小范圍的改正)進(jìn)行動(dòng)態(tài)鏈接處理甜攀,可以更快完成加載。這樣的技術(shù)一直是Linux環(huán)境里一個(gè)熱門研究方向琐馆,像firefox這樣的大型應(yīng)用程序經(jīng)過prelink之后规阀,可以減少幾乎一半的啟動(dòng)時(shí)間,這樣的加速對(duì)于嵌入式環(huán)境來(lái)說(shuō)瘦麸,也就更加重要了谁撼。
但這種技術(shù)有種致命缺陷,需要一臺(tái)Linux機(jī)器滋饲,運(yùn)行交叉編譯環(huán)境厉碟,才能使用prelink。而Android源代碼本就設(shè)計(jì)成至少在MacOS與Linux環(huán)境里執(zhí)行的屠缭,它使用的交叉編譯工具使用到Gnu編譯的部分只完成編譯箍鼓,鏈接還是通過它自己實(shí)現(xiàn)的工具來(lái)完成的。有了需求呵曹,但受限于Linux環(huán)境款咖,于是Android開發(fā)者又繼續(xù)創(chuàng)新何暮。在Android世界里使用的prelink,是固定段位的铐殃,在鏈接時(shí)會(huì)根據(jù)固定配置好地址信息來(lái)處理動(dòng)態(tài)鏈接郭卫,比如libc.so,對(duì)于所有進(jìn)程背稼,libc.so都是固定的位置。在Android一直到2.3版本時(shí)玻蝌,都會(huì)使用build/core/prelink-linux-arm.map這個(gè)文件來(lái)進(jìn)行prelink操作蟹肘,而這個(gè)文件也可以看到prelink處理是何其簡(jiǎn)單:
# core system libraries
libdl.so 0xAFF00000 # [<64K]
libc.so 0xAFD00000 # [~2M]
libstdc++.so 0xAFC00000 # [<64K]
libm.so 0xAFB00000 # [~1M]
liblog.so 0xAFA00000 # [<64K]
libcutils.so 0xAF900000 # [~1M]
libthread_db.so 0xAF800000 # [<64K]
libz.so 0xAF700000 # [~1M]
libevent.so 0xAF600000 # [???]
libssl.so 0xAF400000 # [~2M]
libcrypto.so 0xAF000000 # [~4M]
libsysutils.so 0xAEF00000 # [~1M]
就像我們看到的,libdl.so俯树,對(duì)于任何進(jìn)程帘腹,都會(huì)是在0xAFD00001(libc.so結(jié)束)到0xAFF00000之間這個(gè)區(qū)域之間,而
在Android發(fā)展的初期许饿,這種簡(jiǎn)單的prelink機(jī)制阳欲,一直是有效的,但這不是一種很合理的解決方案陋率。首先球化,這種方式不通用,也不夠節(jié)省資源瓦糟,我們很難想像要在系統(tǒng)層加入firefox筒愚、openoffice這樣大型軟件(幾十、上百個(gè).so文件)菩浙,同時(shí)雖然絕大部分的.so是應(yīng)用程序不會(huì)用到的巢掺,但都被一股腦地塞了進(jìn)來(lái)。最好劲蜻,這些鏈接方式也不安全陆淀,我們雖然可以通過“沙盒”模式來(lái)打造應(yīng)用程序執(zhí)行環(huán)境的安全性,但應(yīng)用程序完全知道一些系統(tǒng)進(jìn)程使用的.so文件的內(nèi)容先嬉,則破解起來(lái)相對(duì)比較容易轧苫,進(jìn)程空間分布很固定,則還可以人為地制造一些棧溢出方式來(lái)進(jìn)行攻擊疫蔓。
雖然作了這方面的努力浸剩,但當(dāng)Android到4.0版時(shí),為了加強(qiáng)系統(tǒng)的安全性鳄袍,開始使用新的動(dòng)態(tài)鏈接技術(shù)绢要,地址空間分布隨機(jī)化(Address Space Layout Randomization,ASLR)拗小,將地址空間上的固定分配變成偽隨機(jī)分布重罪,這時(shí)就也取消了prelink。
Android系統(tǒng)設(shè)計(jì)上,對(duì)于性能剿配,在各方面都進(jìn)行了相當(dāng)成功的嘗試搅幅,最后得到的效果也非常不錯(cuò)。大家經(jīng)常批評(píng)Android整個(gè)生態(tài)環(huán)境很惡劣呼胚,高中低檔的設(shè)備充斥市場(chǎng)茄唐,五花八門的分辨率,但拋開商業(yè)因素不談蝇更,Android作為一套操作系統(tǒng)環(huán)境沪编,可以兼容到這么多種應(yīng)用情境,本就是一種設(shè)計(jì)上很成功的表現(xiàn)年扩。如果說(shuō)這種實(shí)現(xiàn)很復(fù)雜蚁廓,倒還顯得不那么神奇,問題是Android在解決一些很難的工程問題的時(shí)候厨幻,用的技巧還是很簡(jiǎn)單的相嵌,這就非常不容易了。我們寫過代碼的人都會(huì)知道况脆,把代碼寫得極度讓人看不懂饭宾,邏輯復(fù)雜,其實(shí)并不需要太高智商格了,反而是編程能力不行所致捏雌。邏輯清晰,簡(jiǎn)單明了笆搓,又能解決問題性湿,才真正是大神級(jí)的代碼,業(yè)界成功的項(xiàng)目满败,linux肤频、git、apache算墨,都是這方面的典范宵荒。
Android所有這些提升性能的設(shè)計(jì),都會(huì)導(dǎo)致另一個(gè)間接收益净嘀,就是所需使用的電量也相應(yīng)大大降低报咳。同樣的運(yùn)算,如果節(jié)省了運(yùn)算上的時(shí)間挖藏,變相地也減少了電量上的損失暑刃。但這不夠,我們的手機(jī)使用的電池非常有限膜眠,如果不使用一些特殊的省電技術(shù)岩臣,也是不行的溜嗜。于是,我們可以再來(lái)透過應(yīng)用程序架谎,看看Android的功耗管理炸宵。