热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

android压缩大小打包_支付宝App构建优化:Android包大小极致压缩

点击上方“刘望舒”,选择“星标”多点在看,就是真爱作者:琉克|来源:公众号mPaaS1前言本章节我们将围绕《支付宝App构建优化解析》另启

点击上方“刘望舒”,选择“星标”

 多点在看,就是真爱

作者: 琉克 |  来源:公众号 mPaaS

1

前言

本章节我们将围绕《支付宝 App 构建优化解析》另启新系列,细分拆解客户端在“代码管理”、“证书管理”、“版本管理”、“构建打包”等维度的具体实现方案展开讨论,带领大家进一步了解支付宝在 App 构建模块下的持续优化。

本节将主要记录通过对支付宝 Android 包大小进行压缩,来改善运行效率和质量。

2

背景

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

3

方案介绍

支付宝关于包大小优化,支付宝一直在这个方向上努力,因此我们引入了很多方案,比如 “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 的结构可以用下面这张图表示:

2965cf19d5fe7cf3d5703e0e7c984bb2.png

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

  • debugItem 干吗用?

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

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

  2. 所有的指令集行号和源文件行号的对应关系有什么用呢?

    第一点很明显,既然叫 debugItem,那么肯定就是 debug 的时候用。我们平时在用 IDE 进行断点和单步调试的时候都会用到这个区域。

    第二点作用那就是上报 crash 或者主动获取调用堆栈的时候用。因为虚拟机真正执行的时候是执行的指令集,上报堆栈会上报 crash 的对应源文件行号,此时正是通过这个 debugItem 来获取对应的行号,可以用下面的截图比较直观的了解:

9b8836cd675a0979bc2cd1c205f42ed0.png

上图是一个比较常见的 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 包不处理。

4

方案一

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

d1636ef96bc07507ff01fc3e6eead275.png

如上图,核心点有四个:

  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 对象,里面保存了整个堆栈信息,经过反复的阅读源码和尝试,发现我要的指令集行号其实也在这个对象里面。可以用下面一幅简单的图示意:

86100a2ec389ce6b1b235ca08629b58e.png

在打印 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 虚拟机之后的就统一了。

5

方案二

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

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

  • native crash 时的 Java 调用栈。

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

5ffba81c4c2f98f4c413b84c8c123e45.png

如上图:本来每一个方法都会有一个 debugInfoItem,每一个 debuginfoItem 里面都有一个指令集行号和源文件行号的映射关系,我做的修改其实非常简单,就是把多余的 debugInfoItem 全部删掉了,只留了一个 debugInfoItem,所有的方法都指向同一个 debugInfoItem,并且这个 debugInfoItem 中的指令集行号和源文件行号保持一致,这样不管用什么方式来查行号,拿到的都是指令集行号。其中也踩过很多坑,其实光留一个 debugInfoItem 是不够的,要兼容所有虚拟机的查找方式,需要对 debugInfoItem 进行分区,并且 debugInfoItem 表不能太大,遇到过一个坑就是 androidO 上进行 dex2oat 优化的时候,会频繁的遍历这个 debugInfoItem,导致 AOT 编译比较慢,最后都通过 debugInfoItem 分区解决了。这个方案比较彻底,不用改 proguard,也不用 hook native。不过如果只需要处理 crash 的行号问题,那还是首推方案一,这个方案改动有点大,前期也是每天研究 dex 的文件结构,抠每一个细节,有比较大的把握时才敢改。

6

小结

目前该方案已经在支付宝正式上线,前面经过好几轮的外灰验证,还是比较稳定的。支付宝整体包大小减少了 2.1M 左右,真实的 dex 大小减少 3.5M 左右。

通过本节内容,我们初步了解了支付宝在 Android 客户端持续压缩包大小所积累的思考和实践。由于篇幅限制,很多技术要点我们无法一一展开。而相应的技术内核,我们同样应用在了 mPaaS 并对外输出,欢迎大家上手体验:

https://tech.antfin.com/docs/2/49549

关于 Android 端包大小压缩的设计思路和具体实践,同样期待你们的反馈,欢迎一起探讨交流。

----------  END  ----------

推荐文章

不会查看系统源码,还搞什么Android?

Flutter 美团的实践及原理

为什么阿里禁止在 foreach 循环里进行元素的 remove/add 操作

分享大前端、Java、Android等技术,

关注职业发展和行业动态。

ce7e3c18224defa0fa4cafe826539ba3.png




