写点什么

支付宝 App 构建优化解析:Android 包大小极致压缩 (1),阿里 P8 亲自讲解

用户头像
Android架构
关注
发布于: 19 分钟前

方案

支付宝也一直在优化包大小的方向上努力,我们引入了很多方案。 比如:proguard 代码混淆,图片从 png 到 tinypng 到 webp,引入 7zip 压缩方案等。 本方案是有别于上面这些常规的方案,是通过直接删 dex 中的无用信息,达到支付宝包大小瞬间减小 2.1M 的目的,并且不影响整个的运行逻辑和性能,甚至还能降低一点运行内存。

方案介绍

  • 引言



在讲详细方案前得稍微说说整个 Java 系的调试逻辑。 JVM 运行时加载的是 .class 文件,Android 为了使包大小更紧凑,并且运行更高效发明了 dalvik 和 art 虚拟机,两种虚拟机运行的都是 .dex 文件(当然 art 虚拟机还可以同时运行 oat 文件,不在本文章讨论范围)。 所以 dex 文件里面信息的内容和 class 文件包含的信息是完全一致的,不同的是 dex 文件对 class 中的信息做了去重,一个 dex 包含了很多的 class 文件,并且在结构上有比较大的差异,class 是流式的结构,dex 是分区结构,各个区块间通过 offset 索引。后面就只提 dex 的结构,不再提 class 的结构。dex 的结构可以用下面这张图表示:



dex 文件的结构其实非常清晰,分几个大块,header 区,索引区,data 区,map 区。本优化方案优化删除的就是 data 区中的 debugItems 区域。


  • debugItem 干吗用?



首先得知道 debugItem 里面存了什么? 里面主要包含两种信息:


  1. 函数的参数变量和所有的局部变量

  2. 所有的指令集行号和源文件行号的对应关系 有什么用呢: 第一点其实很明显,既然叫 debugItem,那么肯定就是 debug 的时候用的喽,我们平时在用 IDE 进行断点和单步调试的时候都会用到这个区域。 第二点作用那就是上报 crash 或者主动获取调用堆栈的时候用的,因为虚拟机真正执行的时候是执行的指令集,上报堆栈会上报 crash 的对应源文件行号,此时正是通过这个 debugItem 来获取对应的行号,可以用下面的截图比较直观的了解:



上图是一个比较常见的 crash 信息,红框中的行号便是通过查找这个 debugItem 来获取的。


  • debugItem 有多大?



在支付宝的场景下,debug 包有 4-5M,release 包有 3.5M 左右,占 dex 文件大小的比例在 5.5% 左右,和 google 官方的数据是一致的。如果能把这部分直接去掉,是不是很诱人!


  • debugItem 能直接去掉吗?



显然不能,如果去掉了,那所有上报的 crash 信息就会没有行号,所有的行号都会变成 -1,会被喷的找不到北。 其实在 proguard 的时候就是有配置可以去掉或保留这个行号信息,-keep SourceFile, LineNumberTable 就是这个作用,为了方便定位问题,基本所有的开发都保留了这个配置。 所以,方案的核心思路就是去掉 debugItem,同时又能让 crash 上报的时候能拿到正确的行号。至于 IDE 调试,这个比较好解决,我们只要处理 release 包就行了,debug 包不处理。

方案一

核心思路也比较简单,就是行号查找离线化,让本来存放在 App 中的行号对应关系提前抽离出来存放在服务端,crash 上报的时候通过提前抽离的行号表进行行号反解,解决 crash 信息上报无行号,无法定位的问题。 思路虽然简单,实现的时候还是有点复杂,推动上线也比较曲折,方案经过几次调整,大概的方案可以用下面一张图来抽象:



如上图,核心点有四个:


  1. 修改 proguard,利用 proguard 来删除 debugItem (去掉 -keep lineNumberTable),在删除行号表之前 dump 出一个临时的 dex。

  2. 修改 dexdump,把临时的 dex 中的行号表关系 dump 成一个 dexpcmapping 文件(指令集行号和源文件行号映射关系),并存至服务端。

  3. hook app runtime 的 crash handler,把 crash 时的指令集行号上报到反解平台。

  4. 反解平台通过上报指令集行号和提前准备好 dexpcmapping 文件反解出正确的行号。


上面这套方案大概花了两个多星期,撸出了整个 demo,其它几个改造点都不是很难,难点还是在指令集行号的上报。 我们知道所有的 crash 最终都是会有一个 throwable 对象,里面保存了整个堆栈信息,经过反复的阅读源码和尝试,发现我要的指令集行号其实也在这个对象里面。可以用下面一幅简单的图示意:



在打印 crash 堆栈信息前,每个 throwable 都会调用 art 虚拟机提供的一个 jni 方法,返回一个内部的对象叫 stackTrace 保存在 Throwable 对象中,这个 stackTrace 对象里面保存的便是整个方法的调用栈,当然也包括指令集行号,后续获取实际的堆栈信息时会再调用一个 art 的 jni 方法,把这个 stackTrace 方法丢过去,底层通过这个 stackTrace 对象中的指令集行号反解出正式的源文件行号。 好了,其实很简单,反射获取下这个 Throwable 中的 stackTrace 对象,拿到指令集行号,然后,上报。 这里要注意的一个点,比较恶心,每个虚拟机的实现都不一样,首先内部对象的名字,有些叫 stackTrace,有些


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


叫 backstrace,然后这个内部对象的类型也非常有,有些是 int 数组,有些是 long 数组,有些是对象数组,但是都会有这个指令集行号,需要针对不同的虚拟机版本使用不同的方法去解析这个对象,大概要兼容 4 种虚拟机,4.x, 5.x, 6.x, 7.x,7.x 虚拟机之后的就统一了。

方案二

上面这套方案其实挺完美的,没有什么兼容性问题,删除是直接利用 proguard,获取指令集行号直接在 java 层获取,不需要各种 hook,如果只需要处理 crash 的上报,方案一足够了,但是在支付宝有很多场景是远远不够的。 比如:


  • 性能,CPU,内存异常时调用栈。

  • native crash 时的 Java 调用栈。


上面这些 case 都会涉及到堆栈信息,方案一中通过反射调用 throwable 中的 stackTrace 内部对象根本搞不定,需要换种方法。 最开始的思路是尝试 hook art 虚拟机,每天翻源码,看看可以 hook 的点,最后还是放弃了,一个是担心兼容性问题,另一个是 hook 的点太多,比较慌。 最后换了一种思路,尝试直接修改 dex 文件,保留一小块 debugItem,让系统查找行号的时候指令集行号和源文件行号保持一致,这样就什么都不用做,任何监控上报的行号都直接变成了指令集行号,只需修改 dex 文件。可以用下面的示意图表示:



用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
支付宝 App 构建优化解析:Android 包大小极致压缩(1),阿里P8亲自讲解