最近看到一篇博客:Android性能优化之Android 10+ dex2oat实践,对这个优化很感兴趣,打算研究研究能否接入到项目中。不过该博客只讲述了思路,没有给完整源码。本项目参考该博客的思路,实现了该方案。

源码地址:https://github.com/carverZhong/DexOpt

一、dex2oat 详解

以下是官方对于dex2oat的解释:

ART 使用预先 (AOT) 编译,并且从 Android 7.0(代号 Nougat,简称 N)开始结合使用 AOT、即时 (JIT) 编译和配置文件引导型编译。所有这些编译模式的组合均可配置,我们将在本部分中对此进行介绍。例如,Pixel 设备配置了以下编译流程:

  1. 最初安装应用时不进行任何 AOT 编译。应用前几次运行时,系统会对其进行解译,并对经常执行的方法进行 JIT 编译。

  2. 当设备闲置和充电时,编译守护程序会运行,以便根据在应用前几次运行期间生成的配置文件对常用代码进行 AOT 编译。

  3. 下一次重新启动应用时将会使用配置文件引导型代码,并避免在运行时对已经过编译的方法进行 JIT 编译。在应用后续运行期间经过 JIT 编译的方法将会添加到配置文件中,然后编译守护程序将会对这些方法进行 AOT 编译。

ART 包括一个编译器(dex2oat 工具)和一个为启动 Zygote 而加载的运行时 (libart.so)。dex2oat 工具接受一个 APK 文件,并生成一个或多个编译工件文件,然后运行时将会加载这些文件。文件的个数、扩展名和名称因版本而异,但在 Android 8 版本中,将会生成以下文件:

.vdex:其中包含 APK 的未压缩 DEX 代码,以及一些旨在加快验证速度的元数据。

.odex:其中包含 APK 中已经过 AOT 编译的方法代码。

.art (optional):其中包含 APK 中列出的某些字符串和类的 ART 内部表示,用于加快应用启动速度。(配置 ART)

也就是说,dex2oat可以触发APK的AOT编译,并生成对应的产物,APP运行时会加载这些文件。执行过AOT编译的产物能加快启动速度、代码执行效率。

二、代码实现

具体原理还是参考博客:Android性能优化之Android 10+ dex2oat实践。这里说下实现上的细节。

博客的思路是通过一些手段触发系统来进行dex2oat。

1.整体思路

  1. PackageManagerShellCommand.runCompile方法可以触发Secondary Apk进行dex2oat,但是Secondary Apk需要先注册。

  2. 注册的逻辑在IPackageManagerImpl.registerDexModule,其中IPackageManagerImplPackageManagerService的内部类,并继承了IPackageManager.Stub

  3. 最后,再执行PackageManagerShellCommand.runreconcileSecondaryDexFiles反注册,就大功告成了。

所以整体分三步走:

  • 注册Secondary Apk

  • 执行dex2oat

  • 反注册Secondary Apk

2.注册Secondary Apk

IPackageManager是个AIDL接口,而应用中的ApplicationPackageManage刚好持有这个AIDL接口,因此可以通过其调用registerDexModule方法。

