愿历尽千帆 归来仍少年

ART 虚拟机|Dex2oat 调优实践之路

字数统计: 2,946阅读时长: 13 min
2022/03/20

写在前面

我们都知道Dex2oat对应用的启动速度、以及日常使用的流畅度起到了重要作用,不过Google现有的dex2oat机制存在着明显的局限性。

  1. 开机/OTA等场景,容易整机卡顿及开机时间长。
  2. Idle下的触发条件苛刻,发挥空间有限。

前段时间研究了下dex2oat的原理机制,并且对一些场景下的dex2oat行为做了客制化,打算做下小结,以便后续回顾完善。

涉及代码均基于Android S

基本概述

一个App经过优化dex2oat的大致流程如下
摘自谷歌开发者网站
(图片来源:谷歌开发者网站)

简单的理解就是优化是以dex文件中的method为单位,dex2oat在优化时,会根据需要优化一定量的method,也就是说并不是优化的method都会被翻译成oat模式。

根据优化的method的量的多少,分为如下几种:

1
2
3
4
5
6
7
8
9
10
11
enum Filter {
kAssumeVerified, // Skip verification but mark all classes as verified anyway.
kExtract, // Delay verication to runtime, do not compile anything.
kVerify, // Only verify classes.
kSpaceProfile, // Maximize space savings based on profile.
kSpace, // Maximize space savings.
kSpeedProfile, // Maximize runtime performance based on profile.
kSpeed, // Maximize runtime performance.
kEverythingProfile, // Compile everything capable of being compiled based on profile.
kEverything, // Compile everything capable of being compiled.
};

上面列举出的这些模式,简单理解就是越往下的模式,优化的程度越深,自然而然性能会越加流畅,不过耗费的时间以及空间也会随之增长。

触发时机


(图片来源:谷歌开发者网站)

  1. 首次开机
    开机过程中PMS扫描手机中的各个目录,安装的时候,会对这些App进行dex2oat。如果说App比较多,或者dex2oat比较慢,那么开机时间就会比较长。
  2. 应用安装/启动
    其实流程还是会走PMS流程,由PMS执行安装的运行,并发起dex2oat的动作。
  3. 系统空闲时
    系统处于空闲时,且满足一系列的条件比如充电等,会对应用进行dex2oat,在正式dex2oat之前同样会进行一系列的判断,比如根据profile的更新量决定是否进行本次优化,这部分也是我们客制化的重点。
    对应到源码中,触发的原因会更加丰富一些
1
2
3
4
5
6
7
8
9
10
11
12
// Compilation reasons.
public static final int REASON_UNKNOWN = -1;
public static final int REASON_FIRST_BOOT = 0;
public static final int REASON_BOOT = 1;
public static final int REASON_INSTALL = 2;
public static final int REASON_BACKGROUND_DEXOPT = 3;
public static final int REASON_AB_OTA = 4;
public static final int REASON_INACTIVE_PACKAGE_DOWNGRADE = 5;
public static final int REASON_SHARED = 6;
public static final int REASON_AGRESSIVE = 10000;

public static final int REASON_LAST = REASON_SHARED;

我们可以通过命令查询项目上的配置情况

1
2
3
4
5
6
7
8
lihaizhou@lihaizhou:~$ adb shell getprop |grep pm.dex
[pm.dexopt.ab-ota]: [speed-profile]
[pm.dexopt.bg-dexopt]: [speed-profile]
[pm.dexopt.boot]: [verify]
[pm.dexopt.first-boot]: [quicken]
[pm.dexopt.inactive]: [verify]
[pm.dexopt.install]: [speed-profile]
[pm.dexopt.shared]: [speed]

这里顺便提一下,从前面的描述,我们知道系统处于idle且满足充电条件下,才会触发后台dex2oat。那么这里的idle是如何定义的,需要等多长时间呢?
frameworks/base/core/res/res/values/config.xml

1
<integer name="config_jobSchedulerInactivityIdleThreshold">1860000</integer>

对于jobscheduler而言,这个阈值设定的是31min,当用户没有交互超过31min后会认为当前设备处于idle状态。

原理机制

本文只讨论idle下的dex2oat,其它场景下如install、开机后的dex2oat等,流程几乎是一致的。
BackgroundDexOptService @schedule

