Android10 dex2oat實踐

最近看到一篇部落格:Android性能優化之Android 10+ dex2oat實踐,對這個優化很感興趣,打算研究研究能否接入到項目中。不過該部落格只講述了思路,沒有給完整源碼。本項目參考該部落格的思路,實現了該方案。

源碼地址://github.com/carverZhong/DexOpt

一、dex2oat 詳解

以下是官方對於dex2oat的解釋:

ART 使用預先 (AOT) 編譯,並且從 Android 7.0(代號 Nougat,簡稱 N)開始結合使用 AOT、即時 (JIT) 編譯和配置文件引導型編譯。所有這些編譯模式的組合均可配置,我們將在本部分中對此進行介紹。例如,Pixel 設備配置了以下編譯流程:

  1. 最初安裝應用時不進行任何 AOT 編譯。應用前幾次運行時,系統會對其進行解譯,並對經常執行的方法進行 JIT 編譯。

  2. 當設備閑置和充電時,編譯守護程式會運行,以便根據在應用前幾次運行期間生成的配置文件對常用程式碼進行 AOT 編譯。

  3. 下一次重新啟動應用時將會使用配置文件引導型程式碼,並避免在運行時對已經過編譯的方法進行 JIT 編譯。在應用後續運行期間經過 JIT 編譯的方法將會添加到配置文件中,然後編譯守護程式將會對這些方法進行 AOT 編譯。

ART 包括一個編譯器(dex2oat 工具)和一個為啟動 Zygote 而載入的運行時 (libart.so)。dex2oat 工具接受一個 APK 文件,並生成一個或多個編譯工件文件,然後運行時將會載入這些文件。文件的個數、擴展名和名稱因版本而異,但在 Android 8 版本中,將會生成以下文件:

.vdex:其中包含 APK 的未壓縮 DEX 程式碼,以及一些旨在加快驗證速度的元數據。
.odex:其中包含 APK 中已經過 AOT 編譯的方法程式碼。
.art (optional):其中包含 APK 中列出的某些字元串和類的 ART 內部表示,用於加快應用啟動速度。(配置 ART)

也就是說,dex2oat可以觸發APK的AOT編譯,並生成對應的產物,APP運行時會載入這些文件。執行過AOT編譯的產物能加快啟動速度、程式碼執行效率。

二、程式碼實現

具體原理還是參考部落格:Android性能優化之Android 10+ dex2oat實踐。這裡說下實現上的細節。
部落格的思路是通過一些手段觸發系統來進行dex2oat。

1.整體思路

  1. PackageManagerShellCommand.runCompile方法可以觸發Secondary Apk進行dex2oat,但是Secondary Apk需要先註冊。

  2. 註冊的邏輯在IPackageManagerImpl.registerDexModule,其中IPackageManagerImplPackageManagerService的內部類,並繼承了IPackageManager.Stub

  3. 最後,再執行PackageManagerShellCommand.runreconcileSecondaryDexFiles反註冊,就大功告成了。

所以整體分三步走:

  • 註冊Secondary Apk

  • 執行dex2oat

  • 反註冊Secondary Apk

2.註冊Secondary Apk

IPackageManager是個AIDL介面,而應用中的ApplicationPackageManage剛好持有這個AIDL介面,因此可以通過其調用registerDexModule方法。

為此,可以通過反射調用registerDexModule方法。以下是核心實現:

// 註冊Secondary Apk
private fun registerDexModule(apkFilePath: String): Boolean {
    try {
        val callbackClazz = ReflectUtil.findClass("android.content.pm.PackageManager\$DexModuleRegisterCallback")
        ReflectUtil.callMethod(
            getCustomPM(),
            "registerDexModule",
            arrayOf(apkFilePath, null),
            arrayOf(String::class.java, callbackClazz)
        )
        return true
    } catch (thr: Throwable) {
        Log.e(TAG, "registerDexModule: thr.", thr)
    }
    return false
}


/**
 * 創建一個自定義的 PackageManager,避免影響正常的 PackageManager
 */
private fun getCustomPM(): PackageManager {
    val customPM = cacheCustomPM
    if (customPM != null && cachePMBinder?.isBinderAlive == true) {
        return customPM
    }
    val pmBinder = getPMBinder()
    val pmBinderDynamicProxy = Proxy.newProxyInstance(
        context.classLoader, ReflectUtil.getInterfaces(pmBinder::class.java)
    ) { _, method, args ->
        if ("transact" == method.name) {
            // FLAG_ONEWAY => NONE.
            args[3] = 0
        }
        method.invoke(pmBinder, *args)
    }
    val pmStubClass = ReflectUtil.findClass("android.content.pm.IPackageManager\$Stub")
    val pmStubProxy = ReflectUtil.callStaticMethod(pmStubClass,
        "asInterface",
        arrayOf(pmBinderDynamicProxy),
        arrayOf(IBinder::class.java))
    val contextImpl = if (context is ContextWrapper) context.baseContext else context
    val appPM = createAppPM(contextImpl, pmStubProxy!!)
    cacheCustomPM = appPM
    return appPM
}

3.執行dex2oat

這裡有個難點就是,如何才能調用到PackageManagerShellCommand.runCompile?看下調用邏輯:

