猪小花1号

个人签名

282篇博客

当我们在谈论multidex65535时,我们在谈论什么

猪小花1号2018-08-31 13:22

作者:郑文


首先我们并不在讨论车牌号..但在2016年依然讨论multidex是一个图样图森破的问题,本文尽量避免谈论重复的技术点,只探讨一下multidex提供给我们的技术启示。

原理

multidex技术原理可以分成两个部分:

  • 在app启动时,通过Multidex.install api,扩展ClassLoader的dexElements数组来存储所有dex,这个流程会根据android sdk版本的不同做不同的处理,整个流程完全通过反射完成。

  • 编译过程中的分包机制,将app中的class以某种方式将class分布在多个dex中

Multidex.install

从multidex的原理,很容易想起目前比较流行的一个代码热修复方案策略。安卓App热补丁动态修复技术介绍 ,qzone的技术思路在社区诞生了很多技术框架,其中知名度最高的就是Nuwa。但客观的说,nuwa这个项目是完全达不到产品release的要求。在调研完所有同技术路线开源框架后,我们决定造个能应用在线上产品的轮子。

我们的解决方案

  • 使用Transform API进行bytecode的植入,支持高版本gradle
  • 编译流程的优化,减少了人工干预的过程
  • 支持补丁包的签名验证,避免被恶意hook
  • 兼容网易安全部门的加壳方案
  • 支持ART模式:在art模式下,应用补丁包可能导致地址错乱,解决方案是将直接引用修改class的相关类都打包进行补丁包,这是目前其他开源方案没有做的

MultiDex存在的问题

MultiDex机制的出现本身是为了避免出现app 65535问题的出现,但随着业务逻辑的增长,以及不合理的模块划分,导致main dex的方法数也超出了65535,这就导致了main dex capacity exceeded异常。

同时,Multidex的接入额外还会对app的启动性能造成影响。Multidex在install时需要加载dex,首次启动时还需要做odex的转换,而这些都是在ui主线程中完成。 根据 Carlos Sessa的测试,启用multidex后,4.4或以下的设备,app的启动时间平均会增加15%的时间,更严重的情况,甚至在启动时候会出现了黑屏。

目前部分app采取的策略是,放弃掉Multidex的,而转为插件化的架构。通过将非核心模块的lazy load,来达到启动速度的优化,但我们需要明确的是,并不是所有app都似乎插件化架构,为了实现启动加速或热更新将本耦合的业务逻辑硬生生拆解才是本末倒置。

解决方案

Multidex异步化

在Android的性能优化中,最常见的思路就是异步化,减少UI线程的工作。在应用的交互层面上,app启动时,几乎所有app都会有一个SplashActivity。在界面上展示欢迎页,在后台进行初始化的业务逻辑。这就给我们一个启发,我们可以将系统的初始化逻辑,延迟到我们的业务初始化时间点上。

更加具体的方式是,我们可以将Multidex.install异步化,保证主线程的正常进行,待加载完成后通知SplashActivity跳转到真正的业务主界面。

在MultiDex加载的异步化之后,我们可以进行第二步:main dex大小的精简。

Multidex分包原理介绍

Multidex会在入口Application的attachBaseContext,加载second dex,因此multidex分包的基本原则是:保证app启动需要的class放置在main dex上。在gradle 1.5以上,multidex通过CreateManifestKeepList和MutidexTransform完成,分包过程可以分为三步:

  • 生成manifest_keep.txt

CreateManifestKeepList会解析出AndroidManifest.xml中所有的组件类:包括Activity、Service、Receiver以及ContentProvider,这些类将会和Application入口类一起放在build/intermediates/multi-dex/{flavor}/{buildType}/manifest_keep.txt中

  • 生成maindexlist.txt文件

MutidexTransform会查找manifest_keep.txt中所有类的直接引用类,具体的方式是遍历类的所有字段类以及方法,查看方法的参数和返回值的类型,将其放保存在maindexlist.txt

  • 生成main dex

将maindexlist.txt文件包含的所有class编译进main dex

从上面的分析中,我们可以确定的是,MultiDex的分包机制并不严密:

  • MultiDex将AndroidManifest.xml中的所有组件都包含在了manifest_keep.txt。但app在首次启动时,并不需要加载所有的组件,而只是需要入口的activity,供其他app访问的service、contentprovider以及注册获取系统通知的receiver

  • MultidexTransform只查找manifest.keep的直接引用类,间接引用类并没有出现在maindex中,特殊情况下,会出现NoClassDefFoundError的异常,这时候开发者需要自行将需要的class添加到maindexlist.txt

基于这两个缺陷,我们决定对MultiDex的分包流程进行优化:

  • 首先通过替换掉manifest_keep.txt,去除不相关的组件,只保证入口需要的相关class

  • 在gradle中添加extenstion扩展块,通过简单的属性设置即可在main dex中添加被multidex遗漏的class,而不需要手工编写task

额外需要提的一个细节是,dx工具的minimal-main-dex参数

这个参数可以保证只有被–main-dex-list文件中指定的类被打包在第一个dex,其余的都在第二个dex文件中,但在gradle1.5之后,这个参数被默认关闭的,因此我们在gradle插件中通过挂载javaagent的方式对dx过程进行了hook,保证我们的main dex精简化效果。

网易云大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者郑文授权发布。