1. 程式人生 > >Android 虛擬機器簡單介紹——ART、Dalvik、啟動流程分析

Android 虛擬機器簡單介紹——ART、Dalvik、啟動流程分析

Android 虛擬機器方面的知識,我是通過《深入理解 Android 核心設計思想》來學習的,內容特別多(只有一章,但有 160 頁),但感覺和 Android 開發有些偏了,因此很多內容都沒有認真去看,比如 EFL 格式等,這裡只是選取了一些感覺比較重要的做一個大致的簡單的介紹。

虛擬機器基礎知識

Java VM

詳見《深入理解 Java 虛擬機器》

LLVM

LLVM 全稱是 Low Level Virtual Machine,但和虛擬機器沒太大關係,官方定義是:The LLVM Project is a colection of modular and resuable compiler and toolchain technologies。即 LLVM 的價值在於可模組化、可重複使用。

LLVM 框架如下所示:

LLVM

Frontend:負責分析原始碼、檢查錯誤,然後將原始碼編譯成抽象語法樹

Optimizer:通過多種優化手段來提高程式碼的執行效率,在一定程度上獨立於具體的語言和目標平臺

Backend:也被稱為程式碼生成器,用於將前述的原始碼轉化為目標前臺的指令集

LLVM 的模組化:

LLVM 的模組化

可以看出,如果要讓基於 LLVM 框架的編譯器支援一種新語言,那麼所要做的可能僅僅是實現一個新的 Frontend,而已有的 Optimizer 和 Backend 則能做到重複使用。上述能力得到實現的關鍵在於 LLVM 的 Intermediate Representation(IR),IR 能在 LLVM 的編譯器中(具體在 Optimizer 階段)以一種相對獨立的方式來表述各種原始碼,從而很好地剝離了各種不同語言間的差異,進而實現模組的複用。

Android 中的經典垃圾回收演算法

Android 中不管是 Dalvik 還是 Art,它們所使用的垃圾回收演算法都是基於 Mark-Sweep 的。

GC 的觸發時機有:

  1. GC_FOR_MALLOC。堆記憶體已滿的情況下程式嘗試去分配新的記憶體塊

  2. GC_CONCURRENT,堆記憶體超過特定閾值,觸發並行的 GC 事件

  3. GC_HPROF_DUMP_HEAP,開發者主動請求建立 HPROF

  4. GC_EXPLICIT,程式主動呼叫 gc() 函式,儘量避免這種用法

Art 和 Dalvik 之爭

Dalvik 是 Android 4.4 之前的標準虛擬機器,為了效能上的考慮,Dalvik 所做出的努力有:

  1. 多個 Class 檔案融合進一個 Dex 檔案中,以節省記憶體空間

  2. Dex 檔案可以在多個程序之間共享

  3. 在應用程式執行之前完成位元組碼的檢驗操作,因為檢驗操作十分耗時

  4. 優化位元組碼

  5. 多個程序共享的程式碼不能隨意編輯,這是為了保證安全性

但 Android 從誕生起就揹負了“系統龐大,執行慢”的包袱,因此,從 Android 4.4 開始,Art 就以和 Dalvik 暫時共存的形式正式進入了人們的視野,而在 Android Lollipop 中正式取代了 Dalvik 的位置。

Art 相比 Dalvik 在效能上有著顯著的優勢,主要原因在於 Dalvik 虛擬機器多數情況下還得通過直譯器的方式來執行 Dex 資料(JIT 雖然能在一定程度上提高效率,但也僅僅是針對一小部分情況,作用有限);而 Art 虛擬機器則採用了 AOT(Ahead Of Time) 技術,從而大幅提高了效能。

Dalvik 中的 JIT只有在程式執行過程中才會將部分熱點程式碼編譯成機器碼,這在某種程度上也加重了 CPU 的負擔。而 AOT 則會提前將 Java 程式碼翻譯成針對目標平臺的機器碼,雖然這也意味著編譯時間有所增加,但 Android 系統的構建原本就慢,所以這點犧牲還是值得的。

Art 虛擬機器整體框架

