查看原文
其他

百度APP Android包体积优化实践(一)总览

百度Geek说 2022-09-06

The following article is from 百度App技术 Author 正又


GEEK TALK

01

前言

此前百度APP已经具备基本的包体积优化机制、约束和意识,但作为巨舰型APP,业务的高速迭代仍然不可避免地造成了包体积爆炸式增长。包体积或直接或间接地影响着下载转化率、安装时间、运行内存、磁盘空间等重要指标,所以投入精力扫除积弊、发掘更深层次的体积优化项是十分必要的。

根据谷歌商店的内部数据,APK体积每减少10M,平均可增加~1.5%的下载转化率,如下图所示:


图1 谷歌商店应用转化率增加幅度 / 10M [1]

Android包体积优化手段有很多,比如业务裁剪、插件化、混合开发、资源后下发等。本系列文章主要针对的是业务无关、集成在APK中的内容的体积优化,如Dex优化、资源优化、so优化等,我们称之为基础机制优化。

包体积基础机制优化实践将会以系列文章的方式呈现,主要包括以下部分:心路历程、Dex行号优化完整方案、资源优化实践与探索、Dex优化实践与探索、so优化探索、其他优化经验与总结。

本文讲述的是百度APP包体积基础机制优化心路历程,包括起持续指导作用的基本思想、优化对象分析、对现有优化工具的学习、以及最终产出的体积优化项。


GEEK TALK

02

基本思想

2.1 分而治之

们的优化对象不只是APK这个最终产物,也包括APK中的内容,这些内容的体积优化思路与手段不尽相同。

2.2 可持续优化

好的优化机制不止生效于当下,也生效于未来。举例来说,从源码仓库删除当前的Dead Code属于一次性存量优化操作,而编译器的DCE机制(Dead Code Elimination)可持续生效于未来产生的Dead Code。从长线考虑,我们应优先建立后者,然后倒推前者的执行。

2.3 站在前人的肩膀上

包体积优化并不是一个新鲜的话题,Android官方和开发者们都在持续致力于优化体积。重复造轮子是不被提倡的,但对于不同的应用场景,尤其是巨舰型APP,体积优化应该有定制化的方案。

2.4 明确代价,有所取舍

根据热力学第一定律,收益不会凭空产生,一定会伴随着代价,例如人力的投入、编译时间的增加、适配难度的增加等。明确代价后,我们才能决定某优化项是否要做、何时做、如何做。

2.5 约束与意识

除了自动化的优化机制,还需要配套有自动化的体积增长约束,同时从源头提升开发者的体积优化意识,多管齐下才能达到最优效果。

GEEK TALK

03

APK结构分析

接下来我们会简单分析下APK内各组成部分,以及APK作为ZIP,其标准结构是什么样的。

3.1 APK内容分析

图2 APK 结构
  • classes.dex
    APK 中可能包含一个或多个 classes.dex 文件,应用程序内的 Java/Kotlin 源码最终会以 dalvik 字节码的方式存在于 classes.dex 文件中。
  • resources.arsc
    该文件是包含配置信息的资源查询表,起着链接代码与资源的作用。Dex 文件中的 R.class 仅包含资源 id,AssetManager 会利用 id 到 arsc 表中查询与当前设备信息最匹配的资源文件路径(或资源内容)。
  • res/
    包含源码工程中 res 目录下除了 values 外的资源文件,这些文件路径同时会体现在 resources.arsc 中。
  • lib/
    native libraries。即源码工程 jni 目录下的 so 文件,二级目录必须为 NDK支持的 ABI。
  • assets/
    与 res/ 资源目录不同,assets/ 下的资源文件不会在 resources.arsc 中生成查询条目,且 assets/ 下的资源目录可完全自定义,业务代码获取 assets 资源和 res 资源的方式也完全不同。
  • META-INF/
    应用签名信息。该目录在应用签名后生成,包含以下三个文件:
    MANIFEST.MF:摘要文件,包含APK内所有文件的路径及其 SHA1/SHA256 值。
    CERT.SF:对摘要的签名文件,包含APK内所有文件的路径,及其在 MANIFEST.MF 中对应信息的 SHA1/SHA256 值。
    CERT.RSA:保存公钥、加密算法及其私钥加密后的内容。
  • AndroidManifest.xml
    应用清单文件,用于描述应用基本信息,主要包括应用包名、应用id、应用组件、所需权限、设备兼容性等。

3.2 ZIP结构分析

