Android性能优化之Android 10+ dex2oat实践
阅读原文时间:2023年07月13日阅读:1

作者:字节跳动终端技术——郭海洋

背景

对于Android App的性能优化来说,方式方法以及工具都有很多,而dex2oat作为其中的一员,却可能不被大众所熟知。它是Android官方应用于运行时,针对dex进行编译优化的程序,通过对dex进行一系列的指令优化、编译机器码等操作,提升dex加载速度代码运行速度,从而提升安装速度、启动速度、以及应用使用过程中的流畅度,最终提升用户日常的使用体验。

它的适用范围也比较广,可以用于Primary ApkSecondary Apk常规场景插件场景。(Primary Apk是指的常规场景下的主包base.apk)或者插件场景下的宿主包Secondary Apk是指的常规场景下的自行加载的包(.apk)或者插件场景下的插件包(.apk))。

而随着Android系统版本的更迭,发现原本可以在应用进程上触发dex2oat编译的方式,却在targetSdkVersion>=29Android 10+的系统上,不再允许使用。其原因是系统在targetSdkVersion=29的时候,对此做了限制,不允许应用进程上触发dex2oat编译(Android 运行时 (ART) 不再从应用进程调用 dex2oat。这项变更意味着 ART 将仅接受系统生成的 OAT 文件)(OATdex2oat后的产物)。

那当前是否会受到这个限制的影响呢?

2020年的时候Android 11系统正式发布,各大应用市场就开始限制ApptargetSdkVersion>=29,而Android 11系统距今已经发布一年之久,也就意味着,现如今ApptargetSdkVersion>=29是不可避免的。而且随着新Android设备的不断迭代,越来越多的用户,使用上了携带新系统的新机器,使得Android 10+系统的占有量逐步增加,目前为止Android 10+系统的占有量约占整体的30%~40%左右,也就是说这部分机器将会受到这个限制的影响。

那这个限制有什么影响呢?

这个限制的关键是,不允许应用进程上触发dex2oat编译,换句话说就是并不影响系统自身去触发dex2oat编译,那么限制的影响也就是,影响那些需要通过应用进程去触发dex2oat编译的场景。

对于Primary ApkSecondary Apk,它们在常规场景插件场景下,系统都会收集其运行时的热点代码并用于dex2oat进行编译优化。此处触发dex2oat编译是系统行为,并不受限于上述限制。但触发此处dex2oat编译的条件是比较苛刻的,它要求设备必须处于空闲状态且要连接电源,而且其校验的间隔是一天。

在上述条件下,由系统触发的dex2oat编译,基本上很难触发,从而导致dex加载速度下降80%以上,代码运行速度下降11%以上,使得应用的ANR率提升、流畅度下降,最终影响用户的日常使用体验。

对于之前来说改进方案就是通过应用进程触发dex2oat编译来弥补系统触发dex2oat编译的不足,而如今因限制会导致部分机器无法生效。

如何才能让用户体会到dex2oat带来的体验提升呢?问题又如何解决呢?

下面通过探索,一步步的逼近真相,解决问题~

探索

探索之前,先明确下核心点,本次探索的目标就是为了让用户体会到dex2oat带来的体验提升,其最大的阻碍就是系统触发dex2oat的编译条件太苛刻,导致难以触发,之前的成功实践就是基于App维度手动触发dex2oat编译来弥补系统触发dex2oat的编译的不足。

而现在仍需探索的原因就是,原本的成功实践,目前在某些机器上已经受限,为了完成目标,解决掉现有的问题,自然而然的想法就是,限制究竟是什么?限制是如何生效的?是否可以绕过?

目前对于限制的理解,应该仅限于背景中的描述,那Google官方是怎么说的呢?

Android 运行时 (ART) 不再从应用进程调用 dex2oat。这项变更意味着 ART 将仅接受系统生成的 OAT 文件。(Android 运行时只接受系统生成的 OAT 文件

通过Google官方的描述大致可以理解为,原本ART会从应用进程调用dex2oat,现在不再从应用进程调用dex2oat了,从而使得应用进程没有时机触发dex2oat,从而达到限制App维度触发dex2oat的目的。

但问题确实有这么简单嘛?

通过对比Android 9Android 10的代码时发现,Android 9在构建ClassLoader的时候会触发dex2oat,但是 Android 10 上相关代码已经被移除,此处同Google官方的说法一致。

但如果限制仅仅如此的话,可以按照原本ART从应用进程调用dex2oat的方式,然后手动从应用进程调用就可以了。

由于Android`` ``10相关代码已经移除,所以查看下Android 9的代码,看下之前是如何从应用进程调用dex2oat的,相关代码链接:https://android.googlesource.com/platform/art/+/refs/tags/android-9.0.0_r52/runtime/oat_file_assistant.cc#698,通过查看代码可以看出,是通过拼接dex2oat的命令来触发执行的,按照如上代码,拼接dex2oat命令的伪代码如下:

//step1&nbsp;拼接命令List<String>&nbsp;commandAndParams&nbsp;=&nbsp;new&nbsp;ArrayList<>();commandAndParams.add("dex2oat");if&nbsp;(Build.VERSION.SDK_INT&nbsp;>=&nbsp;24)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;commandAndParams.add("--runtime-arg");&nbsp;&nbsp;&nbsp;&nbsp;commandAndParams.add("-classpath");&nbsp;&nbsp;&nbsp;&nbsp;commandAndParams.add("--runtime-arg");&nbsp;&nbsp;&nbsp;&nbsp;commandAndParams.add("&");}commandAndParams.add("--instruction-set="&nbsp;+&nbsp;getCurrentInstructionSet());//&nbsp;verify-none|interpret-only|verify-at-runtime|space|balanced|speed|everything|time//编译模式,不同的模式,影响最终的运行速度和磁盘大小的占用if&nbsp;(mode&nbsp;==&nbsp;Dex2OatCompMode.FASTEST_NONE)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;commandAndParams.add("--compiler-filter=verify-none");}&nbsp;else&nbsp;if&nbsp;(mode&nbsp;==&nbsp;Dex2OatCompMode.FASTER_ONLY_VERIFY)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;//快速编译&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(Build.VERSION.SDK_INT&nbsp;>&nbsp;25)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;commandAndParams.add("--compiler-filter=quicken");&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;else&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;commandAndParams.add("--compiler-filter=interpret-only");&nbsp;&nbsp;&nbsp;&nbsp;}}&nbsp;else&nbsp;if&nbsp;(mode&nbsp;==&nbsp;Dex2OatCompMode.SLOWLY_ALL)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;//全量编译&nbsp;&nbsp;&nbsp;&nbsp;commandAndParams.add("--compiler-filter=speed");}//源码路径(apk&nbsp;or&nbsp;dex路径)commandAndParams.add("--dex-file="&nbsp;+&nbsp;sourceFilePath);//dex2oat产物路径commandAndParams.add("--oat-file="&nbsp;+&nbsp;optimizedFilePath);String[]&nbsp;cmd=&nbsp;commandAndParams.toArray(new&nbsp;String[commandAndParams.size()]);//step2&nbsp;执行命令Runtime.getRuntime().exec(cmd)

将上述拼接的dex2oat命令在Android`` ``9机器的App进程触发执行,确实得到符合预期的dex2oat产物,并可以正常加载和使用,说明命令拼接的是OK的,然后将上述命令在Android 10targetSdkVersion>=29机器的App进程触发执行,发现并没有得到dex2oat产物,并且得到如下日志:

type=1400&nbsp;audit(0.0:569):&nbsp;avc:&nbsp;denied&nbsp;{&nbsp;execute&nbsp;}&nbsp;for&nbsp;name="dex2oat"&nbsp;dev="dm-2"&nbsp;ino=222&nbsp;scontext=u:r:untrusted_app:s0:c12,c257,c512,c768&nbsp;tcontext=u:object_r:dex2oat_exec:s0&nbsp;tclass=file&nbsp;permissive=0

这个日志说明了什么呢?

可以看到日志信息里有avc: denied关键词,说明此操作受SELinux规则管控,并被拒绝。

在进行日志分析之前,先补充一下SELinux的相关知识,下面是Google官方的说明:

Android 使用安全增强型 Linux (SELinux) 对所有进程强制执行强制访问控制 (MAC),甚至包括以 Root/超级用户权限运行的进程(Linux 功能)

简单说,SELinux就是Android系统以进程维度对其进行强制访问控制的管理体系。SELinux是依靠配置的规则对进程进行约束访问权限。

下面回归正题,分析下日志。

日志细节分析如下:

  • type=1400 :表示SYSCALL
  • denied { ``execute`` }:表示执行权限被拒绝;
  • scontext=u:r:``untrusted_app``:s0:c12,c257,c512,c768:表示主体的安全上下文,其中untrusted_appsource type
  • tcontext=u:object_r:``dex2oat_exec``:s0 :表示目标资源的安全上下文,其中dex2oat_exectarget type
  • tclass=file:表示目标资源的class类型
  • permissive=0:当前的SELLinux模式,1表示permissive(宽松的),0表示enforcing(严格的)

简单的说就是,当在Android 10targetSdkVersion>=29的机器上的App进程上执行拼接的dex2oat命令的时候,是由untrusted_app ****触发dex2oat_exec 而由于untrusted_app的规则限制,导致其触发dex2oat_execexecute权限被拒绝。

下面简单总结一下:

  1. 限制1:Android 10+系统删除了在构建ClassLoader时触发dex2oat的相关代码,来限制从应用进程触发dex2oat的入口。
  2. 限制2:Android 10+系统的相关SELinux规则变更,限制targetSdkVersion>=29的时候从应用进程触发dex2oat

现在通过查阅相关代码和SELinux规则以及使用代码验证,真正的见识到了限制到底是什么样子的,又是如何生效的,以及真真切切的感受到它的威力……

那既然知道限制是什么以及限制如何生效的了,那是否可以绕过呢?

通过上面对限制的了解,可以先大胆的假设:

  1. targetSdkVersion设置小于29
  2. 伪装应用进程为系统进程
  3. 关闭Android系统的SELinux检测
  4. 修改规则移除限制

下面开始小心求证,上述假设是否可行?

对于假设1来说,如果全局设置targetSdkVersion小于29的话,则会影响App后续在应用商店的上架,如果局部设置targetSdkVersion小于29的话,不仅难以修改且时机难以把握,dex2oat是单独的进程进行编译操作的,不同的进程对其进行触发编译的时候,会将进程的targetSdkVersion信息作为参数传给它,用于它内部逻辑的判断,而进程信息是存在于系统进程的。

对于假设2来说,目前还没相关的已知操作可以做到类似效果…

对于假设3来说,Android系统确实也提供了关闭SELinux检测的方法,但是需要Root权限。

对于假设4来说,如果全局修改规则,需要重新编译系统,才可以生效,如果局部修改规则(内存中修改),此处所需的权限也比较高,也无权操作。

所以,从目前来看,绕过基本不可行了…

那怎么办?限制绕不过去,目标无法达成了…

或许谜底就在谜面上,既然Android系统限制只能使用系统生成的,那我们就用系统生成的?

只需要让系统可以感知到我们的操作,可以根据我们提供的操作去生成,可以由我们去控制生成的时机以及效果,这样不如同在应用进程触发dex2oat有一样的效果了嘛?

那如何操作呢?

系统是否提供了可以供应用进程触发系统行为,然后由系统触发dex2oat的方式?

通过查阅Android的官方文档以及相关代码发现可以通过如下方式进行操作(强制编译):

  • 基于配置文件编译:adb shell cmd package compile -m speed-profile -f my-package
  • 全面编译:adb shell cmd package compile -m speed -f my-package

上述命令不仅支持选择编译模式(speed-profile or speed),而且还可以选择特定的App进行操作(my-package)。

通过运行上述命令发现确实可以在targetSdkVersion>=29Android 10+的系统上编译出对应的dex2oat产物,且可以正常加载使用!!!

但是上述命令仅支持Primary Apk并不支持Secondary Apk,感觉它的功能还不止于此,还可以继续挖掘一下这个命令的潜力,下面看下这个命令的实现。

分析之前需要先确定命令对应的代码实现,这里使用了个小技巧,通过故意输错命令,发现最终崩溃的位置在PackageManagerShellCommand,然后通过debug源码,梳理了一下完整的代码调用流程,细节如下。

为了方便理解,下面将代码的调用流程使用时序图描述出来。

下图为Primary Apk的编译流程:

无法复制加载中的内容

在梳理Primary Apk的编译流程的时候,发现代码中也有处理Secondary Apk的方法,下面梳理流程如下:

无法复制加载中的内容

然后根据其代码,梳理其编译命令为:adb shell cmd package compile -m speed -f --secondary-dex my-package

至此,我们已经得到了一种可以借助命令使系统触发dex2oat编译的方式,且可以支持Primary ApkSecondary Apk

还有一些细节需要注意,Primary Apk的命令传入的是App的包名,Secondary Apk的命令传入的也是包名,那哪些Secondary Apk会参与编译呢?

这就涉及到Secondary Apk的注册了,只有注册了的Secondary Apk才会参与编译。

下面是Secondary Apk注册的流程:

无法复制加载中的内容

对于Secondary Apk来说只注册不反注册也不行,因为对于Secondary Apk来说,每次编译仅想编译新增的或者未被编译过的,对于已经编译过的,是不想其仍参与编译,所以这些已经编译过的,就需要进行反注册。

下面是Secondary Apk反注册的流程:

无法复制加载中的内容

而且通过查看源码发现,触发此处的方式其实有两种:

  1. 方式一:使用adb shell cmd package + 命令。例如adb shell cmd package compile -m quicken com.bytedance.demo ,其含义就是触发runCompile方法,然后指定编译模式为quicken,指定编译的包名为com.bytedance.demo,由于没有指定是Secondary,所以按照Primary编译。然后其底层通过socket+binder完成通信,最终交由PackageManagerBinder处理。
  2. 方式二:使用PackageManagerBinder,并设定code=SHELL_COMMAND_TRANSACTION,然后将命令以数组的形式封装到data内即可。

对于方式一来说,依赖adb的实现,底层通信需要依赖socket + binder,而对于方式二来说,底层通信直接使用binder,相比来说更高效,所以最终选择第二种方式。

下面简单的总结一下。

在得知限制无法被绕过后,就想到是否可以使得应用进程可以触发系统行为,然后由系统触发dex2oat,然后通过查阅官方文档找到对应的adb命令可以满足诉求,不过此时仅看到Primary Apk的相关实现,然后继续通过查看代码验证其流程,找到Secondary Apk的相关实现,然后根据实际场景的需要,又继续查看代码,找到注册Secondary Apk和反注册Secondary Apk的方法,然后通过对比adb命令的实现和binder的实现差异,最终选用binder的实现方式,来完成上述操作。

既然探索已经完成,那么下面就根据探索的结果,完成落地实践,并验证其效果。

实践

示例代码如下:

//执行快速编译@Overridepublic&nbsp;void&nbsp;dexOptQuicken(String&nbsp;pluginPackageName,&nbsp;int&nbsp;version)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;//step1:如果没有初始化则初始化&nbsp;&nbsp;&nbsp;&nbsp;maybeInit();&nbsp;&nbsp;&nbsp;&nbsp;//step2:将apk路径进行注册到PMS&nbsp;&nbsp;&nbsp;&nbsp;registerDexModule(pluginPackageName,&nbsp;version);&nbsp;&nbsp;&nbsp;&nbsp;//step3:使用binder触发快速编译&nbsp;&nbsp;&nbsp;&nbsp;dexOpt(COMPILE_FILTER_QUICKEN,&nbsp;pluginPackageName,&nbsp;version);&nbsp;&nbsp;&nbsp;&nbsp;//step4:将apk路径反注册到PMS&nbsp;&nbsp;&nbsp;&nbsp;unregisterDexModule(pluginPackageName,&nbsp;version);}//执行全量编译@Overridepublic&nbsp;void&nbsp;dexOptSpeed(String&nbsp;pluginPackageName,&nbsp;int&nbsp;version)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;//step1:如果没有初始化则初始化&nbsp;&nbsp;&nbsp;&nbsp;maybeInit();&nbsp;&nbsp;&nbsp;&nbsp;//step2:将apk路径进行注册到PMS&nbsp;&nbsp;&nbsp;&nbsp;registerDexModule(pluginPackageName,&nbsp;version);&nbsp;&nbsp;&nbsp;&nbsp;//step3:使用binder触发全量编译&nbsp;&nbsp;&nbsp;&nbsp;dexOpt(COMPILE_FILTER_SPEED,&nbsp;pluginPackageName,&nbsp;version);&nbsp;&nbsp;&nbsp;&nbsp;//step4:将apk路径反注册到PMS&nbsp;&nbsp;&nbsp;&nbsp;unregisterDexModule(pluginPackageName,&nbsp;version);}

&nbsp;/**&nbsp;*&nbsp;Try&nbsp;To&nbsp;Init&nbsp;(Build&nbsp;Base&nbsp;env)&nbsp;*/private&nbsp;void&nbsp;maybeInit()&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(mContext&nbsp;==&nbsp;null&nbsp;||&nbsp;mPmBinder&nbsp;!=&nbsp;null)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;&nbsp;&nbsp;PackageManager&nbsp;packageManager&nbsp;=&nbsp;mContext.getPackageManager();&nbsp;&nbsp;&nbsp;&nbsp;Field&nbsp;mPmField&nbsp;=&nbsp;safeGetField(packageManager,&nbsp;"mPM");&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(mPmField&nbsp;==&nbsp;null)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;&nbsp;&nbsp;mPmObj&nbsp;=&nbsp;safeGetValue(mPmField,&nbsp;packageManager);&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(!(mPmObj&nbsp;instanceof&nbsp;IInterface))&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;&nbsp;&nbsp;IInterface&nbsp;mPmInterface&nbsp;=&nbsp;(IInterface)&nbsp;mPmObj;&nbsp;&nbsp;&nbsp;&nbsp;IBinder&nbsp;binder&nbsp;=&nbsp;mPmInterface.asBinder();&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(binder&nbsp;!=&nbsp;null)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;mPmBinder&nbsp;=&nbsp;binder;&nbsp;&nbsp;&nbsp;&nbsp;}}&nbsp;/**&nbsp;*&nbsp;DexOpt&nbsp;(Add&nbsp;Retry&nbsp;Function)&nbsp;*/private&nbsp;void&nbsp;dexOpt(String&nbsp;compileFilter,&nbsp;String&nbsp;pluginPackageName,&nbsp;int&nbsp;version)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;String&nbsp;tempFilePath&nbsp;=&nbsp;PluginDirHelper.getTempSourceFile(pluginPackageName,&nbsp;version);&nbsp;&nbsp;&nbsp;&nbsp;String&nbsp;tempCacheDirPath&nbsp;=&nbsp;PluginDirHelper.getTempDalvikCacheDir(pluginPackageName,&nbsp;version);&nbsp;&nbsp;&nbsp;&nbsp;String&nbsp;tempOatDexFilePath&nbsp;=&nbsp;tempCacheDirPath&nbsp;+&nbsp;File.separator&nbsp;+&nbsp;PluginDirHelper.getOatFileName(tempFilePath);&nbsp;&nbsp;&nbsp;&nbsp;File&nbsp;tempOatDexFile&nbsp;=&nbsp;new&nbsp;File(tempOatDexFilePath);&nbsp;&nbsp;&nbsp;&nbsp;for&nbsp;(int&nbsp;retry&nbsp;=&nbsp;1;&nbsp;retry&nbsp;<=&nbsp;MAX_RETRY_COUNT;&nbsp;retry++)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;execCmd(buildDexOptArgs(compileFilter),&nbsp;null);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(tempOatDexFile.exists())&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;break;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;&nbsp;&nbsp;}}&nbsp;/**&nbsp;*&nbsp;Register&nbsp;DexModule(dex&nbsp;path)&nbsp;To&nbsp;PMS&nbsp;*/private&nbsp;void&nbsp;registerDexModule(String&nbsp;pluginPackageName,&nbsp;int&nbsp;version)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(pluginPackageName&nbsp;==&nbsp;null&nbsp;||&nbsp;mContext&nbsp;==&nbsp;null)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;&nbsp;&nbsp;String&nbsp;originFilePath&nbsp;=&nbsp;PluginDirHelper.getSourceFile(pluginPackageName,&nbsp;version);&nbsp;&nbsp;&nbsp;&nbsp;String&nbsp;tempFilePath&nbsp;=&nbsp;PluginDirHelper.getTempSourceFile(pluginPackageName,&nbsp;version);&nbsp;&nbsp;&nbsp;&nbsp;safeCopyFile(originFilePath,&nbsp;tempFilePath);&nbsp;&nbsp;&nbsp;&nbsp;String&nbsp;loadingPackageName&nbsp;=&nbsp;mContext.getPackageName();&nbsp;&nbsp;&nbsp;&nbsp;String&nbsp;loaderIsa&nbsp;=&nbsp;getCurrentInstructionSet();&nbsp;&nbsp;&nbsp;&nbsp;notifyDexLoad(loadingPackageName,&nbsp;tempFilePath,&nbsp;loaderIsa);}&nbsp;/**&nbsp;*&nbsp;Register&nbsp;DexModule(dex&nbsp;path)&nbsp;To&nbsp;PMS&nbsp;By&nbsp;Binder&nbsp;*/private&nbsp;void&nbsp;notifyDexLoad(String&nbsp;loadingPackageName,&nbsp;String&nbsp;dexPath,&nbsp;String&nbsp;loaderIsa)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(Build.VERSION.SDK_INT&nbsp;>=&nbsp;Build.VERSION_CODES.R)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//deal&nbsp;android&nbsp;11\12&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;realNotifyDexLoadForR(loadingPackageName,&nbsp;dexPath,&nbsp;loaderIsa);&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;else&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//deal&nbsp;android&nbsp;10&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;realNotifyDexLoad(loadingPackageName,&nbsp;dexPath,&nbsp;loaderIsa);&nbsp;&nbsp;&nbsp;&nbsp;}}&nbsp;/**&nbsp;*&nbsp;Register&nbsp;DexModule(dex&nbsp;path)&nbsp;To&nbsp;PMS&nbsp;By&nbsp;Binder&nbsp;for&nbsp;R+&nbsp;*/private&nbsp;void&nbsp;realNotifyDexLoadForR(String&nbsp;loadingPackageName,&nbsp;String&nbsp;dexPath,&nbsp;String&nbsp;loaderIsa)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(mPmObj&nbsp;==&nbsp;null&nbsp;||&nbsp;loadingPackageName&nbsp;==&nbsp;null&nbsp;||&nbsp;dexPath&nbsp;==&nbsp;null&nbsp;||&nbsp;loaderIsa&nbsp;==&nbsp;null)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;&nbsp;&nbsp;Map<String,&nbsp;String>&nbsp;maps&nbsp;=&nbsp;Collections.singletonMap(dexPath,&nbsp;"PCL[]");&nbsp;&nbsp;&nbsp;&nbsp;safeInvokeMethod(mPmObj,&nbsp;"notifyDexLoad",&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;new&nbsp;Object[]{loadingPackageName,&nbsp;maps,&nbsp;loaderIsa},&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;new&nbsp;Class[]{String.class,&nbsp;Map.class,&nbsp;String.class});}&nbsp;/**&nbsp;*&nbsp;Register&nbsp;DexModule(dex&nbsp;path)&nbsp;To&nbsp;PMS&nbsp;By&nbsp;Binder&nbsp;for&nbsp;Q&nbsp;*/private&nbsp;void&nbsp;realNotifyDexLoad(String&nbsp;loadingPackageName,&nbsp;String&nbsp;dexPath,&nbsp;String&nbsp;loaderIsa)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(mPmObj&nbsp;==&nbsp;null&nbsp;||&nbsp;loadingPackageName&nbsp;==&nbsp;null&nbsp;||&nbsp;dexPath&nbsp;==&nbsp;null&nbsp;||&nbsp;loaderIsa&nbsp;==&nbsp;null)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;&nbsp;&nbsp;List<String>&nbsp;classLoadersNames&nbsp;=&nbsp;Collections.singletonList("dalvik.system.DexClassLoader");&nbsp;&nbsp;&nbsp;&nbsp;List<String>&nbsp;classPaths&nbsp;=&nbsp;Collections.singletonList(dexPath);&nbsp;&nbsp;&nbsp;&nbsp;safeInvokeMethod(mPmObj,&nbsp;"notifyDexLoad",&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;new&nbsp;Object[]{loadingPackageName,&nbsp;classLoadersNames,&nbsp;classPaths,&nbsp;loaderIsa},&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;new&nbsp;Class[]{String.class,&nbsp;List.class,&nbsp;List.class,&nbsp;String.class});}&nbsp;/**&nbsp;*&nbsp;UnRegister&nbsp;DexModule(dex&nbsp;path)&nbsp;To&nbsp;PMS&nbsp;*/private&nbsp;void&nbsp;unregisterDexModule(String&nbsp;pluginPackageName,&nbsp;int&nbsp;version)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(pluginPackageName&nbsp;==&nbsp;null&nbsp;||&nbsp;mContext&nbsp;==&nbsp;null)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;&nbsp;&nbsp;String&nbsp;originDir&nbsp;=&nbsp;PluginDirHelper.getSourceDir(pluginPackageName,&nbsp;version);&nbsp;&nbsp;&nbsp;&nbsp;String&nbsp;tempDir&nbsp;=&nbsp;PluginDirHelper.getTempSourceDir(pluginPackageName,&nbsp;version);&nbsp;&nbsp;&nbsp;&nbsp;safeCopyDir(tempDir,&nbsp;originDir);&nbsp;&nbsp;&nbsp;&nbsp;String&nbsp;tempFilePath&nbsp;=&nbsp;PluginDirHelper.getTempSourceFile(pluginPackageName,&nbsp;version);&nbsp;&nbsp;&nbsp;&nbsp;safeDelFile(tempFilePath);&nbsp;&nbsp;&nbsp;&nbsp;reconcileSecondaryDexFiles();}&nbsp;/**&nbsp;*&nbsp;Real&nbsp;UnRegister&nbsp;DexModule(dex&nbsp;path)&nbsp;To&nbsp;PMS&nbsp;(By&nbsp;Binder)&nbsp;*/private&nbsp;void&nbsp;reconcileSecondaryDexFiles()&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;execCmd(buildReconcileSecondaryDexFilesArgs(),&nbsp;null);}&nbsp;/**&nbsp;*&nbsp;Process&nbsp;CMD&nbsp;(By&nbsp;Binder)(Have&nbsp;system&nbsp;permissions)&nbsp;*/private&nbsp;void&nbsp;execCmd(String[]&nbsp;args,&nbsp;Callback&nbsp;callback)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;Parcel&nbsp;data&nbsp;=&nbsp;Parcel.obtain();&nbsp;&nbsp;&nbsp;&nbsp;Parcel&nbsp;reply&nbsp;=&nbsp;Parcel.obtain();&nbsp;&nbsp;&nbsp;&nbsp;data.writeFileDescriptor(FileDescriptor.in);&nbsp;&nbsp;&nbsp;&nbsp;data.writeFileDescriptor(FileDescriptor.out);&nbsp;&nbsp;&nbsp;&nbsp;data.writeFileDescriptor(FileDescriptor.err);&nbsp;&nbsp;&nbsp;&nbsp;data.writeStringArray(args);&nbsp;&nbsp;&nbsp;&nbsp;data.writeStrongBinder(null);&nbsp;&nbsp;&nbsp;&nbsp;ResultReceiver&nbsp;resultReceiver&nbsp;=&nbsp;new&nbsp;ResultReceiverCallbackWrapper(callback);&nbsp;&nbsp;&nbsp;&nbsp;resultReceiver.writeToParcel(data,&nbsp;0);&nbsp;&nbsp;&nbsp;&nbsp;try&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;mPmBinder.transact(SHELL_COMMAND_TRANSACTION,&nbsp;data,&nbsp;reply,&nbsp;0);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;reply.readException();&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;catch&nbsp;(Throwable&nbsp;e)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//Report&nbsp;info&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;finally&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;data.recycle();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;reply.recycle();&nbsp;&nbsp;&nbsp;&nbsp;}}&nbsp;/**&nbsp;*&nbsp;Build&nbsp;dexOpt&nbsp;args&nbsp;*&nbsp;*&nbsp;&nbsp;@param&nbsp;compileFilter&nbsp;compile&nbsp;filter&nbsp;*&nbsp;&nbsp;@return&nbsp;cmd&nbsp;args&nbsp;*/private&nbsp;String[]&nbsp;buildDexOptArgs(String&nbsp;compileFilter)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;buildArgs("compile",&nbsp;"-m",&nbsp;compileFilter,&nbsp;"-f",&nbsp;"--secondary-dex",&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;mContext&nbsp;==&nbsp;null&nbsp;?&nbsp;""&nbsp;:&nbsp;mContext.getPackageName());}&nbsp;/**&nbsp;*&nbsp;Build&nbsp;ReconcileSecondaryDexFiles&nbsp;Args&nbsp;*&nbsp;*&nbsp;&nbsp;@return&nbsp;cmd&nbsp;args&nbsp;*/private&nbsp;String[]&nbsp;buildReconcileSecondaryDexFilesArgs()&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;buildArgs("reconcile-secondary-dex-files",&nbsp;mContext&nbsp;==&nbsp;null&nbsp;?&nbsp;""&nbsp;:&nbsp;mContext.getPackageName());}&nbsp;/**&nbsp;*&nbsp;Get&nbsp;the&nbsp;InstructionSet&nbsp;through&nbsp;reflection&nbsp;*/private&nbsp;String&nbsp;getCurrentInstructionSet()&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;String&nbsp;currentInstructionSet;&nbsp;&nbsp;&nbsp;&nbsp;try&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Class&nbsp;vmRuntimeClazz&nbsp;=&nbsp;Class.forName("dalvik.system.VMRuntime");&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;currentInstructionSet&nbsp;=&nbsp;(String)&nbsp;MethodUtils.invokeStaticMethod(vmRuntimeClazz,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"getCurrentInstructionSet");&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;catch&nbsp;(Throwable&nbsp;e)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;currentInstructionSet&nbsp;=&nbsp;"arm64";&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;currentInstructionSet;}

验证

下面是针对本方案兼容性验证的结果:

目标版本

系统版本

手机品牌

Register Dex Module

Dex Opt

UnRegister Dex Module

手机型号

Target29

Android 10

Vivo

- Yes

- Yes

- Yes

Vivo IQOO

Target29

Android 10

Oppo

- Yes

- Yes

- Yes

Oppo R15

Target29

Android 10

MI

- Yes

- Yes

- Yes

MI 8

Target29

Android 10

华为

- Yes

- Yes

- Yes

华为 nova 7

Target29

Android 11

Vivo

- Yes

- Yes

- Yes

Vivo V20

Target29

Android 11

Oppo

- Yes

- Yes

- Yes

Oppo PDPM00(Oppo Android 11 对Rom进行了修改,目前暂不支持)

Target29

Android 11

MI

- Yes

- Yes

- Yes

MI M2011K2C

Target29

Android 11

华为

- Yes

- Yes

- Yes

无此机器

Target29

Android 12

Piexl

- Yes

- Yes

- Yes

本地真机

Target30

Android 10

Vivo

- Yes

- Yes

- Yes

Vivo S1

Target30

Android 10

Oppo

- Yes

- Yes

- Yes

Oppo Find X

Target30

Android 10

MI

- Yes

- Yes

- Yes

MI 8

Target30

Android 10

华为

- Yes

- Yes

- Yes

华为 P20

Target30

Android 11

Vivo

- Yes

- Yes

- Yes

Vivo V2046A

Target30

Android 11

Oppo

- Yes

- Yes

- Yes

Oppo PDPM00(Oppo Android 11 对Rom进行了修改,目前暂不支持)

Target30

Android 11

MI

- Yes

- Yes

- Yes

MI M2011K2C

Target30

Android 11

华为

- Yes

- Yes

- Yes

无此机器

Target30

Android 12

Piexl

- Yes

- Yes

- Yes

本地真机

目前来看,对于手机品牌来说,该方案均可以兼容,仅Oppo且Android 11的机器上,由于对Rom进行了修改限制,导致此款机器不兼容。

兼容效果还算良好。

下面针对高中低端的机器上,验证下优化前后Dex加载速度的差异:

机器性能

机器型号

包大小

优化前平均耗时

优化后平均耗时

减少耗时占总耗时百分比

低端机

Piexl 2

1.9m

269.5ms

12ms

95.5%

中端机

Vivo S1

1.9m

159ms

8.8ms

94%

高端机

MI 8

1.9m

48.3ms

6.5ms

86%

对于Dex加载耗时的统计,是采用统计首次new ClassLoaderDex加载的耗时。