無論是 Dalvik 還是 Art,或者未來可能出現的新型虛擬機器,它們提供的功能將全部封裝在一個 so 庫中,並且對外需要暴露 JNI_GetDefaultVMInitArgs、JNI_CreateVM 和 JNI_GetCreatedJavaVMs 三個介面,使用者(比如 Zygote)只需要按照統一的介面標準就可以控制和使用所有型別的虛擬機器了。

組成 Android 虛擬機器的核心自系統包括但不限於 Runtime、ClassLoader System、Execution、Engine System、Heap Manager 和 GC 系統、JIT、JNI 環境等。

和標準的 JVM 一樣,類載入器在 Android 虛擬機器中也扮演者很重要的作用,可以分為 Boot ClassLoader、System ClassLoader、Dex ClassLoader 等,所有被載入的類和它們的組成元素都將由 ClassLinker 做統一的管理。

除了位元組碼解釋執行的方式,Art 還支援通過 AOT 來直接執行位元組碼編譯而成的機器碼。AOT 的編譯時機有兩個:隨 Android ROM 構建時一起編譯、程式安裝時執行編譯(針對第三方應用程式)。Art 引入了新的儲存格式,即 OAT 檔案來儲存編譯後的機器程式碼。而 OAT 機器碼的載入需要用到 ELF 的基礎能力。

另外,由於一股腦地在程式安裝階段將 Dex 轉化為 OAT 造成造成了一定的資源浪費,從 Android N 版本開始,Art 又改變了之前的 OAT 策略——程式在安裝時不再統一執行 dex2oat,而改由根據程式的實際執行情況來決定有哪些部分需要被編譯成原生代碼,即恢復了 Interpreter、JIT、OAT 三足鼎立的局面。一方面,這種新變化大幅加快了程式的安裝速度,解決了系統更新時使用者需要經歷漫長等待的問題;另一方面,由於程式的首次啟動必須通過直譯器來執行,Android N 版本必須採用多種手段(新的直譯器,將 Verification 前移等)來保證程式的啟動速度不受影響。

應用程式除了解釋執行外,還會在執行過程中實時做 JIT 編譯——不過它的結果並不會被持久化。另外,虛擬機器會記錄下應用程式在動態執行過程中被執行過的函式,並輸出到 Profile 檔案裡。

AOT compile daemon 將在系統同時滿足 idle 和充電狀態兩個條件時才會被喚醒,並按照一定的邏輯來遍歷執行應用程式的 AOT 優化。由於參與 AOT 的函式數量通常只佔應用程式程式碼的一小部分,所以整體而言 Android N 版本 AOT 結果所佔用的空間大小比舊版本要小很多。

Dex 位元組碼

Java 類檔案和 DEX 檔案對比:

類檔案 VS DEX 檔案

ELF 檔案格式

Art 虛擬機器最大的特點就是通過 dex2oat 將 Dex 預編譯為包含了機器指令的 oat 檔案,從而顯著提升了程式的執行效率。而 oat 檔案本身是基於 Linux 的可執行檔案格式——ELF 所做的擴充套件。

ELF 檔案至少支援 3 中形態:可重定向檔案(Relocatable File)、可執行檔案(Executable File)、可共享的物件檔案(Shared Object File)。

Relocatable File 的一個具體範例是 .o 檔案,它是在編譯過程中產生的中間檔案。

Shared Object File 即動態連結庫,通常以 “.so” 為字尾名。

靜態連結庫的特點是會在程式的編譯連結階段就完成函式和變數的地址解析工作,並使之成為可執行程式中不可分割的一部分。

動態連結庫不需要在編譯時就打包到可執行程式中,而是等到後者在執行階段在實現動態的載入和重定位。動態連結庫在被載入到記憶體中之後,作業系統需要為它執行動態連線操作。