推荐阅读
  • 本文介绍了响应式页面的概念和实现方式,包括针对不同终端制作特定页面和制作一个页面适应不同终端的显示。分析了两种实现方式的优缺点,提出了选择方案的建议。同时,对于响应式页面的需求和背景进行了讨论,解释了为什么需要响应式页面。 ... [详细]
  • Nginx使用AWStats日志分析的步骤及注意事项
    本文介绍了在Centos7操作系统上使用Nginx和AWStats进行日志分析的步骤和注意事项。通过AWStats可以统计网站的访问量、IP地址、操作系统、浏览器等信息,并提供精确到每月、每日、每小时的数据。在部署AWStats之前需要确认服务器上已经安装了Perl环境,并进行DNS解析。 ... [详细]
  • 本文介绍了Python高级网络编程及TCP/IP协议簇的OSI七层模型。首先简单介绍了七层模型的各层及其封装解封装过程。然后讨论了程序开发中涉及到的网络通信内容,主要包括TCP协议、UDP协议和IPV4协议。最后还介绍了socket编程、聊天socket实现、远程执行命令、上传文件、socketserver及其源码分析等相关内容。 ... [详细]
  • 本文介绍了如何使用php限制数据库插入的条数并显示每次插入数据库之间的数据数目,以及避免重复提交的方法。同时还介绍了如何限制某一个数据库用户的并发连接数,以及设置数据库的连接数和连接超时时间的方法。最后提供了一些关于浏览器在线用户数和数据库连接数量比例的参考值。 ... [详细]
  • 本文详细介绍了MysqlDump和mysqldump进行全库备份的相关知识,包括备份命令的使用方法、my.cnf配置文件的设置、binlog日志的位置指定、增量恢复的方式以及适用于innodb引擎和myisam引擎的备份方法。对于需要进行数据库备份的用户来说,本文提供了一些有价值的参考内容。 ... [详细]
  • ZSI.generate.Wsdl2PythonError: unsupported local simpleType restriction ... [详细]
  • 图解redis的持久化存储机制RDB和AOF的原理和优缺点
    本文通过图解的方式介绍了redis的持久化存储机制RDB和AOF的原理和优缺点。RDB是将redis内存中的数据保存为快照文件,恢复速度较快但不支持拉链式快照。AOF是将操作日志保存到磁盘,实时存储数据但恢复速度较慢。文章详细分析了两种机制的优缺点,帮助读者更好地理解redis的持久化存储策略。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • Tomcat/Jetty为何选择扩展线程池而不是使用JDK原生线程池?
    本文探讨了Tomcat和Jetty选择扩展线程池而不是使用JDK原生线程池的原因。通过比较IO密集型任务和CPU密集型任务的特点,解释了为何Tomcat和Jetty需要扩展线程池来提高并发度和任务处理速度。同时,介绍了JDK原生线程池的工作流程。 ... [详细]
  • HTML学习02 图像标签的使用和属性
    本文介绍了HTML中图像标签的使用和属性,包括定义图像、定义图像地图、使用源属性和替换文本属性。同时提供了相关实例和注意事项,帮助读者更好地理解和应用图像标签。 ... [详细]
  • C++字符字符串处理及字符集编码方案
    本文介绍了C++中字符字符串处理的问题,并详细解释了字符集编码方案,包括UNICODE、Windows apps采用的UTF-16编码、ASCII、SBCS和DBCS编码方案。同时说明了ANSI C标准和Windows中的字符/字符串数据类型实现。文章还提到了在编译时需要定义UNICODE宏以支持unicode编码,否则将使用windows code page编译。最后,给出了相关的头文件和数据类型定义。 ... [详细]
  • 本文讨论了在手机移动端如何使用HTML5和JavaScript实现视频上传并压缩视频质量,或者降低手机摄像头拍摄质量的问题。作者指出HTML5和JavaScript无法直接压缩视频,只能通过将视频传送到服务器端由后端进行压缩。对于控制相机拍摄质量,只有使用JAVA编写Android客户端才能实现压缩。此外,作者还解释了在交作业时使用zip格式压缩包导致CSS文件和图片音乐丢失的原因,并提供了解决方法。最后,作者还介绍了一个用于处理图片的类,可以实现图片剪裁处理和生成缩略图的功能。 ... [详细]
  • RouterOS 5.16软路由安装图解教程
    本文介绍了如何安装RouterOS 5.16软路由系统,包括系统要求、安装步骤和登录方式。同时提供了详细的图解教程,方便读者进行操作。 ... [详细]
  • 本文整理了Java面试中常见的问题及相关概念的解析,包括HashMap中为什么重写equals还要重写hashcode、map的分类和常见情况、final关键字的用法、Synchronized和lock的区别、volatile的介绍、Syncronized锁的作用、构造函数和构造函数重载的概念、方法覆盖和方法重载的区别、反射获取和设置对象私有字段的值的方法、通过反射创建对象的方式以及内部类的详解。 ... [详细]
  • 使用圣杯布局模式实现网站首页的内容布局
    本文介绍了使用圣杯布局模式实现网站首页的内容布局的方法,包括HTML部分代码和实例。同时还提供了公司新闻、最新产品、关于我们、联系我们等页面的布局示例。商品展示区包括了车里子和农家生态土鸡蛋等产品的价格信息。 ... [详细]
author-avatar
知足者常乐-----仙_230
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有