温馨提示:这篇文章已超过469天没有更新,请注意相关的内容是否还可用!
摘要:在Android开发中,dex2oat编译优化是一种重要的技术,用于提高应用的运行效率。dex2oat是一种将DEX字节码转换为机器代码的编译工具,通过优化过程,能够显著提升应用的启动速度和运行性能。通过合理配置和使用dex2oat,开发者能够确保Android应用在各种设备上都能流畅运行。
一、ART 即时 (JIT) 编译器实现
Android Runtime (ART) 包含一个具备代码分析功能的即时 (JIT) 编译器,该编译器可以在 Android 应用运行时持续提高其性能。JIT 编译器对 Android 运行组件当前的预先 (AOT) 编译器进行了补充,可以提升运行时性能,节省存储空间,加快应用和系统更新速度。相较于 AOT 编译器,JIT 编译器的优势也更为明显,因为在应用自动更新期间或在无线下载 (OTA) 更新期间重新编译应用时,它不会拖慢系统速度。
尽管 JIT 和 AOT 使用相同的编译器,它们所进行的一系列优化也较为相似,但它们生成的代码可能会有所不同。JIT 会利用运行时类型信息,可以更高效地进行内联,并可让堆栈替换 (OSR) 编译成为可能,而这一切都会使其生成的代码略有不同。
- 1、OAT文件可以直接运行
- 2、非OAT文件需要解释运行
- 3、如果是代码达到热点的方法的阈值,会让JIT在后台编译,编译之后添加到缓存中
- 4、JIT Code Cache 中的代码等到下次运行时如果命中缓存,直接使用缓存代码执行(Code Cache是内存缓存)
JIT 编译涉及以下活动
- 用户运行应用,此举随后触发 ART 加载 .dex 文件。
- 如果有 .oat 文件(即 .dex 文件的 AOT 二进制文件),ART 会直接使用该文件。虽然 .oat 文件会定期生成,但文件中不一定会包含经过编译的代码(即 AOT 二进制文件)。
- 如果 .oat 文件不含经过编译的代码,ART 会通过 JIT 和解释器执行 .dex 文件。
- 针对任何未根据 speed 编译过滤器编译的应用启用 JIT(也就是说,要尽可能多地编译应用中的代码)。
- 将 JIT 配置文件数据转储到只有该应用可以访问的系统目录下的文件中。
- AOT 编译 ( dex2oat ) 守护程序通过解析该文件来推进其编译。
JIT 工作流程
(注意:JIT解释器解释完其实可以立即执行,改图未标注)
- 分析信息会存储在代码缓存中,并会在内存紧张时作为垃圾被回收。
- 无法保证在应用处于后台运行状态时所捕获的快照能够包含完整的数据(即 JIT 编译的所有内容)。
- 该过程不会尝试确保记录所有内容(因为这会影响运行时性能)。
- 方法可能有三种不同的状态:
- 已经过解释(dex 代码)
- 已经过 JIT 编译
- 已经过 AOT 编译 如果同时存在 JIT 和 AOT 代码(例如,由于反复进行逆优化),经过 JIT 编译的代码将是首选代码。
- 在不影响前台应用性能的情况下运行 JIT 所需的内存取决于相关应用。大型应用比小型应用需要更多内存。一般来说,大型应用所需的内存稳定维持在 4 MB 左右。
- Carbage Collect清理内存,为JIT Code Cache腾出空间
二、dex2oat编译
在 compiler_filter.h ,我们可以看到dex2oat一共有12种编译模式:
enum Filter { VerifyNone, // Skip verification but mark all classes as verified anyway. kVerifyAtRuntime, // Delay verication to runtime, do not compile anything. kVerifyProfile, // Verify only the classes in the profile, compile only JNI stubs. kInterpretOnly, // Verify everything, compile only JNI stubs. kTime, // Compile methods, but minimize compilation time. kSpaceProfile, // Maximize space savings based on profile. kSpace, // Maximize space savings. kBalanced, // Good performance return on compilation investment. 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. };
注意:android官方上出现了quicken模式解释为:“运行 DEX 代码验证,并优化一些 DEX 指令,以获得更好的解译器性能”,因此可以推断出interpret-only和quicken相似
以上12种编译模式 按照排列次序逐渐增强 ,那系统默认采用了哪些编译模式呢?我们可以在在手机上执行 getprop | grep pm 查看:
pm.dexopt.ab-ota: [speed-profile] pm.dexopt.bg-dexopt: [speed-profile] pm.dexopt.boot: [verify-profile] pm.dexopt.core-app: [speed] pm.dexopt.first-boot: [interpret-only] pm.dexopt.forced-dexopt: [speed] pm.dexopt.install: [interpret-only] pm.dexopt.nsys-library: [speed] pm.dexopt.shared-apk: [speed]
[dalvik.vm.heapmaxfree]: [8m] [dalvik.vm.heapminfree]: [512k] [persist.radio.apm_sim_not_pwdn]: [1] [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]
其中有几个我们是特别关心的,
- install (应用安装)与 first-boot (应用首次启动)使用的是[interpret-only],即只verify,代码解释执行即不编译任何的机器码,它的性能与Dalvik时完全一致,先让用户愉快的玩耍起来。
- ab-ota (系统升级)与 bg-dexopt (后台编译)使用的是[speed-profile],即只根据“热代码”的profile配置来编译。这也是N中混合编译的核心模式。
- 对于动态加载的代码,即 forced-dexopt ,它采用的是[speed]模式,即最大限度的编译机器码,它的表现与之前的AOT编译一致。
总的来说,程序使用loaddex动态加载的代码是无法享受混合编译带来的好处,我们应当尽量 采用ClassN.dex方式来符合Google的规范 。这不仅在ota还是混合编译上,都会带来很大的提升。
dex2oat支持的编译模式
注意:Android没有明确对balanced支持,另外,android默认支持的编译模式是quicken。
N版本中dex2oat的原理及模式
N版本当中强化了JIT模式。JIT模式是Just in time 的简称。意思是在运行的时候,根据method有使用频率来决定是否要对某一个方法进行优化。虚拟机会统计每一个方法被执行的次数。如果某一个方法执行的次数比较多,达到一定的阈值,就会将升级为hot method,并将其记录在一个profile当中。在系统空闲并且在充电的时候,只将这些方法进行优化。在运行的时候,也会对这些方法进行优化,以方便在运行的时候使用。
dex2oat的命令行参数
首先我们先看一下dex2oat都支持一些什么样的命令行参数:
通用类
- -j:编译时使用多少个线程。缺省值为默认的CPU核数。例:-j8
输入输出的文件类
- --dex-file=:待编译的.dex, .jar或者.apk文件
- --dex-location=:dex文件的路径
- --zip-fd=:包含classes.dex文件的zip的文件描述符
- --zip-location=:zip文件路径
- --oat-file=:输出的oat文件名
- --oat-fd=:输出文件描述符
- --oat-location=:输出的oat文件的路径
- --oat-symbols=:指定输出完整符号的oat路径
- --image=:指定输出的image文件名
- --image-classes=:指定preloaded-classes的路径,例:--image=frameworks/base/preloaded-classes
- --base=:指定boot image的基地址,例:--base=0x50000000
- --boot-image=:指定boot class的文件。例:--boot-image=/system/framework/boot.art,默认值为:$ANDROID_ROOT/system/framework/boot.art
- --android-root=:portable linking所用的库的路径。例:--android-root=out/host/linux-x86,默认值:$ANDROID_ROOT
指令集类
- --instruction-set=(arm|arm64|mips|mips64|x86|x86_64):指定编译的指令集。例:--instruction-set=x86,默认:arm
- --instruction-set-features=。例:--instruction-set-features=div,默认:default
编译器优化选项类
- --compile-pic:Force indirect use of code, methods, and classes. 默认:disabled
- --compiler-filter=(verify-none| interpret-only| space |balanced |speed |everything |time):选择compiler filter。例:--compiler-filter=everything。默认值:speed
- --huge-method-max=:巨型方法的指令数,用于编译器调优。例:--huge-method-max=10000,默认值:10000
- --large-method-max=:大型方法的指令数,用于编译器调优。例:--large-method-max=600,默认值:600
- --small-method-max=:小型方法的指令数,用于编译器调优。例:--small-method-max=60,默认值:60
- --tiny-method-max=:微型方法的指令数,用于编译器调优。例:--tiny-method-max=20,默认值:20
- --num-dex-methods=:小型dex文件的方法上限,用于编译器调优。如果发现是个小型的dex文件,而编译器的filter不是interpret-only或者verify-none的话,就用speed filter。例:--num-dex-method=900,默认值:900
- --inline-depth-limit=:编译器调优用,只建议开发和实验用。例:--inline-depth-limit=5,默认值:5
- --inline-max-code-units=:inline调优用,实验用。例:--inline-max-code-units=100,默认值:100
- --dump-timing: 显示时间都花到哪儿去了。
重定位信息类
- --include-patch-information:编译时包含patch信息,可以在不重编的情况下重定位。
- --no-include-patch-information:不包含patch信息。
调试信息类
- -g:与--generate-debug-info相同
- --generate-debug-info:生成所有可用的调试信息。可以通过标准的strip命令或者objcopy命令来压缩掉无用信息。
- --no-generate-debug-info:不生成调试信息
运行参数类
- --runtime-arg :指定运行时参数,如:初始堆大小,最大堆大小,详细输出等。每次只能传一个参数。例:--runtime-arg -Xms256m
- --profile-file=:指定profile信息,供编译器例用
编译选项类
- --print-pass-names: 打印pass name信息
- --disable-passes=:禁止某些pass项,例:--disable-passes=UseCount,BBOptimizations
- --print-pass-options:打印当前配置的pass信息
- --pass-options=Pass1Name:Pass1OptionName:Pass1Option#,Pass2Name:Pass2OptionName:Pass2Option#:指定pass信息。
临时文件类
- --swap-file=:指定交换文件,例:--swap-file=/data/tmp/swap.001
- --swap-fd=:指定交换文件的描述符
使用示例:
/system/bin/dex2oat --zip-fd=6 --zip-location=/data/app/-1/base.apk --oat-fd=7 --oat-location=/data/dalvik-cache/arm/data@app@-1@base.apk@classes.dex --instruction-set=arm --instruction-set-features=div --runtime-arg -Xms64m --runtime-arg -Xmx512m --swap-fd=8
三、dex2oat编译优化
强制编译
dex2oat在Android中属于系统命令,因此我们无法使用去编译其他app,但是Android同样提供了通过PMS执行此命令的方式
要强制编译,请运行以下命令:
adb shell cmd package compile
强制编译特定软件包的常见用例:
- 基于配置文件:
adb shell cmd package compile -m speed-profile -f my-package
- 全面:
adb shell cmd package compile -m speed -f my-package
强制编译所有软件包的常见用例:
- 基于配置文件:
adb shell cmd package compile -m speed-profile -f -a
- 全面:
adb shell cmd package compile -m speed -f -a
编译插件的DEX
我们经常遇到热修复、插件话等情况,但是系统提供的命令无法支持编译插件或者补丁包,下面是一种可行的方式,只能编译app本身的
public class Dex2OatHelper { private final String TAG = "Dex2OatHelper"; public void makeDex2OatV1(String dexFilePath, String oatFilePath) throws IOException { final File oatFile = new File(oatFilePath); if (!oatFile.exists()) { oatFile.getParentFile().mkdirs(); } try { final List commandAndParams = new ArrayList(); commandAndParams.add("dex2oat"); // for 7.1.1, duplicate class fix if (Build.VERSION.SDK_INT >= 24) { commandAndParams.add("--runtime-arg"); commandAndParams.add("-classpath"); commandAndParams.add("--runtime-arg"); commandAndParams.add("&"); } commandAndParams.add("--dex-file=" + dexFilePath); commandAndParams.add("--oat-fd=" + oatFilePath); commandAndParams.add("--instruction-set=" + Build.CPU_ABI); if (Build.VERSION.SDK_INT > 25) { commandAndParams.add("--compiler-filter=quicken"); } else { commandAndParams.add("--compiler-filter=interpret-only"); } final ProcessBuilder pb = new ProcessBuilder(commandAndParams); pb.redirectErrorStream(true); final Process dex2oatProcess = pb.start(); StreamConsumer.consumeInputStream(dex2oatProcess.getInputStream()); StreamConsumer.consumeInputStream(dex2oatProcess.getErrorStream()); try { final int ret = dex2oatProcess.waitFor(); if (ret != 0) { throw new IOException("dex2oat works unsuccessfully, exit code: " + ret); } } catch (InterruptedException e) { throw new IOException("dex2oat is interrupted, msg: " + e.getMessage(), e); } } finally { } } public static boolean makeDex2OatV2(String dexFilePath, String oatFilePath){ try { DexFile.loadDex(dexFilePath, oatFilePath, 0); }catch (Exception e){ e.printStackTrace(); return false; } return true; } private static class StreamConsumer { static final Executor STREAM_CONSUMER = Executors.newSingleThreadExecutor(); static void consumeInputStream(final InputStream is) { STREAM_CONSUMER.execute(new Runnable() { @Override public void run() { if (is == null) { return; } final byte[] buffer = new byte[256]; try { while ((is.read(buffer)) > 0) { // To satisfy checkstyle rules. } } catch (IOException ignored) { // Ignored. } finally { try { is.close(); } catch (Exception ignored) { // Ignored. } } } }); } } }
清除配置文件数据
要清除配置文件数据并移除经过编译的代码,请运行以下命令:
- 针对一个软件包:
adb shell cmd package compile --reset my-package
- 针对所有软件包:
adb shell cmd package compile --reset -a
- 启动后台优化
adb shell cmd package bg-dexopt-job
以下是一段编译案例
D/dexOptimize: quicken开始优化,app总数1 I/PackageManager.DexOptimizer: Running dexopt (dexoptNeeded=1) on: /data/app/com.lwjfork.example-aSx3aJfLVJcl_MGpXnDDtg==/base.apk pkg=com.lwjfork.example isa=arm dexoptFlags=boot_complete,debuggable,public,enable_hidden_api_checks targetFilter=quicken oatDir=/data/app/com.lwjfork.example-aSx3aJfLVJcl_MGpXnDDtg==/oat classLoaderContext=PCL[]{PCL[/system/framework/org.apache.http.legacy.jar]} D/dexOptimize: {0: package='com.lwjfork.example', appSize='0.0MB', isOptimized=true, costTime=4819} D/dexOptimize: 优化完成,总计耗时:5154, 编译成功率:100.0%, 平均耗时:5154.0
代码层面
我们从PackageManagerShellCommand找到相关参数的用法
pw.println(" compile [-m MODE | -r REASON] [-f] [-c]"); pw.println(" [--reset] [--check-prof (true | false)] (-a | TARGET-PACKAGE)"); pw.println(" Trigger compilation of TARGET-PACKAGE or all packages if \"-a\"."); pw.println(" Options:"); pw.println(" -a: compile all packages"); pw.println(" -c: clear profile data before compiling"); pw.println(" -f: force compilation even if not needed"); pw.println(" -m: select compilation mode"); pw.println(" MODE is one of the dex2oat compiler filters:"); pw.println(" assume-verified"); pw.println(" extract"); pw.println(" verify"); pw.println(" quicken"); pw.println(" space-profile"); pw.println(" space"); pw.println(" speed-profile"); pw.println(" speed"); pw.println(" everything"); pw.println(" -r: select compilation reason"); pw.println(" REASON is one of:"); for (int i = 0; i
最终我们会调用到
PackageDexOptimizer中的performDex相关方法
这里会自动补充一些普通用户无法获取到的dex2oat的参数
/** * Performs dexopt on all code paths and libraries of the specified package for specified * instruction sets. * *
Calls to {@link com.android.server.pm.Installer#dexopt} on {@link #mInstaller} are * synchronized on {@link #mInstallLock}. */ int performDexOpt(PackageParser.Package pkg, String[] sharedLibraries, String[] instructionSets, boolean checkProfiles, String targetCompilationFilter, CompilerStats.PackageStats packageStats, boolean isUsedByOtherApps) { if (!canOptimizePackage(pkg)) { return DEX_OPT_SKIPPED; } synchronized (mInstallLock) { final long acquireTime = acquireWakeLockLI(pkg.applicationInfo.uid); try { return performDexOptLI(pkg, sharedLibraries, instructionSets, checkProfiles, targetCompilationFilter, packageStats, isUsedByOtherApps); } finally { releaseWakeLockLI(acquireTime); } } } /** * Performs dexopt on all code paths of the given package. * It assumes the install lock is held. */ @GuardedBy("mInstallLock") private int performDexOptLI(PackageParser.Package pkg, String[] sharedLibraries, String[] targetInstructionSets, boolean checkForProfileUpdates, String targetCompilerFilter, CompilerStats.PackageStats packageStats, boolean isUsedByOtherApps) { final String[] instructionSets = targetInstructionSets != null ? targetInstructionSets : getAppDexInstructionSets(pkg.applicationInfo); final String[] dexCodeInstructionSets = getDexCodeInstructionSets(instructionSets); final List paths = pkg.getAllCodePaths(); final int sharedGid = UserHandle.getSharedAppGid(pkg.applicationInfo.uid); final String compilerFilter = getRealCompilerFilter(pkg.applicationInfo, targetCompilerFilter, isUsedByOtherApps); final boolean profileUpdated = checkForProfileUpdates && isProfileUpdated(pkg, sharedGid, compilerFilter); final String sharedLibrariesPath = getSharedLibrariesPath(sharedLibraries); // Get the dexopt flags after getRealCompilerFilter to make sure we get the correct flags. final int dexoptFlags = getDexFlags(pkg, compilerFilter); // Get the dependencies of each split in the package. For each code path in the package, // this array contains the relative paths of each split it depends on, separated by colons. String[] splitDependencies = getSplitDependencies(pkg); int result = DEX_OPT_SKIPPED; for (int i = 0; i
接下来到Installer类的dexopt方法
public void dexopt(String apkPath, int uid, @Nullable String pkgName, String instructionSet, int dexoptNeeded, @Nullable String outputPath, int dexFlags, String compilerFilter, @Nullable String volumeUuid, @Nullable String sharedLibraries, @Nullable String seInfo) throws InstallerException { assertValidInstructionSet(instructionSet); if (!checkBeforeRemote()) return; try { mInstalld.dexopt(apkPath, uid, pkgName, instructionSet, dexoptNeeded, outputPath, dexFlags, compilerFilter, volumeUuid, sharedLibraries, seInfo); } catch (Exception e) { throw InstallerException.from(e); } }
编译效果对比
默认: random : 160.0ms 200.0ms 160.0ms 190.0ms 200.0ms 平均:182ms createApplication : 20.0ms 20.0ms 20.0ms 20.0ms 20.0ms 平均:20ms recordReciever: 170.0ms 170.0ms 240.0ms 130.0ms 160.0ms 平均:174ms createTestActivity : 860.0ms 400.0ms 560.0ms 380.0ms 370.0ms 平均:514ms createMainActivity: 1060.0ms 980.0ms 1020.0ms 980.0ms 960.0ms 平均:1000ms layoutResId : 1070.0ms 1030.0ms 1050.0ms 1090.0ms 1000.0ms 平均:1048ms createTestService : 180.0ms 190.0ms 250.0ms 150.0ms 180.0ms 平均:170ms md5: 390.0ms 450.0ms 450.0ms 470.0ms 400.0ms 平均:432ms appSize : 2.54MB quicken模式 首次编译耗时: 5154ms random : 200.0ms 210.0ms 220.0ms 150.0ms 190.0ms 平均:194ms createApplication : 20.0ms 20.0ms 20.0ms 30.0ms 20.0ms 平均:22ms recordReciever: 280.0ms 140.0ms 150.0ms 150.0ms 170.0ms 平均:178ms createTestActivity : 620.0ms 380.0ms 470.0ms 350.0ms 420.0ms 平均:448ms createMainActivity: 710.0ms 640.0ms 690.0ms 660.0ms 680.0ms 平均:676ms layoutResId : 720.0ms 660.0ms 770.0ms 670.0ms 700.0ms 平均:704ms createTestService : 270.0ms 150.0ms 180.0ms 160.0ms 160.0ms 平均:184ms md5: 460.0ms 400.0ms 490.0ms 390.0ms 480.0m 平均:444ms appSize : 4.46MB speed模式 首次编译耗时: 7125ms random : 200.0ms 190.0ms 140.0ms 160.0ms 200.0ms 平均:178ms createApplication : 20.0ms 20.0ms 30.0ms 20.0ms 20.0ms 平均:22ms recordReciever: 160.0ms 150.0ms 150.0ms 120.0ms 130.0ms 平均:142ms createTestActivity : 420.0ms 400.0ms 430.0ms 360.0ms 360.0ms 平均:394ms createMainActivity: 720.0ms 780.0ms 670.0ms 680.0ms 680.0ms 平均:706ms layoutResId : 680.0ms 710.0ms 650.0ms 640.0ms 660.0ms 平均:668ms createTestService : 220.0ms 150.0ms 150.0ms 130.0ms 130.0ms 平均:156ms md5: 570.0ms 430.0ms 520.0ms 420.0ms 400.0ms 平均:468ms appSize : 4.46MB everything模式 首次编译耗时: 9020ms random : 210.0ms 180.0ms 160.0ms 300.0ms 200.0ms 平均:210ms createApplication : 20.0ms 20.0ms 20.0ms 20.0ms 20.0ms 平均:20ms recordReciever: 190.0ms 160.0ms 160.0ms 190.0ms 150.0ms 平均:170ms createTestActivity : 510.0ms 400.0ms 390.0ms 470.0ms 420.0ms 平均:438ms createMainActivity: 670.0ms 680.0ms 770.0ms 710.0ms 730.0ms 平均:438ms layoutResId : 670.0ms 670.0ms 670.0ms 680.0ms 680.0ms 平均:712ms createTestService : 210.0ms 190.0ms 180.0ms 190.0ms 150.0ms 平均:184ms md5: 430.0ms 550.0ms 440.0ms 410.0ms 400.0ms 平均:446ms appSize : 4.46MB
- 针对一个软件包:
- 基于配置文件:
- 基于配置文件:
- 用户运行应用,此举随后触发 ART 加载 .dex 文件。
还没有评论,来说两句吧...