为此,可以通过反射调用registerDexModule方法。以下是核心实现:

  1. // 注册Secondary Apk
  2. private fun registerDexModule(apkFilePath: String): Boolean {
  3. try {
  4. val callbackClazz = ReflectUtil.findClass("android.content.pm.PackageManager\$DexModuleRegisterCallback")
  5. ReflectUtil.callMethod(
  6. getCustomPM(),
  7. "registerDexModule",
  8. arrayOf(apkFilePath, null),
  9. arrayOf(String::class.java, callbackClazz)
  10. )
  11. return true
  12. } catch (thr: Throwable) {
  13. Log.e(TAG, "registerDexModule: thr.", thr)
  14. }
  15. return false
  16. }
  17. /**
  18. * 创建一个自定义的 PackageManager,避免影响正常的 PackageManager
  19. */
  20. private fun getCustomPM(): PackageManager {
  21. val customPM = cacheCustomPM
  22. if (customPM != null && cachePMBinder?.isBinderAlive == true) {
  23. return customPM
  24. }
  25. val pmBinder = getPMBinder()
  26. val pmBinderDynamicProxy = Proxy.newProxyInstance(
  27. context.classLoader, ReflectUtil.getInterfaces(pmBinder::class.java)
  28. ) { _, method, args ->
  29. if ("transact" == method.name) {
  30. // FLAG_ONEWAY => NONE.
  31. args[3] = 0
  32. }
  33. method.invoke(pmBinder, *args)
  34. }
  35. val pmStubClass = ReflectUtil.findClass("android.content.pm.IPackageManager\$Stub")
  36. val pmStubProxy = ReflectUtil.callStaticMethod(pmStubClass,
  37. "asInterface",
  38. arrayOf(pmBinderDynamicProxy),
  39. arrayOf(IBinder::class.java))
  40. val contextImpl = if (context is ContextWrapper) context.baseContext else context
  41. val appPM = createAppPM(contextImpl, pmStubProxy!!)
  42. cacheCustomPM = appPM
  43. return appPM
  44. }

3.执行dex2oat

这里有个难点就是,如何才能调用到PackageManagerShellCommand.runCompile?看下调用逻辑:

  1. // 代码位于PackageManagerService.java。
  2. // IPackageManagerImpl是PackageManagerService的内部类。
  3. @Override
  4. public void onShellCommand(FileDescriptor in, FileDescriptor out,
  5. FileDescriptor err, String[] args, ShellCallback callback,
  6. ResultReceiver resultReceiver) {
  7. (new PackageManagerShellCommand(this, mContext, mDomainVerificationManager.getShell()))
  8. .exec(this, in, out, err, args, callback, resultReceiver);
  9. }

IPackageManager.Stub继承了Binder,而这个方法是Binder中的,调用逻辑如下:

  1. // Binder.java
  2. protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply,
  3. int flags) throws RemoteException {
  4. if (code == INTERFACE_TRANSACTION) {
  5. reply.writeString(getInterfaceDescriptor());
  6. return true;
  7. } else if (code == DUMP_TRANSACTION) {
  8. // 省略部分代码...
  9. return true;
  10. } else if (code == SHELL_COMMAND_TRANSACTION) {
  11. ParcelFileDescriptor in = data.readFileDescriptor();
  12. ParcelFileDescriptor out = data.readFileDescriptor();
  13. ParcelFileDescriptor err = data.readFileDescriptor();
  14. String[] args = data.readStringArray();
  15. ShellCallback shellCallback = ShellCallback.CREATOR.createFromParcel(data);
  16. ResultReceiver resultReceiver = ResultReceiver.CREATOR.createFromParcel(data);
  17. try {
  18. if (out != null) {
  19. // 重点!!!调用了 shellCommand 方法
  20. shellCommand(in != null ? in.getFileDescriptor() : null,
  21. out.getFileDescriptor(),
  22. err != null ? err.getFileDescriptor() : out.getFileDescriptor(),
  23. args, shellCallback, resultReceiver);
  24. }
  25. } finally {
  26. // 省略部分代码...
  27. }
  28. return true;
  29. }
  30. return false;
  31. }
  32. public void shellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out,
  33. @Nullable FileDescriptor err,
  34. @NonNull String[] args, @Nullable ShellCallback callback,
  35. @NonNull ResultReceiver resultReceiver) throws RemoteException {
  36. // 这里调用的!!!
  37. onShellCommand(in, out, err, args, callback, resultReceiver);
  38. }

所以这里逻辑清晰了,再次整理下逻辑:

  • Binder.onTransact收到 SHELL_COMMAND_TRANSACTION 命令会执行 shellCommand方法

  • shellCommand方法又调用了onShellCommand方法

  • IPackageManager.Stub继承了Binder

  • IPackageManagerImpl继承了IPackageManager.Stub并重写了onShellCommand方法

  • IPackageManagerImpl的onShellCommand执行了PackageManagerShellCommand相关逻辑

