本文作者:熊大
引言
在热修复和插件化场景中会波及动静加载 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!