Android ART運行時無縫替換Dalvik虛擬機器的過程分析

來源:互聯網
上載者:User

        Android 4.4發布了一個ART運行時,準備用來替換掉之前一直使用的Dalvik虛擬機器,希望籍此解決飽受詬病的效能問題。老羅不打算分析ART的實現原理,只是很有興趣知道ART是如何無縫替換掉原來的Dalvik虛擬機器的。畢竟在原來的系統中,大量的代碼都是運行在Dalvik虛擬機器裡面的。開始覺得這個替換工作是挺複雜的,但是分析了相關代碼之後,發現思路是很清晰的。本文就詳細分析這個無縫的替換過程。


        我們知道,Dalvik虛擬機器實則也算是一個Java虛擬機器,只不過它執行的不是class檔案,而是dex檔案。因此,ART運行時最理想的方式也是實現為一個Java虛擬機器的形式,這樣就可以很容易地將Dalvik虛擬機器替換掉。注意,我們這裡說實現為Java虛擬機器的形式,實際上是指提供一套完全與Java虛擬機器相容的介面。例如,Dalvik虛擬機器在介面上與Java虛擬機器是一致的,但是它的內部可以是完全不一樣的東西。

         實際上,ART運行時就是真的和Dalvik虛擬機器一樣,實現了一套完全相容Java虛擬機器的介面。為了方便描述,接下來我們就將ART運行時稱為ART虛擬機器,它和Dalvik虛擬機器、Java虛擬機器的關係1所示:

        從圖1可以知道,Dalvik虛擬機器和ART虛擬機器都實現了三個用來抽象Java虛擬機器的介面:

       1. JNI_GetDefaultJavaVMInitArgs -- 擷取虛擬機器的預設初始化參數

       2. JNI_CreateJavaVM -- 在進程中建立虛擬機器執行個體

       3. JNI_GetCreatedJavaVMs -- 擷取進程中建立的虛擬機器執行個體

       在Android系統中,Davik虛擬機器實現在libdvm.so中,ART虛擬機器實現在libart.so中。也就是說,libdvm.so和libart.so匯出了JNI_GetDefaultJavaVMInitArgs、JNI_CreateJavaVM和JNI_GetCreatedJavaVMs這三個介面,供外界調用。

       此外,Android系統還提供了一個系統屬性persist.sys.dalvik.vm.lib,它的值要麼等於libdvm.so,要麼等於libart.so。當等於libdvm.so時,就表示當前用的是Dalvik虛擬機器,而當等於libart.so時,就表示當前用的是ART虛擬機器。

       以上描述的Dalvik虛擬機器和ART虛擬機器的共同之處,當然它們之間最顯著還是不同之處。不同的地方就在於,Dalvik虛擬機器執行的是dex位元組碼,ART虛擬機器執行的是本地機器碼。這意味著Dalvik虛擬機器包含有一個解譯器,用來執行dex位元組碼,具體可以參考Dalvik虛擬機器簡要介紹和學習計劃這個系列的文章。當然,Android從2.2開始,也包含有JIT(Just-In-Time),用來在運行時動態地將執行頻率很高的dex位元組碼翻譯成本地機器碼,然後再執行。通過JIT,就可以有效地提高Dalvik虛擬機器的執行效率。但是,將dex位元組碼翻譯成本地機器碼是發生在應用程式的運行過程中的,並且應用程式每一次重新啟動並執行時候,都要做重做這個翻譯工作的。因此,即使用採用了JIT,Dalvik虛擬機器的總體效能還是不能與直接執行本地機器碼的ART虛擬機器相比。

        那麼,ART虛擬機器執行的本地機器碼是從哪裡來的呢?Android的運行時從Dalvik虛擬機器替換成ART虛擬機器,並不要求開發人員要將重新將自己的應用直接編譯成目標機器碼。也就是說,開發人員開發出的應用程式經過編譯和打包之後,仍然是一個包含dex位元組碼的APK檔案。既然應用程式套件組合含的仍然是dex位元組碼,而ART虛擬機器需要的是本地機器碼,這就必然要有一個翻譯的過程。這個翻譯的過程當然不能發生應用程式啟動並執行時候,否則的話就和Dalvik虛擬機器的JIT一樣了。在電腦的世界裡,與JIT相對的是AOT。AOT進Ahead-Of-Time的簡稱,它發生在程式運行之前。我們用靜態語言(例如C/C++)來開發應用程式的時候,編譯器直接就把它們翻譯成目標機器碼。這種靜態語言的編譯方式也是AOT的一種。但是前面我們提到,ART虛擬機器並不要求開發人員將自己的應用直接編譯成目標機器碼。這樣,將應用的dex位元組碼翻譯成本地機器碼的最恰當AOT時機就發生在應用安裝的時候。

       我們知道,沒有ART虛擬機器之前,應用在安裝的過程,其實也會執行一次“翻譯”的過程。只不過這個“翻譯”的過程是將dex位元組碼進行最佳化,也就是由dex檔案產生odex檔案。這個過程由安裝服務PackageManagerService請求守護進程installd來執行的。從這個角度來說,在應用安裝的過程中將dex位元組碼翻譯成本地機器碼對原來的應用安裝流程基本上就不會產生什麼影響。

        有了以上的背景知識之後,我們接下來就從兩個角度來瞭解ART虛擬機器是如何做到無縫替換Dalvik虛擬機器的:

        1. ART虛擬機器的啟動過程;

        2. Dex位元組碼翻譯成本地機器碼的過程。

        我們知道,Android系統在啟動的時候,會建立一個Zygote進程,充當應用程式進程孵化器。Zygote進程在啟動的過程中,又會建立一個Dalvik虛擬機器。Zygote進程是通過複製自己來建立新的應用程式進程的。這意味著Zygote進程會將自己的Dalvik虛擬機器複製給應用程式進程。通過這種方式就可以大大地提高應用程式的啟動速度,因為這種方式避免了每一個應用程式進程在啟動的時候都要去建立一個Dalvik。事實上,Zygote進程通過自我複製的方式來建立應用程式進程,省去的不僅僅是應用程式進程建立Dalvik虛擬機器的時間,還能省去應用程式進程載入各種系統庫和系統資源的時間,因為它們在Zygote進程中已經載入過了,並且也會連同Dalvik虛擬機器一起複製到應用程式進程中去。關於Zygote進程和應用程式進程啟動的更多知識,可以參考Android系統進程Zygote啟動過程的原始碼分析和Android應用程式進程啟動過程的原始碼分析這兩篇文章。

        即然應用程式進程裡面的Dalvik虛擬機器都是從Zygote進程中複製過來的,那麼接下來我們就繼續Zygote進程是如何建立Dalvik虛擬機器的。從Dalvik虛擬機器的啟動過程分析這篇文章可以知道,Zygote進程中的Dalvik虛擬機器是從AndroidRutime::start這個函數開始建立的。因此,接下來我們就看看這個函數的實現:

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中。

          AndroidRutime類的成員函數start最主要是做了以下三件事情:

          1. 建立一個JniInvocation執行個體,並且調用它的成員函數init來初始化JNI環境;

          2. 調用AndroidRutime類的成員函數startVm來建立一個虛擬機器及其對應的JNI介面,即建立一個JavaVM介面和一個JNIEnv介面;

          3. 有了上述的JavaVM介面和JNIEnv介面之後,就可以在Zygote進程中載入指定的class了。

          其中,第1件事情和第2件事情又是最關鍵的。因此,接下來我們繼續分析它們所對應的函數的實現。

          JniInvocation類的成員函數init的實現如下所示:

#ifdef HAVE_ANDROID_OSstatic const char* kLibrarySystemProperty = "persist.sys.dalvik.vm.lib";#endifstatic const char* kLibraryFallback = "libdvm.so";bool JniInvocation::Init(const char* library) {#ifdef HAVE_ANDROID_OS  char default_library[PROPERTY_VALUE_MAX];  property_get(kLibrarySystemProperty, default_library, kLibraryFallback);#else  const char* default_library = kLibraryFallback;#endif  if (library == NULL) {    library = default_library;  }  handle_ = dlopen(library, RTLD_NOW);  if (handle_ == NULL) {    if (strcmp(library, kLibraryFallback) == 0) {      // Nothing else to try.      ALOGE("Failed to dlopen %s: %s", library, dlerror());      return false;    }    // Note that this is enough to get something like the zygote    // running, we can't property_set here to fix this for the future    // because we are root and not the system user. See    // RuntimeInit.commonInit for where we fix up the property to    // avoid future fallbacks. http://b/11463182    ALOGW("Falling back from %s to %s after dlopen error: %s",          library, kLibraryFallback, dlerror());    library = kLibraryFallback;    handle_ = dlopen(library, RTLD_NOW);    if (handle_ == NULL) {      ALOGE("Failed to dlopen %s: %s", library, dlerror());      return false;    }  }  if (!FindSymbol(reinterpret_cast<void**>(&JNI_GetDefaultJavaVMInitArgs_),                  "JNI_GetDefaultJavaVMInitArgs")) {    return false;  }  if (!FindSymbol(reinterpret_cast<void**>(&JNI_CreateJavaVM_),                  "JNI_CreateJavaVM")) {    return false;  }  if (!FindSymbol(reinterpret_cast<void**>(&JNI_GetCreatedJavaVMs_),                  "JNI_GetCreatedJavaVMs")) {    return false;  }  return true;}
        這個函數定義在檔案libnativehelper/JniInvocation.cpp中。

        JniInvocation類的成員函數init所做的事情很簡單。它首先是讀取系統屬性persist.sys.dalvik.vm.lib的值。前面提到,系統屬性persist.sys.dalvik.vm.lib的值要麼等於libdvm.so,要麼等於libart.so。因此,接下來通過函數dlopen載入到進程來的要麼是libdvm.so,要麼是libart.so。無論載入的是哪一個so,都要求它匯出JNI_GetDefaultJavaVMInitArgs、JNI_CreateJavaVM和JNI_GetCreatedJavaVMs這三個介面,並且分別儲存在JniInvocation類的三個成員變數JNI_GetDefaultJavaVMInitArgs_、JNI_CreateJavaVM_和JNI_GetCreatedJavaVMs_中。這三個介面也就是前面我們提到的用來抽象Java虛擬機器的三個介面。

        從這裡就可以看出,JniInvocation類的成員函數init實際上就是根據系統屬性persist.sys.dalvik.vm.lib來初始化Dalvik虛擬機器或者ART虛擬機器環境。

        接下來我們繼續看AndroidRutime類的成員函數startVm的實現:

int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv){    ......    /*     * Initialize the VM.     *     * The JavaVM* is essentially per-process, and the JNIEnv* is per-thread.     * If this call succeeds, the VM is ready, and we can start issuing     * JNI calls.     */    if (JNI_CreateJavaVM(pJavaVM, pEnv, &initArgs) < 0) {        ALOGE("JNI_CreateJavaVM failed\n");        goto bail;    }    ......}
         這個函數定義在檔案frameworks/base/core/jni/AndroidRuntime.cpp中。

         AndroidRutime類的成員函數startVm最主要就是調用函數JNI_CreateJavaVM來建立一個JavaVM介面及其對應的JNIEnv介面:

extern "C" jint JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args) {  return JniInvocation::GetJniInvocation().JNI_CreateJavaVM(p_vm, p_env, vm_args);}
        這個函數定義在檔案libnativehelper/JniInvocation.cpp中。

        JniInvocation類的靜態成員函數GetJniInvocation返回的便是前面所建立的JniInvocation執行個體。有了這個JniInvocation執行個體之後,就繼續調用它的成員函數JNI_CreateJavaVM來建立一個JavaVM介面及其對應的JNIEnv介面:

jint JniInvocation::JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args) {  return JNI_CreateJavaVM_(p_vm, p_env, vm_args);}
        這個函數定義在檔案libnativehelper/JniInvocation.cpp中。

        JniInvocation類的成員變數JNI_CreateJavaVM_指向的就是前面所載入的libdvm.so或者libart.so所匯出的函數JNI_CreateJavaVM,因此,JniInvocation類的成員函數JNI_CreateJavaVM返回的JavaVM介面指向的要麼是Dalvik虛擬機器,要麼是ART虛擬機器。

        通過上面的分析,我們就很容易知道,Android系統通過將ART運行時抽象成一個Java虛擬機器,以及通過系統屬性persist.sys.dalvik.vm.lib和一個適配層JniInvocation,就可以無縫地將Dalvik虛擬機器替換為ART運行時。這個替換過程設計非常巧妙,因為涉及到的代碼修改是非常少的。

        以上就是ART虛擬機器的啟動過程,接下來我們再分析應用程式在安裝過程中將dex位元組碼翻譯為本地機器碼的過程。

         Android應用程式的安裝過程可以參考Android應用程式安裝過程原始碼分析這篇文章。 簡單來說,就是Android系統通過PackageManagerService來安裝APK,在安裝的過程,PackageManagerService會通過另外一個類Instalerl的成員函數dexopt來對APK裡面的dex位元組碼進行最佳化:

public final class Installer {    ......    public int dexopt(String apkPath, int uid, boolean isPublic) {        StringBuilder builder = new StringBuilder("dexopt");        builder.append(' ');        builder.append(apkPath);        builder.append(' ');        builder.append(uid);        builder.append(isPublic ? " 1" : " 0");        return execute(builder.toString());    }    ......}
         這個函數定義在檔案frameworks/base/services/java/com/android/server/pm/Installer.java中。

         Installer通過socket向守護進程installd發送一個dexopt請求,這個請求是由installd裡面的函數dexopt來處理的:

int dexopt(const char *apk_path, uid_t uid, int is_public){    struct utimbuf ut;    struct stat apk_stat, dex_stat;    char out_path[PKG_PATH_MAX];    char dexopt_flags[PROPERTY_VALUE_MAX];    char persist_sys_dalvik_vm_lib[PROPERTY_VALUE_MAX];    char *end;    int res, zip_fd=-1, out_fd=-1;    ......    /* The command to run depend ones the value of persist.sys.dalvik.vm.lib */    property_get("persist.sys.dalvik.vm.lib", persist_sys_dalvik_vm_lib, "libdvm.so");    /* Before anything else: is there a .odex file?  If so, we have     * precompiled the apk and there is nothing to do here.     */    sprintf(out_path, "%s%s", apk_path, ".odex");    if (stat(out_path, &dex_stat) == 0) {        return 0;    }    if (create_cache_path(out_path, apk_path)) {        return -1;    }    ......    out_fd = open(out_path, O_RDWR | O_CREAT | O_EXCL, 0644);    ......    pid_t pid;    pid = fork();    if (pid == 0) {        ......        if (strncmp(persist_sys_dalvik_vm_lib, "libdvm", 6) == 0) {            run_dexopt(zip_fd, out_fd, apk_path, out_path, dexopt_flags);        } else if (strncmp(persist_sys_dalvik_vm_lib, "libart", 6) == 0) {            run_dex2oat(zip_fd, out_fd, apk_path, out_path, dexopt_flags);        } else {            exit(69);   /* Unexpected persist.sys.dalvik.vm.lib value */        }        exit(68);   /* only get here on exec failure */    }     ......}

         這個函數定義在檔案frameworks/native/cmds/installd/commands.c中。

         函數dexopt首先是讀取系統屬性persist.sys.dalvik.vm.lib的值,接著在/data/dalvik-cache目錄中建立一個odex檔案。這個odex檔案就是作為dex檔案最佳化後的輸出檔案。再接下來,函數dexopt通過fork來建立一個子進程。如果系統屬性persist.sys.dalvik.vm.lib的值等於libdvm.so,那麼該子進程就會調用函數run_dexopt來將dex檔案最佳化成odex檔案。另一方面,如果系統屬性persist.sys.dalvik.vm.lib的值等於libart.so,那麼該子進程就會調用函數run_dex2oat來將dex檔案最佳化成oart檔案,實際上就是將dex位元組碼翻譯成本地機器碼,並且儲存在一個oat檔案中。

        函數run_dexopt和run_dex2oat的實現如下所示:

static void run_dexopt(int zip_fd, int odex_fd, const char* input_file_name,    const char* output_file_name, const char* dexopt_flags){    static const char* DEX_OPT_BIN = "/system/bin/dexopt";    static const int MAX_INT_LEN = 12;      // '-'+10dig+'\0' -OR- 0x+8dig    char zip_num[MAX_INT_LEN];    char odex_num[MAX_INT_LEN];    sprintf(zip_num, "%d", zip_fd);    sprintf(odex_num, "%d", odex_fd);    ALOGV("Running %s in=%s out=%s\n", DEX_OPT_BIN, input_file_name, output_file_name);    execl(DEX_OPT_BIN, DEX_OPT_BIN, "--zip", zip_num, odex_num, input_file_name,        dexopt_flags, (char*) NULL);    ALOGE("execl(%s) failed: %s\n", DEX_OPT_BIN, strerror(errno));}static void run_dex2oat(int zip_fd, int oat_fd, const char* input_file_name,    const char* output_file_name, const char* dexopt_flags){    static const char* DEX2OAT_BIN = "/system/bin/dex2oat";    static const int MAX_INT_LEN = 12;      // '-'+10dig+'\0' -OR- 0x+8dig    char zip_fd_arg[strlen("--zip-fd=") + MAX_INT_LEN];    char zip_location_arg[strlen("--zip-location=") + PKG_PATH_MAX];    char oat_fd_arg[strlen("--oat-fd=") + MAX_INT_LEN];    char oat_location_arg[strlen("--oat-name=") + PKG_PATH_MAX];    sprintf(zip_fd_arg, "--zip-fd=%d", zip_fd);    sprintf(zip_location_arg, "--zip-location=%s", input_file_name);    sprintf(oat_fd_arg, "--oat-fd=%d", oat_fd);    sprintf(oat_location_arg, "--oat-location=%s", output_file_name);    ALOGV("Running %s in=%s out=%s\n", DEX2OAT_BIN, input_file_name, output_file_name);    execl(DEX2OAT_BIN, DEX2OAT_BIN,          zip_fd_arg, zip_location_arg,          oat_fd_arg, oat_location_arg,          (char*) NULL);    ALOGE("execl(%s) failed: %s\n", DEX2OAT_BIN, strerror(errno));}
         這兩個函數定義在檔案frameworks/native/cmds/installd/commands.c中。

         這從裡就可以看出,函數run_dexopt通過調用/system/bin/dexopt來對dex位元組碼進行最佳化,而函數run_dex2oat通過調用/system/bin/dex2oat來將dex位元組碼翻譯成本地機器碼。注意,無論是對dex位元組碼進行最佳化,還是將dex位元組碼翻譯成本地機器碼,最終得到的結果都是儲存在相同名稱的一個odex檔案裡面的,但是前者對應的是一個dexy檔案(表示這是一個最佳化過的dex),後者對應的是一個oat檔案(實際上是一個自訂的elf檔案,裡麵包含的都是本地機器指令)。通過這種方式,原來任何通過絕對路徑引用了該odex檔案的代碼就都不需要修改了。

        通過上面的分析,我們就很容易知道,只需要將dex檔案的最佳化過程替換成dex檔案翻譯成本地機器碼的過程,就可以輕鬆地在應用安裝過程,無縫地將Dalvik虛擬機器替換成ART運行時。

        最後,還有一個地方需要注意的是,應用程式的安裝發生在兩個時機,第一個時機是系統啟動的時候,第二個時機系統啟動完成後使用者自行安裝的時候。在第一個時機中,系統除了會對/system/app和/data/app目錄下的所有APK進行dex位元組碼到本地機器碼的翻譯之外,還會對/system/framework目錄下的APK或者JAR檔案,以及這些APK所引用的外部JAR,進行dex位元組碼到本地機器碼的翻譯。這樣就可以保證除了應用之外,系統中使用Java來開發的系統服務,也會統一地從dex位元組碼翻譯成本地機器碼。也就是說,將Android系統中的Dalvik虛擬機器替換成ART運行時之後,系統中的代碼都是由ART運行時來執行的了,這時候就不會對Dalvik虛擬機器產生任何的依賴。

        至此,我們就分析完成ART運行時無縫替換Dalvik虛擬機器的過程了,更多的乾貨分享衣關注老羅的新浪微博:http://weibo.com/shengyangluo。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.