Dex加载耗时同包大小属于正相关,包越大,加载耗时越多;同机器性能属于负相关,机器性能越好,加载耗时越少。

通过上述数据可以看出,优化前后耗时差距还是非常明显的,机器性能越差优化越明显。

Dex加载速度优化明显。

下面针对高中低端的机器上,验证下优化前后场景运行速度的差异:

机器性能

机器型号

优化前平均耗时

优化后平均耗时

减少耗时占总耗时百分比

低端机

Piexl 2

45ms

36ms

20%

中端机

Vivo S1

36.75ms

31.23ms

13.6%

高端机

MI 8

13ms

11.5ms

11.5%

对于场景运行耗时的统计,是采用对场景启动前后打点,然后计算时间差。

由于非全量编译对运行速度影响较小,上述数据为未优化同全量编译优化的对比数据。

场景耗时场景复杂度属于正相关,场景复杂度越高,场景耗时越多;同机器性能属于负相关,机器性能越好,场景耗时越少。

通过上述数据可以看出,优化后对运行速度还是有质的提升的,且会随场景复杂度的提升,带来更大的提升。

总结

最终,通过假借系统之手来触发dex2oat的方式,绕过targetSdkVersion>=29Android10+上的限制,效果较为明显,dex加载速度提升80%以上,场景运行速度提升11%以上。

关于字节终端技术团队

字节跳动终端技术团队(Client Infrastructure)是大前端基础技术的全球化研发团队(分别在北京、上海、杭州、深圳、广州、新加坡和美国山景城设有研发团队),负责整个字节跳动的大前端基础设施建设,提升公司全产品线的性能、稳定性和工程效率;支持的产品包括但不限于抖音、今日头条、西瓜视频、飞书、懂车帝等,在移动端、Web、Desktop等各终端都有深入研究。

就是现在!客户端/前端/服务端/端智能算法/测试开发 面向全球范围招聘!一起来用技术改变世界,感兴趣请联系chenxuwei.cxw@bytedance.com,邮件主题简历-姓名-求职意向-期望城市-电话。

火山引擎应用开发套件 MARS 是字节跳动终端技术团队过去九年在抖音、今日头条、西瓜视频、飞书、懂车帝等 App 的研发实践成果,面向移动研发、前端开发、QA、 运维、产品经理、项目经理以及运营角色,提供一站式整体研发解决方案,助力企业研发模式升级,降低企业研发综合成本。

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章