图3 ZIP 标准结构示意图
  • 压缩源文件信息
    Local file header:描述源文件信息。
    File data:源文件数据。
    Data descriptor:校验码及压缩前后大小。
  • 中心目录区
    Central directory
    记录 ZIP 目录结构。每一条 file header 对应一个源文件,描述文件相关信息。
  • 中心目录结束标识
    End of central directory record
    标识 ZIP 包结束,包含 ZIP 包及中心目录的简要信息。

GEEK TALK

04

现有优化工具介绍

对于发展初期的应用,体积优化的优先级较低,直接使用以下体积优化工具是性价比最高的选择。百度APP 同样对比借鉴了以下工具,从中衍生出了全新的、定制化的优化需求。

4.1 ProGuard

在 AGP3.3 之前,ProGuard 作为官方体积优化工具,负责在编译完成之后对class 文件进行缩减混淆等操作,其优化结果交给 Dx/D8 转化为 Dex 产物。

图4 Proguard 处理对象及作用示意图 [9]
ProGuard 的优化操作主要包括:
缩减:安全移除无用类、方法、字段和属性。
混淆:缩短类与成员的名称。
优化:指令级别的优化,合并重复指令、清理无用指令、提升指令执行效率。

4.2 R8

AGP 3.3之后官方开始推荐使用 R8,R8 与 ProGuard 不只是简单的替代关系,它还将脱糖、D8 整合到了一起,极大的提升了构建效率。
图5 R8 处理对象及作用示意图  [9]
R8 基本兼容此前的 ProGuard 规则,但仍存在些许差异(applymapping、行号处理、Kotlin元数据处理、无用判定等)。R8 不再高优考虑兼容性问题后,两者会派生出越来越多的不同点,建议定期关注,博采众长。
丨Jack & Jill
小插曲:官方在2015年推行过一段时间的Jack & Jill工具,它甚至把javac也囊括了进来,算是真正实现了端到端的编译。但Jack的性能与生态相比javac实在差距太大,官方出于成本考虑最后还是弃坑了。

4.3 AndResGuard

AndResGuard 是微信推出资源优化工具。它的基本思想类似于 ProGuard 中的混淆,体积优化是它的附加收益,同时还提供了压缩、加密等选项。

4.4 ByteX

ByteX 是字节开源的一套Java字节码插桩工具,目前主要包括优化与检查工作,其中一些子项最终会带来体积收益。包括R类内联、移除debug信息、access 方法内联等。

4.5 Booster

Booster 是滴滴开源的一套质量优化框架,其中包括体积优化专项,例如资源文件压缩、资源产物.ap_ 压缩、去冗余资源、R类内联、DataBinding BR内联等。

4.6 AGP

Android Gradle Plugin(AGP)包含了多个体积优化任务,提供了许多优化配置项,大部分任务已经作为APK打包的标配。

一般来讲,我们的优化任务会依赖于这些任务的执行。如果定制的优化无法兼容现存任务,则需要关闭或hook这些任务。接下来将按照编译顺序简单介绍几个优化任务与配置:
  • OptimizeResources
    AGP4.2+ 新增的资源优化任务,目前只实现了资源文件路径的缩短,默认开启,可通过 android.enableResourceOptimizations 关闭。
  • StripSymbols
    NDK 会利用 llvm-strip 移除掉 native libraries 中的unneeded symbols,这部分优化工作也可以放在so编译期间完成。
  • MinifyWithR8/ProGuard
    利用 R8 或 ProGuard 实现代码优化,此处就不再赘述了。
  • ShrinkResources
    由 ShrinkResources 开关控制,启用前提是必须开启 minifyEnable。其作用是将未被引用的资源文件替换为一个体积很小的格式文件(仍存在占位体积,同时保留了该资源条目,所以 resources.arsc 体积并不会减少),可通过 res/raw/keep.xml 文件配置 shrinkMode 和白名单。
  • PackageOptions
    打包时选项,包括过滤 exclude、相同文件仅打包 pickFirst、全部打包 merge、so优化豁免 doNotStrip。
  • Splits
    分包/过滤策略,配置项包括 ABI、资源配置(语言、分辨率等)。

GEEK TALK

05

百度APP优化项项概览

5.1 Dex优化