所以我们的核心是找到IPackageManager.aidl,并向其发送 SHELL_COMMAND_TRANSACTION 命令。得益于Android Binder机制,我们可以在应用进程拿到IPackageManger的Binder,并通过它来发送命令。

代码实现如下:

  1. // 执行dex2oat
  2. private fun performDexOpt() {
  3. val args = arrayOf(
  4. "compile", "-f", "--secondary-dex", "-m",
  5. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) "verify" else "speed-profile",
  6. context.packageName
  7. )
  8. executeShellCommand(args)
  9. }
  10. // IPackageManager.aidl 发送 SHELL_COMMAND_TRANSACTION 命令
  11. private fun executeShellCommand(args: Array<String>) {
  12. val lastIdentity = Binder.clearCallingIdentity()
  13. var data: Parcel? = null
  14. var reply: Parcel? = null
  15. try {
  16. data = Parcel.obtain()
  17. reply = Parcel.obtain()
  18. data.writeFileDescriptor(FileDescriptor.`in`)
  19. data.writeFileDescriptor(FileDescriptor.out)
  20. data.writeFileDescriptor(FileDescriptor.err)
  21. data.writeStringArray(args)
  22. data.writeStrongBinder(null)
  23. resultReceiver.writeToParcel(data, 0)
  24. getPMBinder().transact(SHELL_COMMAND_TRANSACTION, data, reply, 0)
  25. reply.readException()
  26. } catch (t: Throwable) {
  27. Log.e(TAG, "executeShellCommand error.", t)
  28. } finally {
  29. data?.recycle()
  30. reply?.recycle()
  31. }
  32. Binder.restoreCallingIdentity(lastIdentity)
  33. }

4.反注册Secondary Apk

反注册也是执行PackageManagerShellCommand相关方法,只不过给的参数不一样。所以大部分逻辑跟第三步是一样的。代码实现如下:

  1. private fun reconcileSecondaryDexFiles() {
  2. val args = arrayOf("reconcile-secondary-dex-files", context.packageName)
  3. executeShellCommand(args)
  4. }

最后,本项目的代码组织情况如下:

  • DexOpt:外部调用接口,执行DexOpt.dexOpt即可开启dex2oat。

  • ApkOptimizerN:负责Android7-Android9的dex2oat逻辑。

  • ApkOptimizerQ:负责Android10的dex2oat逻辑。也是本文的讲解重点。

三、优缺点

把这项技术应用到了一个插件化项目中,对插件APK进行dex2oat优化,总结下其优缺点。

1.优点

  • 插件的加载速度大大增加(实测可以达到90%以上),对插件化框架的冷启动有很大的意义。
  • 代码运行的速度有微小的提升。测试了跳转Activity、Service这些场景,能够提升20-80ms左右,跟机型有很大的关系。

2.缺点

  • dex2oat产物也会占用一定的存储空间。所以如果插件更新记得及时删除老的oat文件。
  • dex2oat 执行时间较长,首次还是建议直接加载插件,在后台执行dex2oat优化。
  • 部分手机执行后没有成功生成oat文件,还是存在机型兼容问题。