1
2
3
4
5
6
7
// Schedule a daily job which scans installed packages and compiles
// those with fresh profiling data.
js.schedule(new JobInfo.Builder(JOB_IDLE_OPTIMIZE, sDexoptServiceName)
.setRequiresDeviceIdle(true)
.setRequiresCharging(true)
.setPeriodic(IDLE_OPTIMIZATION_PERIOD)
.build());

可以看到需要满足的条件还是比较苛刻的,需要满足idle状态,且处于充电条件下,检测周期为一天,均满足的情况下才会触发dex2oat。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Override
public boolean onStartJob(JobParameters params) {
if (DEBUG) {
Slog.i(TAG, "onStartJob");
}

// NOTE: PackageManagerService.isStorageLow uses a different set of criteria from
// the checks above. This check is not "live" - the value is determined by a background
// restart with a period of ~1 minute.
PackageManagerService pm = (PackageManagerService)ServiceManager.getService("package");
if (pm.isStorageLow()) {
Slog.i(TAG, "Low storage, skipping this run");
return false;
}

final ArraySet<String> pkgs = pm.getOptimizablePackages();
if (pkgs.isEmpty()) {
Slog.i(TAG, "No packages to optimize");
return false;
}

mThermalStatusCutoff =
SystemProperties.getInt("dalvik.vm.dexopt.thermal-cutoff", THERMAL_CUTOFF_DEFAULT);

boolean result;
if (params.getJobId() == JOB_POST_BOOT_UPDATE) {
result = runPostBootUpdate(params, pm, pkgs);
} else {
result = runIdleOptimization(params, pm, pkgs);
}

return result;
}

主要关注runIdleOptimization,略过部分流程。
BackgroundDexOptService @optimizePackage frameworks/base/services/core/java/com/android/server/pm/BackgroundDexOptService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
*
* Optimize package if needed. Note that there can be no race between
* concurrent jobs because PackageDexOptimizer.performDexOpt is synchronized.
* @param pm An instance of PackageManagerService
* @param pkg The package to be downgraded.
* @param isForPrimaryDex. Apps can have several dex file, primary and secondary.
* @return true if the package was downgraded.
*/
private boolean optimizePackage(PackageManagerService pm, String pkg,
boolean isForPrimaryDex) {
int reason = PackageManagerService.REASON_BACKGROUND_DEXOPT;
int dexoptFlags = DexoptOptions.DEXOPT_CHECK_FOR_PROFILES_UPDATES
| DexoptOptions.DEXOPT_BOOT_COMPLETE
| DexoptOptions.DEXOPT_IDLE_BACKGROUND_JOB;

// System server share the same code path as primary dex files.
// PackageManagerService will select the right optimization path for it.
return (isForPrimaryDex || PLATFORM_PACKAGE_NAME.equals(pkg))
? performDexOptPrimary(pm, pkg, reason, dexoptFlags)
: performDexOptSecondary(pm, pkg, reason, dexoptFlags);
}

我们以performDexOptPrimary为例,继续往下看

1
2
3
4
5
6
private boolean performDexOptPrimary(PackageManagerService pm, String pkg, int reason,
int dexoptFlags) {
int result = trackPerformDexOpt(pkg, /*isForPrimaryDex=*/ false,
() -> pm.performDexOptWithStatus(new DexoptOptions(pkg, reason, dexoptFlags)));
return result == PackageDexOptimizer.DEX_OPT_PERFORMED;
}

frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java

1
2
3
4
5
6
7
8
9
/**
* Perform dexopt on the given package and return one of following result:
* {@link PackageDexOptimizer#DEX_OPT_SKIPPED}
* {@link PackageDexOptimizer#DEX_OPT_PERFORMED}
* {@link PackageDexOptimizer#DEX_OPT_FAILED}
*/
/* package */ int performDexOptWithStatus(DexoptOptions options) {
return performDexOptTraced(options);
}