動態連結庫的處理過程如下:

  1. 在編譯階段,程式經歷了預編譯、編譯、彙編及連結操作後,最終形成一個 ELF 可執行程式。同時程式所依賴的動態庫會被記錄到 .dynamic 區段中;載入動態庫所需的 Linker 由 .interp 來指示。

  2. 程式執行起來後,系統首先會通過 .interp 區段找到聯結器的絕對路徑,然後將控制權交給它

  3. Linker 負責解析 .dynamic 中的記錄,得出程式依賴的所有動態連結庫

  4. 動態連結庫載入完成後,它們所包含的 export 函式在記憶體中的地址就可以確定下來了,Linker 通過預設機制(如 GOT)來保證程式中引用到外部函式的地方可以正常工作,即完成 Dynamic Relocation

OAT

與 OAT 相關的檔案格式字尾有幾種:

  1. .art,這個檔案也被稱為 image,由 dex2oat 工具生成,它的內部包含了很多 Dex 檔案,內部儲存的是載入好的class資訊以及一些事先建立好的物件,Zygote 在啟動過程中會載入 boot.art

  2. .oat,由 dex2oat 工具生成,boot.oat 內部儲存的是程式碼

  3. .odex,在 Dalvik 中,.odex 表示被優化後的 Dex 檔案;Art 中也同樣存在 odex 檔案,但和 Dalvik 中的情況不同

應用程式的安裝流程:

應用程式的安裝流程1

應用程式的安裝流程2

Android 虛擬機器的典型啟動流程

Android 虛擬機器啟動的大致流程圖如下:

Android 啟動過程

下面分析具體的程式碼執行過程,首先看指令碼 init.rc 與 zygote 相關的內容:

service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
    class main
    socket zygote stream 660 root system
    onrestart write /sys/android_power/request_state wake
    onrestart write /sys/power/state on
    onrestart restart media
    onrestart restart netd

app_process 是 zygote 的載體,其 main 函式如下:

int main(int argc, const char* const argv[])
{
    ...
    if (zygote) {
        runtime.start("com.android.internal.os.ZygoteInit",
                startSystemServer ? "start-system-server" : "");
    } else if (className) {
        ...
        runtime.start("com.android.internal.os.RuntimeInit",
                application ? "application" : "tool");
    } else {
        ...
    }
}

runtime 指 AndroidRuntime:

void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
    JniInvocation jni_invocation;
    jni_invocation.Init(NULL); // 初始化當前的執行環境
    
    JNIEnv* env;
    if (startVm(&mJavaVM, &env, zygote) != 0) { // 啟動虛擬機器
        return;
    }
    
    onVmCreated(env); // 虛擬機器建立成功,執行回撥函式通知呼叫者
    
    /*
     * 註冊 native 函式
     */
    if (startReg(env) < 0) {
        ALOGE("Unable to register all android natives\n");
        return;
    }
    
    /*
     * 開始執行目標物件的主函式
     */
    char* slashClassName = toSlashClassName(className != NULL ? className : "");
    jclass startClass = env->FindClass(slashClassName);
    if (startClass == NULL) {
        ALOGE("JavaVM unable to locate class '%s'\n", slashClassName);
        /* keep going */
    } else {
        // 執行 main 函式
        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
        }
    }
    
}

在啟動虛擬機器之前,需要初始化當前的執行環境,具體是由 JniInvocation::Init 實現的:

bool JniInvocation::Init(const char* library) {
    library = GetLibrary(library, buffer); // 獲取虛擬機器動態連結庫的名稱

    handle_ = dlopen(library, kDlopenFlags); // 開啟虛擬機器動態連結庫

    ...

    // 分別查詢 VM 庫中的 3 個重要介面實現
    // 無論是 ART,還是 Dalvik,或者未來的其它虛擬機器都需要實現這 3 個介面,它們是統一的介面標準
    // zygote 通過這 3 個介面控制 Android 的虛擬機器
    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;
}

現在可以啟動和初始化虛擬機器了,核心工作是在 startVm 中完成的:

int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote)
{
    ...
    if (JNI_CreateJavaVM(pJavaVM, pEnv, &initArgs) < 0) {
        ALOGE("JNI_CreateJavaVM failed\n");
        return -1;
    }
    return 0;
}

可以看到,它呼叫了 JniInvocation::Init 中找到的 JNI_CreateJavaVM 介面,該介面用於建立一個虛擬機器:

extern "C" jint JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args) {
  ...
  
  RuntimeOptions options;
  for (int i = 0; i < args->nOptions; ++i) { // 解析所有的虛擬機器選項
    JavaVMOption* option = &args->options[i];
    options.push_back(std::make_pair(std::string(option->optionString), option->extraInfo));
  }
  
  bool ignore_unrecognized = args->ignoreUnrecognized;
  
  if (!Runtime::Create(options, ignore_unrecognized)) { // 建立一個 Runtime 執行環境
    return JNI_ERR;
  }
  
  
  android::InitializeNativeLoader();
  Runtime* runtime = Runtime::Current();
  bool started = runtime->Start(); // 通過 Runtime 來啟動其管理的虛擬機器
  if (!started) { // 啟動失敗
    delete Thread::Current()->GetJniEnv();
    delete runtime->GetJavaVM();
    LOG(WARNING) << "CreateJavaVM failed";
    return JNI_ERR;
  }
  
  *p_env = Thread::Current()->GetJniEnv();
  *p_vm = runtime->GetJavaVM(); // 獲得已經啟動完成的虛擬機器示例,並傳遞給呼叫者
  
  return JNI_OK;
}

Runtime::Start 成功啟動了一個虛擬機器後,會通過 Init 函式來初始化:

bool Runtime::Init(RuntimeArgumentMap&& runtime_options_in) {

  ...
  
  heap_ = new gc::Heap(...); // 建立堆管理物件
  
  ...
  
  java_vm_ = JavaVMExt::Create(this, runtime_options, &error_msg);  // 建立 Java 虛擬機器物件
  
  
  Thread::Startup();
  Thread* self = Thread::Attach("main", false, nullptr, false); // 主執行緒 attach
  
  if (UNLIKELY(IsAotCompiler())) {
    class_linker_ = new AotClassLinker(intern_table_);
  } else {
    class_linker_ = new ClassLinker(intern_table_);
  }
  
  if (GetHeap()->HasBootImageSpace()) { // 當前 Heap 是否包含 Boot Image(比如 boot.art)
    bool result = class_linker_->InitFromBootImage(&error_msg);
    ...
  } else {
    
    if (runtime_options.Exists(Opt::BootClassPathDexList)) {
      boot_class_path.swap(*runtime_options.GetOrDefault(Opt::BootClassPathDexList));
    } else {
      OpenDexFiles(dex_filenames,
                   dex_locations,
                   runtime_options.GetOrDefault(Opt::Image),
                   &boot_class_path);
    }
    
    instruction_set_ = runtime_options.GetOrDefault(Opt::ImageInstructionSet);
    
    if (!class_linker_->InitWithoutImage(std::move(boot_class_path), &error_msg)) {
      LOG(ERROR) << "Could not initialize without image: " << error_msg;
      return false;
    }
    // TODO: Should we move the following to InitWithoutImage?
    SetInstructionSet(instruction_set_);
    ...
  }
  
  return true;
}

總結:

總結:

  1. Android 虛擬機器是 init.rc 被解析的時候啟動的

  2. init.rc 在解析 zygote 的時候,呼叫了 AndroidRuntime 的 start 方法來啟動 Android 虛擬機器

  3. AndroidRuntime 首先通過 JniInvocation::Init 初始化執行環境,找到 JNI_GetDefaultVMInitArgs、JNI_CreateVM 和 JNI_GetCreatedJavaVMs 三個標準介面

  4. 接著執行 startVm,這個方法成功執行後,Android 虛擬機器就被真正啟動了

  5. startVm 內部呼叫之前找到的標準介面之一 JNI_CreateVM

  6. JNI_CreateVM 內部建立了一個 Runtime 執行環境,並通過 Runtime 啟動了虛擬機器

  7. 虛擬機器成功啟動後,呼叫了 Runtime::Init 方法,用於初始化虛擬機器,包括:建立 Java 虛擬機器、建立 Heap 堆管理物件、載入主執行緒等

  8. 最後,通過 onVmCreated 方法通知虛擬機器已經成功建立