Android10 dex2oat实践的更多相关文章

  1. Android性能优化之Android 10+ dex2oat实践

    作者:字节跳动终端技术--郭海洋 背景 对于Android App的性能优化来说,方式方法以及工具都有很多,而dex2oat作为其中的一员,却可能不被大众所熟知.它是Android官方应用于运行时,针 ...

  2. 【腾讯Bugly干货分享】微信热补丁Tinker的实践演进之路

    本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57ad7a70eaed47bb2699e68e Dev Club 是一个交流移动 ...

  3. 【腾讯bugly干货分享】微信Android热补丁实践演进之路

    本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://bugly.qq.com/bbs/forum.php?mod=viewthread&tid=1264& ...

  4. 微信Android热补丁实践演进之路

    版权声明:本文由张绍文原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/81 来源:腾云阁 https://www.qclou ...

  5. ART模式下基于dex2oat脱壳的原理分析

    本文博客地址:http://blog.csdn.net/qq1084283172/article/details/78513483 一般情况下,Android Dex文件在加载到内存之前需要先对dex ...

  6. webp图片实践之路

    最近,我们在项目中实践了webp图片,并且抽离出了工具模块,整合到了项目的基础模板中.传闻IOS10也将要支持webp,那么使用webp带来的性能提升将更加明显.估计在不久的将来,webp会成为标配. ...

  7. Hangfire项目实践分享

    Hangfire项目实践分享 目录 Hangfire项目实践分享 目录 什么是Hangfire Hangfire基础 基于队列的任务处理(Fire-and-forget jobs) 延迟任务执行(De ...

  8. TDD在Unity3D游戏项目开发中的实践

    0x00 前言 关于TDD测试驱动开发的文章已经有很多了,但是在游戏开发尤其是使用Unity3D开发游戏时,却听不到特别多关于TDD的声音.那么本文就来简单聊一聊TDD如何在U3D项目中使用以及如何使 ...

  9. Logstash实践: 分布式系统的日志监控

    文/赵杰 2015.11.04 1. 前言 服务端日志你有多重视? 我们没有日志 有日志,但基本不去控制需要输出的内容 经常微调日志,只输出我们想看和有用的 经常监控日志,一方面帮助日志微调,一方面及 ...

随机推荐

  1. CEOI 2019 Day2 T2 魔法树 Magic Tree (LOJ#3166、CF1993B、and JOI2021 3.20 T3) (启发式合并平衡树,线段树合并)

    前言 已经是第三次遇到原题. 第一次是在 J O I 2021 S p r i n g C a m p \rm JOI2021~Spring~Camp JOI2021 Spring Camp 里遇到的 ...

  2. [2021.4.9多校省选模拟35]隐形斗篷 (prufer序列,背包DP)

    题面 我编不下去了! 给出 n n n 个点,第 i i i 个点的度数限制为 a i a_i ai​,现在需要选出 x x x 个点构成一颗树,要求这 x x x 个点中每个点的度数不超过这个点的 ...

  3. 【java】学习路径20-Date、Calender日期与时间

    简单的说,Date和Calender基本上是差不多的. 在最开始的时候只有Date,没有Calender. 在jdk不断更新的时候,发现了Date有一点缺陷,于是推出了Calender. // Dat ...

  4. python进阶__用socket封装TCP

    想要理解socket协议,点击链接,出门左转 一.TCP 通信的服务器端编程的基本步骤: 服务器端先创建一个 socket 对象. 服务器端 socket 将自己绑定到指定 IP 地址和端口. 服务器 ...

  5. 抛砖系列之git仓库拆分工具git-filter-repo

    最近负责把团队内的git仓库做了一次分拆,解锁一个好用的工具git-filter-repo,给大伙抛砖一波,希望以后遇到类似场景时可以信手拈来. 背景 笔者团队目前是把业务相关的java项目都放到了一 ...

  6. 深度剖析Istio共享代理新模式Ambient Mesh

    摘要:今年9月份,Istio社区宣布Ambient Mesh开源,由此引发国内外众多开发者的热烈讨论. 本文分享自华为云社区<深度剖析!Istio共享代理新模式Ambient Mesh>, ...

  7. Centos7.6内核升级

    文章转载自:https://mp.weixin.qq.com/s?__biz=MzI1MDgwNzQ1MQ==&mid=2247483766&idx=1&sn=4750fd4e ...

  8. Elasticsearch:Dynamic mapping

    Elasticsearch最重要的功能之一是它试图摆脱你的方式,让你尽快开始探索你的数据. 要索引文档,您不必首先创建索引,定义映射类型和定义字段 - 您只需索引文档,那么index,type和fie ...

  9. FastDFS与nginx配置使用的配置信息

    # 获取图片 location /group[1-9]/M0[0-9] { root /home/vdc1/fastdfs_storage/data; ngx_fastdfs_module; } # ...

  10. Mapping

    dynamic针对的是新增的字段,不是对mapping中已有的字段 (原有mapping中的字段不受影响,只影响新增的字段) 当dynamic被设置成false的时候,存在新增字段可以被写入到索引文件 ...