省略部分中间调用过程
PackageDexOptimizer.java@ performDexOptLI
frameworks/base/services/core/java/com/android/server/pm/PackageDexOptimizer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* Performs dexopt on all code paths of the given package.
* It assumes the install lock is held.
*/
@GuardedBy("mInstallLock")
private int performDexOptLI(AndroidPackage pkg, @NonNull PackageSetting pkgSetting,
String[] targetInstructionSets, CompilerStats.PackageStats packageStats,
PackageDexUsage.PackageUseInfo packageUseInfo, DexoptOptions options) {
//....
final String compilerFilter = getRealCompilerFilter(pkg,
options.getCompilerFilter(), isUsedByOtherApps);
// If we don't have to check for profiles updates assume
// PROFILE_ANALYSIS_DONT_OPTIMIZE_SMALL_DELTA which will be a no-op with respect to
// profiles.
final int profileAnalysisResult = options.isCheckForProfileUpdates()
? analyseProfiles(pkg, sharedGid, profileName, compilerFilter)
: PROFILE_ANALYSIS_DONT_OPTIMIZE_SMALL_DELTA;
// Get the dexopt flags after getRealCompilerFilter to make sure we get the correct
// flags.
final int dexoptFlags = getDexFlags(pkg, pkgSetting, compilerFilter, options);

for (String dexCodeIsa : dexCodeInstructionSets) {
int newResult = dexOptPath(pkg, pkgSetting, path, dexCodeIsa, compilerFilter,
profileAnalysisResult, classLoaderContexts[i], dexoptFlags, sharedGid,
packageStats, options.isDowngrade(), profileName, dexMetadataPath,
options.getCompilationReason());
//....
}
//....
}
  1. getRealCompilerFilter:获取该pkg实际的编译模式
  2. analyseProfiles:检测profile是否更新,并确定是否需要优化。
  3. dexOptPath:实际的优化工作

略过部分中间调用流程,dexOptPath函数经过层层调用,最终会通过跨进程来到
frameworks/native/cmds/installd/dexopt.cpp @ dexopt
该函数内容较长,拆分开来

  1. 根据应用的路径解析其dex文件
1
2
3
4
5
6
UniqueFile in_dex(open(dex_path, O_RDONLY, 0), dex_path);
if (in_dex.fd() < 0) {
*error_msg = StringPrintf("installd cannot open '%s' for input during dexopt", dex_path);
LOG(ERROR) << *error_msg;
return -1;
}
  1. 生成对应的oat文件
1
2
3
4
5
6
RestorableFile out_oat =
open_oat_out_file(dex_path, oat_dir, is_public, uid, instruction_set, is_secondary_dex);
if (out_oat.fd() < 0) {
*error_msg = "Could not open out oat file.";
return -1;
}
  1. 调用dex2oat 二进制文件去做实际的oat工作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bool use_dex2oat64 = false;
// Check whether the device even supports 64-bit ABIs.
if (!GetProperty("ro.product.cpu.abilist64", "").empty()) {
use_dex2oat64 = GetBoolProperty("dalvik.vm.dex2oat64.enabled", false);
}
const char* dex2oat_bin = select_execution_binary(
(use_dex2oat64 ? kDex2oat64Path : kDex2oat32Path),
(use_dex2oat64 ? kDex2oatDebug64Path : kDex2oatDebug32Path),
background_job_compile);

auto execv_helper = std::make_unique<ExecVHelper>();

LOG(VERBOSE) << "DexInv: --- BEGIN '" << dex_path << "' ---";

RunDex2Oat runner(dex2oat_bin, execv_helper.get());
runner.Initialize(out_oat.GetUniqueFile(), out_vdex.GetUniqueFile(), out_image.GetUniqueFile(),
in_dex, in_vdex, dex_metadata, reference_profile, class_loader_context,
join_fds(context_input_fds), swap_fd.get(), instruction_set, compiler_filter,
debuggable, boot_complete, for_restore, target_sdk_version,
enable_hidden_api_checks, generate_compact_dex, use_jitzygote_image,
compilation_reason);

至此,idle下的dex2oat流程梳理完毕。

画了一个核心的调用过程

影响因素

