最近看到一篇部落格:Android效能優化之Android 10+ dex2oat實踐,對這個優化很感興趣,打算研究研究能否接入到專案中。不過該部落格只講述了思路,沒有給完整原始碼。本專案參考該部落格的思路,實現了該方案。
原始碼地址:https://github.com/carverZhong/DexOpt
以下是官方對於dex2oat的解釋:
ART 使用預先 (AOT) 編譯,並且從 Android 7.0(代號 Nougat,簡稱 N)開始結合使用 AOT、即時 (JIT) 編譯和組態檔引導型編譯。所有這些編譯模式的組合均可設定,我們將在本部分中對此進行介紹。例如,Pixel 裝置設定了以下編譯流程:
最初安裝應用時不進行任何 AOT 編譯。應用前幾次執行時,系統會對其進行解譯,並對經常執行的方法進行 JIT 編譯。
當裝置閒置和充電時,編譯守護程式會執行,以便根據在應用前幾次執行期間生成的組態檔對常用程式碼進行 AOT 編譯。
下一次重新啟動應用時將會使用組態檔引導型程式碼,並避免在執行時對已經過編譯的方法進行 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。
PackageManagerShellCommand.runCompile
方法可以觸發Secondary Apk進行dex2oat,但是Secondary Apk需要先註冊。
註冊的邏輯在IPackageManagerImpl.registerDexModule
,其中IPackageManagerImpl
是PackageManagerService
的內部類,並繼承了IPackageManager.Stub
。
最後,再執行PackageManagerShellCommand.runreconcileSecondaryDexFiles
反註冊,就大功告成了。
所以整體分三步走:
註冊Secondary Apk
執行dex2oat
反註冊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
}
這裡有個難點就是,如何才能呼叫到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)
}
反註冊也是執行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優化,總結下其優缺點。