支付宝 App 构建优化解析:Android 包大小极致压缩

包大小的重要性已经不需要多说,包大小直接影响用户的下载,留存,还有部分厂商预装强制要求必须小于一定的值。但是随着业务的迭代开发,应用会越来越大,安装包会不停的膨胀,所以包大小缩减是一个长期的治理过程。

方案

支付宝也一直在优化包大小的方向上努力,我们引入了很多方案。 比如: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,有些叫 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 文件。可以用下面的示意图表示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如上图:本来每一个方法都会有一个 debugInfoItem,每一个 debuginfoItem 里面都有一个指令集行号和源文件行号的映射关系,我做的修改其实非常简单,就是把多余的 debugInfoItem 全部删掉了,只留了一个 debugInfoItem,所有的方法都指向同一个 debugInfoItem,并且这个 debugInfoItem 中的指令集行号和源文件行号保持一致,这样不管用什么方式来查行号,拿到的都是指令集行号。

其中也踩过很多坑,其实光留一个 debugInfoItem 是不够的,要兼容所有虚拟机的查找方式,需要对 debugInfoItem 进行分区,并且 debugInfoItem 表不能太大,遇到过一个坑就是 androidO 上进行 dex2oat 优化的时候,会频繁的遍历这个 debugInfoItem,导致 AOT 编译比较慢,最后都通过 debugInfoItem 分区解决了。

这个方案比较彻底,不用改 proguard,也不用 hook native。不过如果只需要处理 crash 的行号问题,那还是首推方案一,这个方案改动有点大,前期也是每天研究 dex 的文件结构,抠每一个细节,有比较大的把握时才敢改。

小结

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后我还整理了很多Android中高级的PDF技术文档。以及一些大厂面试真题解析文档。

image

Android高级架构师之路很漫长,一起共勉吧!

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!**

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值