dex2oat 优化后虽然能够增加应用运行的流畅度, 但是如果在短时间内大量发起则会影响用户界面操作, 造成整机性能问题,所以我们对dex2oat的预期效果就是尽可能的快完成,尽可能不影响用户。
那么有哪些因素会影响dex2oat的执行时长呢?

  1. 工作量
    dex2oat发生的时候,会将原文件中的dex文件抽出来,逐个指令的判断,然后进行翻译,并生成大量的中间内容。并占用所有有CPU(目前的策略是有多少个核,就启动多少个线程)。
    对于单个应用而言,选择不同的模式该应用优化的耗时也会不一样,比如选择everything和interpret-only这两种不同的模式,耗时可能会悬殊几倍。
  2. 线程数
    启动的线程数越多,dex2oat就会在越短的时间内完成
    启动的线程数以及耗时时长可以通过日志打印查看
1
2
3
4
5
6
7
8
9
10
11
12
void LogCompletionTime() {
// Note: when creation of a runtime fails, e.g., when trying to compile an app but when there
// is no image, there won't be a Runtime::Current().
// Note: driver creation can fail when loading an invalid dex file.
LOG(INFO) << "dex2oat took "
<< PrettyDuration(NanoTime() - start_ns_)
<< " (" << PrettyDuration(ProcessCpuNanoTime() - start_cputime_ns_) << " cpu)"
<< " (threads: " << thread_count_ << ") "
<< ((Runtime::Current() != nullptr && driver_ != nullptr) ?
driver_->GetMemoryUsageString(kIsDebugBuild || VLOG_IS_ON(compiler)) :
"");
}
  1. EMMC性能
    如果dex文件比较大,会产生大量的中间内容,这些在memory当中是保存不下的,所以采用了swap机制,会将一些内容交换到EMMC当中,而且有大量的读操作,同时会将结果保存至emmc当中,所以emmc的性能也是非常关键的。
  2. 负载:整机负载较高的话,dex2oat容易拿不到足够的cpu资源,也会造成耗时变长。
  3. 内存:如果处于低内存情况下的话,容易引起IO拉升,可能会导致dex2oat遇到IO上的瓶颈。

优化方案

我们对dex2oat的客制化优化方案如下图所示:

基本思路:

  1. 增加周期性后台优化策略,同样需要满足一定的条件如idle下且Charge情况下,对于热点应用以及小的dex文件进行优化。
  2. 为了避免开机时间过长,系统启动过程中不再对所有应用进行dex2oat,开机后接收到开机广播delay十分钟,对热点应用进行dex2oat优化。
  3. 根据系统负载的情况,动态调整dex2oat使用的线程数以及编译模式。

为了测试应用在优化后的性能表现,通过高速相机测试做完dex2oat后应用启动速度均值提升15%左右,掉帧情况也得到了大幅缓解。

拓展

我们顺便介绍下谷歌的Baseline profile
Android 9 (API 级别 28) 在 Play Cloud 中引入了 ART 优化配置文件,以缩短应用启动时间。
基准配置文件在构建时创建,作为 APK 的一部分发送到 Play 中,然后在下载应用时,从 Play 发送至用户。

(图片摘自开发者网站)
从上面的描述我们可以得知,profile文件由应用开发者按照谷歌提供的规则,在构建时就创建好,然后通过play商店上架。当用户下载该应用时,profile连同apk会一起安装,这样就可以确保用户在首次安装时便能享受到性能的提升。
这种做法相对于系统现有的dex2oat机制来说,所做的事情显得轻量很多,对系统负担也会大幅降低。
因为profile由应用开发者提供,这就使得profile变得不再神秘。因为开发者有源代码,并且熟悉自己的业务流程,这样的话开发者本地构建profile后可以不断的调试并测试性能的数据,最终提供一份最优的基准profile。

小结

至此,关于dex2oat原理及客制化修改介绍到这里。
其实不管是谷歌的dex2oat策略,还是厂商的客制化方案,其目的都是明确的:
尽可能在用户的使用过程中,能够享受到直接运行机器码所带来的性能提升,同时避免dex2oat本身的工作对系统带来的负面影响。

参考文献

https://blog.csdn.net/feelabclihu/article/details/105502166
https://developer.android.google.cn/

CATALOG
  1. 1. 写在前面
  2. 2. 基本概述
  3. 3. 触发时机
  4. 4. 原理机制
  5. 5. 影响因素
  6. 6. 优化方案
  7. 7. 拓展
  8. 8. 小结
  9. 9. 参考文献