// 程式碼位於PackageManagerService.java。
// IPackageManagerImpl是PackageManagerService的內部類。
@Override
public void onShellCommand(FileDescriptor in, FileDescriptor out,
                FileDescriptor err, String[] args, ShellCallback callback,
                ResultReceiver resultReceiver) {
    (new PackageManagerShellCommand(this, mContext, mDomainVerificationManager.getShell()))
                    .exec(this, in, out, err, args, callback, resultReceiver);
}

IPackageManager.Stub繼承了Binder,而這個方法是Binder中的,調用邏輯如下:

// Binder.java
protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply,
                             int flags) throws RemoteException {
  if (code == INTERFACE_TRANSACTION) {
    reply.writeString(getInterfaceDescriptor());
    return true;
  } else if (code == DUMP_TRANSACTION) {
    // 省略部分程式碼...
    return true;
  } else if (code == SHELL_COMMAND_TRANSACTION) {
    ParcelFileDescriptor in = data.readFileDescriptor();
    ParcelFileDescriptor out = data.readFileDescriptor();
    ParcelFileDescriptor err = data.readFileDescriptor();
    String[] args = data.readStringArray();
    ShellCallback shellCallback = ShellCallback.CREATOR.createFromParcel(data);
    ResultReceiver resultReceiver = ResultReceiver.CREATOR.createFromParcel(data);
    try {
      if (out != null) {
        // 重點!!!調用了 shellCommand 方法
        shellCommand(in != null ? in.getFileDescriptor() : null,
                out.getFileDescriptor(),
                err != null ? err.getFileDescriptor() : out.getFileDescriptor(),
                args, shellCallback, resultReceiver);
      }
    } finally {
      // 省略部分程式碼...
    }
    return true;
  }
  return false;
}

public void shellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out,
                         @Nullable FileDescriptor err,
                         @NonNull String[] args, @Nullable ShellCallback callback,
                         @NonNull ResultReceiver resultReceiver) throws RemoteException {
    // 這裡調用的!!!
    onShellCommand(in, out, err, args, callback, resultReceiver);
}

所以這裡邏輯清晰了,再次整理下邏輯:

  • Binder.onTransact收到 SHELL_COMMAND_TRANSACTION 命令會執行 shellCommand方法

  • shellCommand方法又調用了onShellCommand方法

  • IPackageManager.Stub繼承了Binder

  • IPackageManagerImpl繼承了IPackageManager.Stub並重寫了onShellCommand方法

  • IPackageManagerImpl的onShellCommand執行了PackageManagerShellCommand相關邏輯

所以我們的核心是找到IPackageManager.aidl,並向其發送 SHELL_COMMAND_TRANSACTION 命令。得益於Android Binder機制,我們可以在應用進程拿到IPackageManger的Binder,並通過它來發送命令。

程式碼實現如下:

// 執行dex2oat
private fun performDexOpt() {
    val args = arrayOf(
        "compile", "-f", "--secondary-dex", "-m",
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) "verify" else "speed-profile",
        context.packageName
    )
    executeShellCommand(args)
}

// IPackageManager.aidl 發送 SHELL_COMMAND_TRANSACTION 命令
private fun executeShellCommand(args: Array<String>) {
    val lastIdentity = Binder.clearCallingIdentity()
    var data: Parcel? = null
    var reply: Parcel? = null
    try {
        data = Parcel.obtain()
        reply = Parcel.obtain()
        data.writeFileDescriptor(FileDescriptor.`in`)
        data.writeFileDescriptor(FileDescriptor.out)
        data.writeFileDescriptor(FileDescriptor.err)
        data.writeStringArray(args)
        data.writeStrongBinder(null)
        resultReceiver.writeToParcel(data, 0)
        getPMBinder().transact(SHELL_COMMAND_TRANSACTION, data, reply, 0)
        reply.readException()
    } catch (t: Throwable) {
        Log.e(TAG, "executeShellCommand error.", t)
    } finally {
        data?.recycle()
        reply?.recycle()
    }
    Binder.restoreCallingIdentity(lastIdentity)
}

4.反註冊Secondary Apk

反註冊也是執行PackageManagerShellCommand相關方法,只不過給的參數不一樣。所以大部分邏輯跟第三步是一樣的。程式碼實現如下:

private fun reconcileSecondaryDexFiles() {
    val args = arrayOf("reconcile-secondary-dex-files", context.packageName)
    executeShellCommand(args)
}

最後,本項目的程式碼組織情況如下:

  • DexOpt:外部調用介面,執行DexOpt.dexOpt即可開啟dex2oat。

  • ApkOptimizerN:負責Android7-Android9的dex2oat邏輯。

  • ApkOptimizerQ:負責Android10的dex2oat邏輯。也是本文的講解重點。

三、優缺點

把這項技術應用到了一個插件化項目中,對插件APK進行dex2oat優化,總結下其優缺點。

1.優點

  • 插件的載入速度大大增加(實測可以達到90%以上),對插件化框架的冷啟動有很大的意義。
  • 程式碼運行的速度有微小的提升。測試了跳轉Activity、Service這些場景,能夠提升20-80ms左右,跟機型有很大的關係。

2.缺點

  • dex2oat產物也會佔用一定的存儲空間。所以如果插件更新記得及時刪除老的oat文件。
  • dex2oat 執行時間較長,首次還是建議直接載入插件,在後台執行dex2oat優化。
  • 部分手機執行後沒有成功生成oat文件,還是存在機型兼容問題。
Tags: