編輯:關於Android編程
Android在4.4就已推出新運行時ART,准備替代用了有些時日的Dalvik。不過當時尚屬測試版,主角仍是Dalvik。 直到今年的Google I/O大會,ART才正式取代Dalvik。這個消息在科技界引起不小轟動,也吸引不少技術人員對它的“技術分析”。可惜這些“技術分析”不過是引用了官方的數據和圖表而已。這一系列文章將對ART進行真正的技術分析。老規矩,分析前先進行簡要介紹和制定學習計劃。
老羅的新浪微博:http://weibo.com/shengyangluo,歡迎關注!
ART的發布之所以引起大家的關注,是因為Andoid與iOS相比,一直被人诟病它的流暢性。Android的流暢性問題,有一部分原因就歸結於它的應用程序和部分系統服務是運行虛擬機之上的,也就是運行在Dalvik虛擬機之上,而iOS的應用程序和系統服務都是直接執行本地機器指令的。除了使用ART替換Dalvik之外,我們也應當看到,Android從3.0開始,就不遺余力地改進系統的流暢性。例如,3.0增加了對應用程序2D UI的硬件加速渲染,也就是GPU渲染。在此之前,應用程序的2D UI一直都是使用軟件渲染,也就是CPU渲染。又如4.1通過Project Butter,在UI架構中引入了VSYNC、Triple Buffer和HWComposer等技術,極大地提高UI的流暢性。
ART之所以會比Dalvik快,是因為ART執行的是本地機器指令,而Dalvik執行的是Dex字節碼,通過通過解釋器執行。盡管Dalvik也會對頻繁執行的代碼進行JIT生成本地機器指令來執行,但畢竟在應用程序運行的過程中將Dex字節碼翻譯成本地機器機器指令也會影響到應用程序本身的執行,因此即使Dalvik使用了JIT,也在一定程度上也比不上直接就可以執行本地機器指令的運行時。
在前面Android ART運行時無縫替換Dalvik虛擬機的過程分析一文中,我們提到,ART像Dalvik一樣,都實現Java虛擬機接口,如圖1所示:
圖1 Dalvik、ART和Java VM的關系
Zygote進程在啟動的過程中,正是通過圖1所示的接口創建Dalvik或者ART虛擬機的,這樣看來,ART雖然執行的本地機器指令,但是它表面看來,又是一個不折不扣的虛擬機。也正是因為這樣,ART才可以在不重新編譯APK的基礎上,直接可以加載和運行APK。這也是ART運行時可以無縫替換Dalvik運行時的原理。因此,我們就可以得出一個結論:ART是一個執行本地機器指令的虛擬機。這個結論似乎有點矛盾,既然是執行本地機器指令,為什麼又稱為虛擬機呢?從接下來的文章分析可以知道,ART除了實現Java虛擬機接口之外,其內部還有垃圾收集機制,同時還有Java核心類庫調用,因此,隨著對ART的深入分析,我們就認為這個結論是不矛盾的了。
上面提到,ART才可以在不重新編譯APK的基礎上,直接對其進行加載和運行,這是由於APK在安裝時被執行了AOT。AOT(Ahead Of Time)是相對JIT(Just In Time)而言的。也就是在APK運行之前,就對其包含的Dex字節碼進行翻譯,得到對應的本地機器指令,於是就可以在運行時直接執行了。這種技術不但使得我們可以不對原有的APK作任何修改,還可以使得這些APK只需要在安裝時翻譯一次,就可以無數次以本地機器指令的形式運行。這種技術與我們用C/C++語言編寫一個程序,然後用GCC編譯得到一個可執行程序,最後這個可執行程序就可以無數次地加載到系統執行,是差不多的。
在ART中,打包在APK裡面的Dex字節碼是通過LLVM翻譯成本地機器指令的。LLVM是一個用來快速開發自己的編譯器的框架系統,關於它的介紹,可以參考它的作者之一Chris Lattner寫的這篇文章:http://www.aosabook.org/en/llvm.html。說起Chris Lattner,他就是Apple今年發布的Swift語言的首席架構師啊,所以我們就可以感受到LLVM有多強大了。總體來說,LLVM包含有三類組件,如圖2所示:
圖2 LLVM組件
其中,前端(Frontend)對輸入的源代碼(Source Code)進行語法分析後,生成一棵抽象語法樹(Abstract Syntax Tree,AST),並且可以進一步將得到的抽象語法樹轉化一種稱為LLVM IR的中間語言。LLVM IR是一種與編程語言無關的中間語言,也就是說,不管是C語言,還是Fortran、Ada語言編寫的源文件,經過語法分析後,最終都可以得到一個對應的LLVM IR文件。這個LLVM IR文件可以作為後面的優化器(Optimizer)和後端(Backend)的輸入文件。優化器對LLVM IR文件進行優化,例如消除代碼裡面的冗余計算,以提到最終生成的代碼的執行效率。後端負責生成最終的機器指令。
LLVM的上述架構大大簡化開發編譯器的流程,因為開發者需要關注的僅僅是前端,然後就可以利用現成的優化器來進行代碼優化,並且利用現成的後端生成各種體系結構相關的機器指令,如圖3所示:
圖3 利用現成的與語言無關的優化器和後端為語言相關的前端生成各種體系結構相關的機器指令
在圖3中,我們分別為C、Fortran和Ada三種語言開發三個不同的前端,然後利用現成的優化器對它們生成的LLVM IR語言進行優化,並且通過現成的後端生成X86、PowerPC和ARM三種不同體系結構的機器指令。
如果我們沒有忘記,在Dalvik運行時中,APK在安裝的時候,安裝服務PackageManagerService會通過守護進程installd調用一個工具dexopt對打包在APK裡面包含有Dex字節碼的classes.dex進行優化,優化得到的文件保存在/data/dalvik-cache目錄中,並且以.odex為後綴名,表示這是一個優化過的Dex文件。在ART運行時中,APK在安裝的時候,同樣安裝服務PackageManagerService會通過守護進程installd調用另外一個工具dex2oat對打包在APK裡面包含有Dex字節碼進翻譯。這個翻譯器實際上就是基於LLVM架構實現的一個編譯器,它的前端是一個Dex語法分析器。翻譯後得到的是一個ELF文件,這個ELF文件同樣是以.odex後綴結束,並且也是保存在/data/dalvik-cache目錄中。
ELF是Linux系統使用的一種文件格式,我們平時接觸的靜態庫、動態庫和可執行文件都是以這種格式保存的,但是由dexoat工具生成的文件與上述三種文件都不一樣,它有兩個特殊的段oatdata和oatexec,分別用來儲存原來打包在APK裡面的dex文件和翻譯這個dex文件裡面的類方法得到本地機器指令,如圖4所示:
圖4 ART翻譯classes.dex後得到的ELF文件格式
在ELF文件的動態段(dymanic section)中,還導出了三個符號oatdata、oatexec和oatlastword,分別用來描述oatdata和oatexec段加段到內存後的起止地址。在oatdata段中,包含了兩個重要的信息,一個信息是原來的classes.dex文件的完整內容,另一個信息引導ART找到classes.dex文件裡面的類方法所對應的本地機器指令,這些本地機器指令就保存在oatexec段中。
舉個例子說,我們在classes.dex文件中有一個類A,那麼當我們知道類A的名字後,就可以通過保存在oatdata段的dex文件得到類A的所有信息,比如它的父類、成員變量和成員函數等。另一方面,類A在oatdata段中有一個對應的OatClass結構體。這個OatClass結構體描述了類A的每一個方法所對應的本地機器指令在oatexec段的位置。也就是說,當我們知道一個類及其某一個方法的名字(簽名)之後,就可以通過oatdata段的dex文件內容和OatClass結構體找到其在oatexec段的本地機器指令,這樣就可以執行這個類方法了。
通過上面的分析,我們就將ART的運行原理都簡要地介紹了,總結如下:
1. 在Android系統啟動過程中創建的Zygote進程利用ART運行時導出的Java虛擬機接口創建ART虛擬機。
2. APK在安裝的時候,打包在裡面的classes.dex文件會被工具dex2oat翻譯成本地機器指令,最終得到一個ELF格式的文件。
3. APK運行時,上述生成的ELF文件會被加載到內存中,並且ART虛擬機可以通過裡面的oatdata和oatexec段找到任意一個類的方法對應的本地機器指令來執行。
對於第1點,ART虛擬機的創建過程中,可以參考前面Android ART運行時無縫替換Dalvik虛擬機的過程分析一文。
對於第2點,APK裡面的Dex字節碼被dex2oat工具翻譯生本地機器指令的過程,掌握它需要有扎實的編譯知識。由於知識、能力、時間有限,因此這一部分的內容我們就略過不分析了,但是這將不會影響我們對ART運行時的理解。
對於第3點,是我們理解ART運行時的關鍵所在。以ART虛擬機的啟動過程為例,從前面Android ART運行時無縫替換Dalvik虛擬機的過程分析一文可以知道,在AndroidRuntime類的成員函數start中,ART虛擬機創建和初始化完成後,Zygote進程就會通過它導出的JNI接口CallStaticVoidMethod使得它以指定的類方法為入口正式進入運行狀態,如下所示:
void AndroidRuntime::start(const char* className, const char* options) { ...... /* start the virtual machine */ JniInvocation jni_invocation; jni_invocation.Init(NULL); JNIEnv* env; if (startVm(&mJavaVM, &env) != 0) { return; } ...... /* * Start VM. This thread becomes the main thread of the VM, and will * not return until the VM exits. */ char* slashClassName = toSlashClassName(className); jclass startClass = env->FindClass(slashClassName); if (startClass == NULL) { ALOGE("JavaVM unable to locate class '%s'\n", slashClassName); /* keep going */ } else { jmethodID startMeth = env->GetStaticMethodID(startClass, "main", "([Ljava/lang/String;)V"); if (startMeth == NULL) { ALOGE("JavaVM unable to find main() in '%s'\n", className); /* keep going */ } else { env->CallStaticVoidMethod(startClass, startMeth, strArray); #if 0 if (env->ExceptionCheck()) threadExitUncaughtException(env); #endif } } ...... }這個函數定義在文件frameworks/base/core/jni/AndroidRuntime.cpp中。
在AndroidRuntime類的成員函數start中,參數className的值等於“com.android.internal.os.ZygoteInit”,本地變量env是從調用另外一個成員函數startVM創建的ART虛擬機獲得的JNI接口。函數的目標就是要找到一個名稱為com.android.internal.os.ZygoteInit的類,以及它的靜態成員函數main,然後就以這個函數為入口,開始運行ART虛擬機。為此,函數執行了以下步驟:
1. 調用JNI接口FindClass加載com.android.internal.os.ZygoteInit類。
2. 調用JNI接口GetStaticMethodID找到com.android.internal.os.ZygoteInit類的靜態成員函數main。
3. 調用JNI接口CallStaticVoidMethod開始執行com.android.internal.os.ZygoteInit類的靜態成員函數main。
注意,在第3步中,要執行的是CallStaticVoidMethod開始執行com.android.internal.os.ZygoteInit類的靜態成員函數main的本地機器指令,而不是Dex字節碼。這樣就引出以下三個關鍵問題:
1. ART如何找到com.android.internal.os.ZygoteInit類?
2. ART如何找到com.android.internal.os.ZygoteInit類的靜態成員函數main?
3. ART如何找到com.android.internal.os.ZygoteInit類的靜態成員函數main的本地機器指令?
解決上述三個問題所需要的信息都存在於dex2oat工具生成的ELF文件中。因此,在接下來的文章中,我們將通過分析dex2oat工具生成的ELF文件來回答上述三個問題,使得我們可以更加好地理解ART的工作原理。
如上所述,由於ART在查找類方法時,需要用到保存在ELF文件的oatdata段的原dex文件內容,實質上就是要對dex文件進行解析,以獲得相關的信息。這與Dalvik虛擬機在dex文件中查找類和方法信息的過程是一樣的。這意味著要理解ART運行時,必須先要理解Dalvik虛擬機。Dalvik虛擬機的相關知識可以參考Dalvik虛擬機簡要介紹和學習計劃這個系列的文章。這告訴我們一個道理,舊的知識並沒有過時,它對我們學習新的知識是有幫助的,有時候甚至是必須的。所以大家就不要覺得最前面寫的那些基於Android 2.3版本的文章是沒有用的了。我們在學習一樣新東西的時候,無論是新的知識,還是舊的知識,對我們理解它的原理,都是很有幫助的!
我們除了要以dex2oat工具生成的ELF文件作為切入點來分析ART運行時之外,還會結合ART運行時的垃圾收集機制來說明ART運行時與Dalvik虛擬機一樣也是一個虛擬機,以此加深對ART運行時的理解。與Dalvik虛擬機的垃圾收集機制相比,ART運行時的垃圾收集機制更為復雜,由此帶來的垃圾收集效率也更高。因此我們在分析ART運行時的垃圾收集機制之前,先會分析Dalvik虛擬機的垃圾收集機制。一方面是有利於我們循序漸進、由簡地繁地講解ART運行時的垃圾收集原理,另一方面也方便我們對比ART運行時和Dalvik虛擬機的垃圾收集機制有哪些不同,從而可以更好地理解為ART運行時的垃圾收集效率更高。
綜上所述,接下來我們就按照以下幾個情景來分析ART的工作原理:
1. Dalvik虛擬機加載Dex文件,並且從中查找類和方法的過程。
2. ART運行時查找類方法的本地機器指令的過程。
3. Dalvik虛擬機的垃圾收集過程。
4. ART運行時的垃圾收集過程。
以上情景將基於Android 4.4源碼進行分析,一方面是因為Android L版本的源碼還沒有放出來,另一方面是相信萬變不離其宗,即使Androd L版本的ART實現有變化,但是基本的原理還是一樣的。敬請關注!同時更多的信息可以關注老羅的新浪微博:http://weibo.com/shengyangluo。
Intent是Android中用來調用其它組件的類,通過Intent,我們可以非常方便的調用Activity,Broadcast Receiver和Service。Int
這兩天學習了使用Path繪制貝塞爾曲線相關,然後自己動手做了一個類似QQ未讀消息可拖拽的小氣泡,效果圖如下:接下來一步一步的實現整個過程。基本原理其實就是使用Path繪制
前言Android自定義控件經常會用到Canvas繪制2D圖形,在優化自己自定義控件技能之前,必須熟練掌握Canvas繪圖機制。本文從以下三個方面對Canvas繪圖機制進
介紹A text field allows the user to type text into your app. It can be either single li