百度APP 实现 Dex 的体积优化项可以分为两类:源码编译期间的优化;APK 打包期间对 Dex 文件的优化。两者的区别主要是优化对象不同,所以基于不同的优化工具实现,前者基于Java字节码工具实现(如 ASM),后者基于 Dex 字节码工具实现(如 Titan-Dex [10])。
丨Titan-Dex
Titan-Dex 是百度开源的面向Android Dalvik(ART)字节码(bytecode)格式的操纵框架,可以在二进制格式下实现修改已有的类,或者动态生成新的类。百度Titan-hotfix工具即基于此框架实现。
  • R类优化
       工程组件越多,R类所占体积越大,未关闭资源依赖传递的情况下则更严重。我们在编译期将代码中调用 R.type.name 的地方全部替换成了对应的id常量,最终 R.class 会作为无用类被 R8/ProGuard 清理掉。
  • 行号优化
       Dex 中的 debug 区域占5~10%的大小,但其最大的作用是分析崩溃堆栈时定位。该区域可以通过去除 ProGuard 规则 -keepattributes SourceFile,LineNumberTable 完全移除。我们选择在指令级别完成 debug infos 的映射与复用,同时联动百度性能平台(目前仅供公司内部使用,功能可类比腾讯 bugly)完成崩溃堆栈的还原,既优化了体积,又不会影响堆栈的分析。
  • 注解优化
       Dex中注解分为三种类型:Build、Runtime、System。Build 和 Runtime对应 ProGuard 规则 -keepattributes *Annotation*,可优化的 System 注解根据具体类型分别对应 -keepattributes InnerClasses, Signature, EnclosingMethod。跟行号一样,可以通过去除这些规则完成一刀切的优化。但由于我们接入的三方组件自带这些 ProGuard 规则,且部分类的 System 注解有保留的需要,我们选择后置地处理 Dex 文件,基于 Dex 字节码工具完成目标注解的移除。

5.2 资源优化

资源优化的对象分为两类,一是资源查询表 resources.arsc,部分优化操作会涉及到 res/ 及 R文件的修改,但本质都是从 resources.arsc 出发的;二是原始资源文件,包括 res/和 assets/。
介绍优化项前,我们先看一张网上最经典的 resources.arsc 结构图(来源CSDN社区):
图6 resources.arsc 结构图
  • 资源同名化
       在实际应用中,我们默认通过资源 id 查找资源内容,对资源名的使用频率十分低,仅限于通过资源名反查资源 id 以及 通过资源 id 获取资源名两种情况。所以资源项名称字符串池所占据的空间即是我们的优化对象。极限优化结果是,这个池子里仅存放一个字符串,所有 ResTable_entry 的资源项名称 index均指向这个池子里仅有的字符串,即所有资源的名字都变得一样了。实际场景中,我们会有豁免和降级为混淆的需求,例如通过资源名反向查询资源id等情况。
  • 资源文件路径优化
       与 AndResGuard 中的资源路径混淆效果相似,都是尽可能缩短资源文件的路径长度,从而减少 ResTable_entry 的 value 大小。我们将路径 Hash 值转换到碰撞最少的位数,作为最终的混淆结果,其优点在于混淆结果基本上是固定的,无需 applymapping。除此之外,我们还较为激进地去掉了大部分文件的后缀名。
  • 资源配置优化
       从 arsc 中的资源 id 包含了偏移量信息,系统通过偏移量在 arsc 中定位资源。所以图7中的空白区域必须保留一个4字节的占位,以满足偏移量查询方式。我们正在对此部分做优化,宗旨是通过优化不必要的 configuraion,达到减少对齐占位的目的。
图7 resources.arsc 空白占位示意图
  • 图片压缩
       由于 webp 格式受限于 minsdkversion18,我们目前还是针对 png 图片做压缩优化,使用的工具包括 TinyPng 和 ImageOptim。除了出图阶段压缩外,也会有后置流水线做压缩检查。
  • 无用资源清理
       如4.5中提到的,ShrinkResources 并不会真正移除未被引用的资源文件。不过我们可以拿到被 shrink 的 resources 列表,然后再利用资源优化工具做真正的删除。
丨New ShrinkResource
2022.1发布的 AGPv7.1.0 更新了资源缩减功能,添加了实验性选项 android.experimental.enableNewResourceShrinker.preciseShrinking,该选项设置为true后 ShrinkResources 会完全移除未使用的文件资源及 value 资源,但 arsc 中仍会存在这些资源的填充占位。
  • arsc 压缩
       resources.arsc 的压缩体积收益很高,但对其进行压缩会影响启动速度和内存指标。具体原因是:系统在加载 arsc 文件时,若 arsc 文件未压缩,可使用 mmap 进行内存映射;若 arsc 文件被压缩了,则需要将其解压缩后读取到RAM 缓冲区,会增加内存使用,也会拖慢启动速度。在业界大都压缩 arsc 的情况下,百度APP 出于综合考量一直未对 arsc 文件进行压缩。无独有偶,官方出于同样的考虑,从 Android11 开始强制要求 resources.arsc 不可压缩且保持4位对齐,否则会直接安装失败。
