本文作者:熊大
引言
在熱修復和外掛化場景中會涉及動態載入 dex,要使它們中程式碼的執行速度與安裝的 APK 相當,需要對它們進行正確的最佳化。根據以往的經驗,在熱修復場景中,錯誤的方式導致 dex 沒有得到最佳化時,修復後 App 的啟動速度比修復前慢 50%。本文將在下面的部分介紹在 Android 5.0 以來的各系統版本中對動態載入的 dex 進行最佳化的方式及原理。
Android 5
從 Android 5.0 開始,系統引入了預先編譯機制(AOT),在應用安裝時,使用 dex2oat 工具將 dex 編譯為可執行檔案。
此時可以透過 DexFile.loadDex
來觸發 dex2oat 最佳化 dex,其呼叫過程如下:
DexFile.loadDex -> new DexFile -> DexFile.openDexFile -> DexFile.openDexFileNative -> DexFile_openDexFileNative -> ClassLinker::OpenDexFilesFromOat -> ClassLinker::CreateOatFileForDexLocation -> ClassLinker::GenerateOatFile
可以看到在 ClassLinker::GenerateOatFile
函式中會執行 dex2oat 命令來最佳化 dex。
// art/runtime/class_linker.cc(android-5.0.2)
bool ClassLinker::GenerateOatFile(const char* dex_filename,
int oat_fd,
const char* oat_cache_filename,
std::string* error_msg) {
// ...
std::vector<std::string> argv;
argv.push_back(dex2oat);
argv.push_back("--runtime-arg");
argv.push_back("-classpath");
argv.push_back("--runtime-arg");
argv.push_back(Runtime::Current()->GetClassPathString());
Runtime::Current()->AddCurrentRuntimeFeaturesAsDex2OatArguments(&argv);
if (!Runtime::Current()->IsVerificationEnabled()) {
argv.push_back("--compiler-filter=verify-none");
}
if (Runtime::Current()->MustRelocateIfPossible()) {
argv.push_back("--runtime-arg");
argv.push_back("-Xrelocate");
} else {
argv.push_back("--runtime-arg");
argv.push_back("-Xnorelocate");
}
if (!kIsTargetBuild) {
argv.push_back("--host");
}
argv.push_back(boot_image_option);
argv.push_back(dex_file_option);
argv.push_back(oat_fd_option);
argv.push_back(oat_location_option);
const std::vector<std::string>& compiler_options = Runtime::Current()->GetCompilerOptions();
for (size_t i = 0; i < compiler_options.size(); ++i) {
argv.push_back(compiler_options[i].c_str());
}
return Exec(argv, error_msg);
}
所以,可用 DexFile.loadDex
進行 dex 最佳化。
Android 7
從 Android 7.0 開始,為解決 AOT 帶來的安裝時間長和佔用空間大等問題,系統引入了配置檔案引導型編譯,結合 AOT 和 即時編譯(JIT)一起使用:
- 應用安裝時不再進行 AOT 編譯
- 在應用的執行過程中,對未編譯的程式碼進行解釋,將執行的方法資訊記錄到配置檔案中,並對經常執行的方法進行 JIT 編譯
- 當裝置閒置和充電時,根據生成的配置檔案對常用程式碼進行 AOT 編譯
配置檔案引導型編譯跟以前的 AOT 編譯的一個主要區別是執行 dex2oat 時使用的編譯過濾器不同,前者使用 speed-profile
,而後者使用 speed
。dex2oat 的所有編譯過濾器定義在 compiler_filter.h
中,在不同系統版本中型別會有變化,主要有以下 4 種:
verify
:僅執行 dex 程式碼驗證quicken
:執行 dex 程式碼驗證,並最佳化一些 dex 指令,以獲得更好的直譯器效能(Android 8 引入,Android 12 移除)speed
:執行 dex 程式碼驗證,並對所有方法進行 AOT 編譯speed-profile
:執行 dex 程式碼驗證,並對配置檔案中列出的方法進行 AOT 編譯
編譯過濾器會影響 dex 最佳化的效果,回到前面給出的最佳化方法 DexFile.loadDex
,其在新系統版本中會使用最佳化所需的編譯過濾器嗎?DexFile.loadDex
在 Android 7.0 上呼叫過程如下:
DexFile.loadDex -> new DexFile -> DexFile.openDexFile -> DexFile.openDexFileNative -> DexFile_openDexFileNative -> OatFileManager::OpenDexFilesFromOat -> OatFileAssistant::MakeUpToDate -> OatFileAssistant::GenerateOatFile
依然會在 OatFileAssistant::GenerateOatFile
函式中執行 dex2oat 命令,使用的編譯過濾器是在調 OatFileAssistant::MakeUpToDate
函式時傳入的 speed
,所以 DexFile.loadDex
在 Android 7.0 上依然適用。
// art/runtime/oat_file_manager.cc(android-7.0.0)
CompilerFilter::Filter OatFileManager::filter_ = CompilerFilter::Filter::kSpeed;
std::vector<std::unique_ptr<const DexFile>> OatFileManager::OpenDexFilesFromOat(
const char* dex_location,
const char* oat_location,
jobject class_loader,
jobjectArray dex_elements,
const OatFile** out_oat_file,
std::vector<std::string>* error_msgs) {
// ...
if (!oat_file_assistant.IsUpToDate()) {
// ...
switch (oat_file_assistant.MakeUpToDate(filter_, /*out*/ &error_msg)) {
// ...
}
}
// ...
}
Android 8
在 Android 8.0 中,DexFile.loadDex
的呼叫過程基本不變,但編譯過濾器改為透過 GetRuntimeCompilerFilterOption
函式得到。
// art/runtime/oat_file_assistant.cc(android-8.0.0)
OatFileAssistant::MakeUpToDate(bool profile_changed, std::string* error_msg) {
CompilerFilter::Filter target;
if (!GetRuntimeCompilerFilterOption(&target, error_msg)) {
return kUpdateNotAttempted;
}
// ...
}
GetRuntimeCompilerFilterOption
函式優先取當前 Runtime
的啟動引數 --compiler-filter
指定的編譯過濾器,如不存在,則用預設的 quicken
。
// art/runtime/oat_file_assistant.cc(android-8.0.0)
static bool GetRuntimeCompilerFilterOption(CompilerFilter::Filter* filter,
std::string* error_msg) {
// ...
*filter = OatFileAssistant::kDefaultCompilerFilterForDexLoading;
for (StringPiece option : Runtime::Current()->GetCompilerOptions()) {
if (option.starts_with("--compiler-filter=")) {
const char* compiler_filter_string = option.substr(strlen("--compiler-filter=")).data();
if (!CompilerFilter::ParseCompilerFilter(compiler_filter_string, filter)) {
// ...
return false;
}
}
}
return true;
}
// art/runtime/oat_file_assistant.h(android-8.0.0)
class OatFileAssistant {
public:
// The default compile filter to use when optimizing dex file at load time if they
// are out of date.
static const CompilerFilter::Filter kDefaultCompilerFilterForDexLoading =
CompilerFilter::kQuicken;
// ...
};
Runtime
的啟動引數 --compiler-filter
的值由設定的系統屬性 vold.decrypt
和 dalvik.vm.dex2oat-filter
決定:
- 如果
vold.decrypt
屬性值等於trigger_restart_min_framework
或1
,則為assume-verified
否則為
dalvik.vm.dex2oat-filter
屬性值// frameworks/base/core/jni/AndroidRuntime.cpp(android-8.0.0) int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote) { // ... property_get("vold.decrypt", voldDecryptBuf, ""); bool skip_compilation = ((strcmp(voldDecryptBuf, "trigger_restart_min_framework") == 0) || (strcmp(voldDecryptBuf, "1") == 0)); // ... if (skip_compilation) { addOption("-Xcompiler-option"); addOption("--compiler-filter=assume-verified"); // ... } else { parseCompilerOption("dalvik.vm.dex2oat-filter", dex2oatCompilerFilterBuf, "--compiler-filter=", "-Xcompiler-option"); } }
透過 adb 用
getprop
命令檢視系統屬性值,可知vold.decrypt
的屬性值不等於trigger_restart_min_framework
或1
,且dalvik.vm.dex2oat-filter
屬性不存在,所以DexFile.loadDex
最終使用的編譯過濾器為quicken
,達不到 dex 最佳化的要求。
無法實現對 dex 的所有方法進行 AOT 編譯,可以退而求其次,透過建立 BaseDexClassLoader
或其子類物件,讓動態載入的 dex 跟安裝的應用一樣,初始只做基本最佳化,隨著程式碼的執行,常用程式碼會被 AOT 編譯。
BaseDexClassLoader
的構造方法會依次執行以下 2 個步驟來分別實現基本最佳化和對常用程式碼進行 AOT 編譯:
- 建立
DexPathList
物件 執行
DexLoadReporter.report
方法// libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java(android-8.0.0) public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) { super(parent); this.pathList = new DexPathList(this, dexPath, librarySearchPath, null); if (reporter != null) { reporter.report(this.pathList.getDexPaths()); } }
首先,建立 DexPathList
物件會觸發建立 DexFile
物件,進而會如前文所述使用編譯過濾器 quicken
執行基本最佳化,呼叫過程如下:
new DexPathList -> DexPathList.makeDexElements -> DexPathList.loadDexFile -> new DexFile
在介紹為什麼 DexLoadReporter.report
方法可以讓動態載入的 dex 能被 AOT 編譯之前,先看看 BaseDexClassLoader.reporter
的來源。在應用啟動過程中,系統會根據 dalvik.vm.usejitprofiles
屬性值來決定是否將 DexLoadReporter
單例設給 BaseDexClassLoader
的靜態變數 reporter
,透過 getprop
命令檢視可知 dalvik.vm.usejitprofiles
屬性值為 true
,所以 BaseDexClassLoader.reporter
的值為 DexLoadReporter
單例。
// frameworks/base/core/java/android/app/ActivityThread.java(android-8.0.0)
private void handleBindApplication(AppBindData data) {
// ...
if (SystemProperties.getBoolean("dalvik.vm.usejitprofiles", false)) {
BaseDexClassLoader.setReporter(DexLoadReporter.getInstance());
}
// ...
}
DexLoadReporter.report
方法透過執行如下 2 步來實現動態載入的 dex 會被系統執行基於配置檔案的 AOT 編譯:
- 向
PackageManagerService
註冊 dex 使用資訊,使系統在執行後臺 dex 最佳化時能獲得動態載入的 dex 資訊進行最佳化 向
VMRuntime
註冊記錄執行的方法資訊的配置檔案,使動態載入的 dex 中的方法被執行時也會被記錄// frameworks/base/core/java/android/app/DexLoadReporter.java(android-8.0.0) public void report(List<String> dexPaths) { // ... // Notify the package manager about the dex loads unconditionally. // The load might be for either a primary or secondary dex file. notifyPackageManager(dexPaths); // Check for secondary dex files and register them for profiling if // possible. registerSecondaryDexForProfiling(dexPaths); }
notifyPackageManager
方法經過如下呼叫過程後,將 dex 資訊註冊到PackageDexUsage
中,並寫入到/data/system/package-dex-usage.list
檔案中。DexLoadReporter.notifyPackageManager -> PackageManagerService.notifyDexLoad -> DexManager.notifyDexLoad -> PackageDexUsage.record -> PackageDexUsage.maybeWriteAsync
registerSecondaryDexForProfiling
方法會以 dex 檔案路徑加.prof
字尾作為路徑建立配置檔案,並將其註冊到VMRuntime
中。在執行過程中會判斷 dex 檔案是否是 secondary dex 檔案,即非安裝的 APK 檔案,判斷方式為 dex 檔案是否位於應用的 data 目錄中,所以需要將動態載入的 dex 放在應用的 data 目錄中。
最後來分析下系統執行後臺 dex 最佳化的流程,看看透過建立 BaseDexClassLoader
或其子類物件的方式註冊到 PackageDexUsage
中的 dex 能否被最佳化。系統會在啟動時向 JobScheduler
註冊後臺 dex 最佳化任務,呼叫過程如下:
SystemServer.run -> SystemServer.startOtherServices -> BackgroundDexOptService.schedule
後臺 dex 最佳化任務會在裝置空閒且充電時執行,任務執行時間間隔至少 1 天。
// frameworks/base/services/core/java/com/android/server/pm/BackgroundDexOptService.java(android-8.0.0)
public static void schedule(Context context) {
JobScheduler js = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
// ...
js.schedule(new JobInfo.Builder(JOB_IDLE_OPTIMIZE, sDexoptServiceName)
.setRequiresDeviceIdle(true)
.setRequiresCharging(true)
.setPeriodic(IDLE_OPTIMIZATION_PERIOD)
.build());
// ...
}
系統執行後臺 dex 最佳化任務的呼叫過程如下:
BackgroundDexOptService.onStartJob -> BackgroundDexOptService.runIdleOptimization -> BackgroundDexOptService.idleOptimization
在 BackgroundDexOptService.idleOptimization
方法中,會根據 dalvik.vm.dexopt.secondary
屬性值決定是否對 secondary dex 進行最佳化,使用 getprop
命令檢視可知該屬性值為 true
,所以後臺 dex 最佳化的目標包含 secondary dex。
// frameworks/base/services/core/java/com/android/server/pm/BackgroundDexOptService.java(android-8.0.0)
private int idleOptimization(PackageManagerService pm, ArraySet<String> pkgs, Context context) {
// ...
if (SystemProperties.getBoolean("dalvik.vm.dexopt.secondary", false)) {
// ...
result = optimizePackages(pm, pkgs, lowStorageThreshold, /*is_for_primary_dex*/ false,
sFailedPackageNamesSecondary);
}
return result;
}
後續對 secondary dex 進行最佳化的呼叫過程如下,最終透過從 ServiceManager
獲取的 installd
服務提供的 dexopt
介面執行 dex 最佳化。
BackgroundDexOptService.optimizePackages -> PackageManagerService.performDexOptSecondary -> DexManager.dexoptSecondaryDex -> DexManager.dexoptSecondaryDex -> PackageDexOptimizer.dexOptSecondaryDexPath -> PackageDexOptimizer.dexOptSecondaryDexPathLI -> Installer.dexopt
在 DexManager.dexoptSecondaryDex
方法中,會先從 PackageDexUsage
獲取已註冊的 dex 資訊,然後執行 dex 最佳化,所以只有已註冊到 PackageDexUsage
中的 dex 能被最佳化。
// frameworks/base/services/core/java/com/android/server/pm/dex/DexManager.java(android-8.0.0)
public boolean dexoptSecondaryDex(String packageName, String compilerFilter, boolean force) {
// ...
PackageUseInfo useInfo = getPackageUseInfo(packageName);
// ...
for (Map.Entry<String, DexUseInfo> entry : useInfo.getDexUseInfoMap().entrySet()) {
// ...
int result = pdo.dexOptSecondaryDexPath(pkg.applicationInfo, dexPath,
dexUseInfo.getLoaderIsas(), compilerFilter, dexUseInfo.isUsedByOtherApps());
// ...
}
// ...
}
public PackageUseInfo getPackageUseInfo(String packageName) {
return mPackageDexUsage.getPackageUseInfo(packageName);
}
前文提到註冊 dex 時會將 dex 資訊寫入檔案,且在系統啟動建立 PackageManagerService
物件時會讀取檔案中的 dex 資訊,呼叫過程如下:
new PackageManagerService -> DexManager.load -> DexManager.loadInternal -> PackageDexUsage.read
所以即使從註冊 dex 到本次系統生命週期結束都沒滿足執行後臺 dex 最佳化條件,但下次系統啟動後,以前註冊的 dex 還可以在滿足執行條件時被最佳化。
installd
服務執行於 installd
守護程式中,該程式在系統啟動時由 init 程式啟動,並在啟動時建立 installd
服務例項註冊到 ServiceManager
中。installd
服務的 dexopt
介面經過如下呼叫過程後,最終會執行 dex2oat 命令。
InstalldNativeService::dexopt -> android::installd::dexopt -> run_dex2oat
經過以上分析,可以確定建立 BaseDexClassLoader
或其子類物件可以讓動態載入的 dex 得到跟安裝的應用一樣的最佳化效果。
Android 10
建立 BaseDexClassLoader
或其子類物件,在 Android 10 及以上系統中,依然能在系統執行後臺 dex 最佳化時對動態載入的 dex 進行最佳化,但從 Android 10 開始,系統引入了 class loader context,要求必須建立 PathClassLoader
或 DexClassLoader
或 DelegateLastClassLoader
的物件,可以選擇用 PathClassLoader
。
// frameworks/base/services/core/java/com/android/server/pm/dex/DexManager.java(android-10.0.0)
/*package*/ void notifyDexLoadInternal(ApplicationInfo loadingAppInfo,
List<String> classLoaderNames, List<String> classPaths, String loaderIsa,
int loaderUserId) {
// ...
String[] classLoaderContexts = DexoptUtils.processContextForDexLoad(
classLoaderNames, classPaths);
// ...
for (String dexPath : dexPathsToRegister) {
// ...
if (searchResult.mOutcome != DEX_SEARCH_NOT_FOUND) {
// ...
if (classLoaderContexts != null) {
// ...
if (mPackageDexUsage.record(searchResult.mOwningPackageName,
dexPath, loaderUserId, loaderIsa, isUsedByOtherApps, primaryOrSplit,
loadingAppInfo.packageName, classLoaderContext)) {
mPackageDexUsage.maybeWriteAsync();
}
}
} else {
// ...
}
// ...
}
}
// frameworks/base/services/core/java/com/android/server/pm/dex/DexoptUtils.java(android-10.0.0)
/*package*/ static String[] processContextForDexLoad(List<String> classLoadersNames,
List<String> classPaths) {
// ...
for (int i = 1; i < classLoadersNames.size(); i++) {
if (!ClassLoaderFactory.isValidClassLoaderName(classLoadersNames.get(i))
|| classPaths.get(i) == null) {
return null;
}
// ...
}
// ...
}
public static boolean isValidClassLoaderName(String name) {
// This method is used to parse package data and does not accept null names.
return name != null && (isPathClassLoaderName(name) || isDelegateLastClassLoaderName(name));
}
從 Android 10 開始,建立 DexFile
物件不再會觸發執行 dex2oat 命令,所以建立 PathClassLoader
物件已無法實現在初始時對 dex 進行基本最佳化。
同時,從 Android 10 開始,SELinux 增加了對應用執行 dex2oat 命令的限制,所以也無法透過 ProcessBuilder
或 Runtime
執行 dex2oat 命令來對 dex 進行基本最佳化。在 file_contexts
檔案中定義了 dex2oat 工具的安全上下文,指定了只有擁有 dex2oat_exec 的許可權的程式才能執行 dex2oat。
# system/sepolicy/private/file_contexts(android-10.0.0)
/system/bin/dex2oat(d)? u:object_r:dex2oat_exec:s0
在 seapp_contexts
檔案中指定了不同 targetSdkVersion 對應的程式安全上下文型別,例如當應用的 targetSdkVersion 大於等於 29 時,其程式安全上下文型別為 untrusted_app。
# system/sepolicy/private/seapp_contexts(android-10.0.0)
user=_app minTargetSdkVersion=29 domain=untrusted_app type=app_data_file levelFrom=all
user=_app minTargetSdkVersion=28 domain=untrusted_app_27 type=app_data_file levelFrom=all
user=_app minTargetSdkVersion=26 domain=untrusted_app_27 type=app_data_file levelFrom=user
user=_app domain=untrusted_app_25 type=app_data_file levelFrom=user
可透過 ps -Z
命令來檢視程式的安全上下文,結果跟規則指定的一致:
- targetSdkVersion = 28:
u:r:untrusted_app_27:s0:c101,c259,c512,c768
- targetSdkVersion = 29:
u:r:untrusted_app:s0:c101,c259,c512,c768
不同程式安全上下文型別所擁有的許可權定義在跟型別對應的檔案中,與 targerSdkVersion 小於 29 的應用對應的許可權規則檔案中宣告瞭應用程式有 dex2oat_exec 型別的讀和執行許可權,而與 targerSdkVersion 大於等於 29 的應用對應的檔案中沒有對 dex2oat_exec 型別的許可權的宣告,所以在 Android 10 及以上系統中,當應用的 targetSdkVersion 大於等於 29 時,無法在應用程式中執行 dex2oat 命令。
# system/sepolicy/private/untrusted_app_27.te(android-10.0.0)
# The ability to invoke dex2oat. Historically required by ART, now only
# allowed for targetApi<=28 for compat reasons.
allow untrusted_app_27 dex2oat_exec:file rx_file_perms;
userdebug_or_eng(`auditallow untrusted_app_27 dex2oat_exec:file rx_file_perms;')
# system/sepolicy/private/untrusted_app.te(android-10.0.0)
typeattribute untrusted_app coredomain;
app_domain(untrusted_app)
untrusted_app_domain(untrusted_app)
net_domain(untrusted_app)
bluetooth_domain(untrusted_app)
PMS 透過 aidl 提供了 performDexOptSecondary
介面,可對 secondary dex 進行最佳化,且能指定編譯過濾器,可用來實現初始時的基本最佳化。該介面透過 Binder
的 shell command 方式對外暴露,呼叫過程如下:
Binder.onTransact -> Binder.shellCommand -> PackageManagerService.onShellCommand -> ShellCommand.exec -> PackageManagerShellCommand.onCommand -> PackageManagerShellCommand.runCompile -> PackageManagerService.performDexOptSecondary
所以可透過反射獲取 PMS 的 Binder
介面例項,然後用對應的 transation code 來調 Binder
的 shell command 介面,傳入調 performDexOptSecondary
介面所需的引數的方式來讓 PMS 執行對 secondary dex 的最佳化。
fun performDexOptSecondaryByShellCommand(context: Context) {
runCatching {
val pm = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String::class.java).invoke(null, "package") as? IBinder
var data: Parcel? = null
var reply: Parcel? = null
val lastIdentity = Binder.clearCallingIdentity()
try {
data = Parcel.obtain()
reply = Parcel.obtain()
data.writeFileDescriptor(FileDescriptor.`in`)
data.writeFileDescriptor(FileDescriptor.out)
data.writeFileDescriptor(FileDescriptor.err)
val args = arrayOf("compile", "-f", "--secondary-dex", "-m", if (Build.VERSION.SDK_INT >= 31) "verify" else "speed-profile", context.packageName)
data.writeStringArray(args)
data.writeStrongBinder(null)
ResultReceiver(Handler(Looper.getMainLooper())).writeToParcel(data, 0)
val shellCommandTransaction: Int = '_'.toInt() shl 24 or ('C'.toInt() shl 16) or ('M'.toInt() shl 8) or 'D'.toInt()
pm?.transact(shellCommandTransaction, data, reply, 0)
reply.readException()
} finally {
reply?.recycle()
data?.recycle()
Binder.restoreCallingIdentity(lastIdentity)
}
}.onFailure { it.printStackTrace() }
}
初始時對 dex 進行基本最佳化與應用安裝對應,使用的編譯過濾器也應保持一致,應用安裝場景使用的編譯過濾器由 pm.dexopt.install
系統屬性指定,其值為 speed-profile
,在 Android 12 及以上系統中,可用新引入的 pm.dexopt.install-bulk-secondary
屬性的值 verify
。
綜上,可結合建立 PathClassLoader
物件和調 PMS 提供的 performDexOptSecondary
介面來對動態載入的 dex 進行效果跟安裝的應用一樣的最佳化。
小結
本文在分析系統相關實現的基礎上,介紹了在 Android 5.0 以來的各系統版本中實現對動態載入的 dex 進行最佳化,使執行速度與安裝的 APK 相當的方式:
- 系統版本大於等於 5.0 且小於 8.0:使用
DexFile.loadDex
- 系統版本大於等於 8.0 且小於 10:建立
PathClassLoader
物件 - 系統版本大於等於 10:建立
PathClassLoader
物件,並透過 PMSBinder
的 shell command 調performDexOptSecondary
介面
參考資料
- ART
- Android SELinux 概念
- Android 各版本原始碼
本文釋出自網易雲音樂技術團隊,文章未經授權禁止任何形式的轉載。我們常年招收各類技術崗位,如果你準備換工作,又恰好喜歡雲音樂,那就加入我們 grp.music-fe(at)corp.netease.com!