图8 Android11强调 resources.arsc 压缩对齐问题

5.3 ZIP优化

  • 压缩
       目前我们使用的压缩算法有 7z 和 zopfli[11],后者压缩率和压缩耗时都有明显增加,稳定性还在验证中。采用新的压缩算法时需特别注意两点,一是不要压缩 resources.arsc;二是注意压缩、 对齐、签名操作的顺序。
  • 文件路径优化
       如章节3.2 ZIP结构分析所示,APK 中有三处体积与文件路径长度相关:META-INF/、压缩源文件数据区的 local file header、中心目录区的 file header,资源文件路径优化效果同样会体现在这里。同理控制 assets/ 下的文件路径也可带来体积收益。

5.4 其他

  • 夜间模式优化
       目前百度APP的夜间资源是一个 APK 包,此前实现方式是与主包中的资源名保持一致,通过反射的方法查询对应 id。现改为 id 一致,这样既避免了反射查询的耗时,章节 5.2中的资源优化也可以应用到资源 APK,进一步减小体积。
  • 混淆规则
       ProGuard/R8 提供了多种多样的规则用以豁免代码优化操作,如果使用不当可能会造成体积的白白浪费。未来我们计划制定一套详细的 ProGuard 规则使用规范,并对每个组件的 ProGuard 规则都进行校验,例如不允许出现本组件包名范围外的 keep 规则、不允许出现包级别 keep 等。
  • 体积流水线
       主要包括仓库体积约束流水线,二进制组件体积检查流水线,以及 APK 组成体积分析流水线,分阶段进行约束与分析。目前百度APP 建设了 APK 体积监控流水线,每当主线有代码合入、触发编译打包后,会即时对编译产物 APK 做体积分析,并与上一次编译产物进行比对,可以马上发现异常的体积增长。

GEEK TALK

06

总结

第五章介绍的优化项自21年8月至22年2月分批次上线,期间业务依旧在高速迭代,虽然有体积监控流水线作用,包体积仍会不可避免地增加。由于优化机制十分底层,需进行充分的线下测试与线上小流量灰度,验证稳定性后才能正式上线。上线/灰度前后百度APP 包体积大小对比如下:
优化项
优化前体积
优化后体积
Diff
Dex 行号优化 & 混淆规则收敛 (上线)
123.58MB
119.37MB
4.21MB
资源名/路径优化 & 7z 压缩 & 夜间模式优化 (上线)
122.54MB
116.36MB
6.18MB
图片压缩 & 热修插桩优化 (上线)
117.07MB
116.00MB
1.07MB
Dex 注解优化 (灰度)
117.16MB
115.95MB
1.21MB
启用R8 (灰度)
119.06MB
116.62MB
2.44MB
zopfli 压缩 & 资源缩减 & 资源配置优化 (灰度)
119.57MB
116.65MB
2.92MB
本文主要介绍了百度APP 包体积优化的基本思路,解构了优化对象,介绍了现有优化工具,最后简单介绍了百度APP 具体实践的优化项及收益。后续我们会针对每个优化类型详细介绍其原理与实现,同时逐步开源通用的体积优化工具,敬请期待。

 END


参考资料:

[1] 包大小与安装转化率 
https://medium.com/googleplaydev/shrinking-apks-growing-installs-5d3fcba23ce2
[2] ZIP格式 
https://pkware.cachefly.net/webdocs/APPNOTE/APPNOTE-6.2.0.txt
[3] ProGuard  
https://www.guardsquare.com/proguard
[4] R8 
https://r8.googlesource.com/r8
[5] ProGuard与R8对比 
https://www.guardsquare.com/blog/proguard-and-r8
[6] AndResGuard 
https://github.com/shwenzhang/AndResGuard
[7] ByteX 
https://github.com/bytedance/ByteX
[8] Booster 
https://github.com/didi/booster
[9] AGP 
https://developer.android.com/studio/releases/gradle-plugin
[10] Titan-Dex 
https://github.com/baidu/titan-dex
[11] zopfli 
https://en.wikipedia.org/wiki/Zopfli

推荐阅读:

百度APP iOS端内存优化实践-大块内存监控方案

百家号基于AE的视频渲染技术探索

百度工程师教你玩转设计模式(观察者模式)

Linux透明大页机制在云上大规模集群实践介绍

超高效!Swagger-Yapi的秘密

百度直播iOS SDK平台化输出改造




一键三连,好运连连,bug不见👇

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存