序、众所周知,每一次开始写这种框架系类的文章的时候,就是我在找工作了,有闲暇时间可以写点东西。哈哈哈哈,26年找工作,简直就是。。。。。
重要的事情说三遍
有岗位的请 CALL 我,谢谢大家了。。。。
有岗位的请 CALL 我,谢谢大家了。。。。
有岗位的请 CALL 我,谢谢大家了。。。。
做 Android 包体积优化时,我以前最容易犯的一个错误是:
看到 APK 大,就马上开始删图片、开
shrinkResources、检查第三方库。
这些手段当然没有错,但顺序错了。
因为一个 APK 变大的原因可能完全不同:
-
有的项目是 DEX 太大;
-
有的是第三方 SDK 带进来了大量代码;
-
有的是 Native
.so占了几十 MB; -
有的是图片资源过多;
-
有的是
assets中塞了模型、数据库或者数据文件; -
还有一些项目是多语言、多 Density、多 ABI 同时打进了 APK。
如果连体积主要花在哪里都不知道,直接开始优化,很容易变成:
优化了半天,删掉 200 KB 图片,结果发现真正的大头是一个 15 MB 的 SO。
所以我现在认为:
Android 包体积优化的第一步不是"优化",而是"分析"。
这一篇先不讨论 R8、资源压缩、SO 裁剪、ReDex 等具体手段,而是从 APK 本身开始,把 APK 的体积组成和分析方法搞清楚。
一、为什么 Android 应用需要关注包体积?
包体积最直接影响的是用户下载。
尤其对于网络条件一般、流量敏感或者存储空间有限的用户,一个明显偏大的应用会增加下载和安装成本。Google 的 Android 官方文档也一直把降低下载大小作为应用质量优化的一部分,并指出更大的应用体积可能影响安装成功率和卸载行为。
但实际项目中,我们还需要区分几个概念:
APK 文件大小
↓
用户实际下载大小
↓
安装后的磁盘占用
这三个数字并不是完全相同的。
因此我们平时说:
"我的 App 有 30 MB。"
首先就应该问:
这 30 MB 指的是 APK 文件大小,应用市场下载大小,还是安装后的占用?
这一点后面会继续讲。
二、APK 到底是什么?
先从最基础的开始。
APK:
Android Package。
从文件组织形式上看,APK 遵循 ZIP 文件格式,所以可以把一个 APK 当成 ZIP 容器查看。
Android 官方 APK Analyzer 文档也明确说明:
APK 文件遵循 ZIP 文件格式,APK Analyzer 会按照 APK 内真实的目录和文件层级展示内容。
实际上我们把:
app-release.apk
改成:
app-release.zip
然后解压,也能够看到里面的大部分文件。
一个典型 APK 大概包含:
-
AndroidManifest.xml -
classes.dex -
classes2.dex -
resources.arsc -
res/ -
assets/ -
lib/ -
META-INF/
理解这些东西分别是什么,是包体积优化的基础。
三、AndroidManifest.xml
Android 项目源码中我们会维护:
AndroidManifest.xml
但是最终 APK 中的 Manifest,并不只是简单把源码文件复制进去。
项目可能存在:
-
App Manifest
-
Library Manifest
-
Flavor Manifest
-
BuildType Manifest
构建过程中这些 Manifest 会进行 Merge。
最终 APK 中保存的是构建后的 Manifest,而且通常使用 Binary XML 格式。Android Studio APK Analyzer 可以把这个二进制 Manifest 重新解析成人类可读的 XML。
从纯包体积角度来说:
AndroidManifest.xml
一般不是最大的部分。
但是它对于分析:
-
第三方 SDK
-
Permission
-
Component
-
Provider
-
Service
-
Receiver
非常有价值。
有时候我们就是通过最终 Manifest 才发现:
某个已经不用的 SDK 其实还被打进了 APK。
四、classes.dex:Android 代码真正进入 APK 后的样子
对于大多数普通 Android 项目来说:
Java / Kotlin 代码最终不会以 .java 或 .kt 文件存在 APK 中。
大体流程可以先理解成:
Java / Kotlin
↓
Java Bytecode
↓
D8 / R8
↓
DEX
最终形成:
classes.dex
如果一个 DEX 放不下所有引用或者构建策略需要多个 DEX,还可能出现:
classes2.dex
classes3.dex
classes4.dex
......
也就是:
MultiDex。
官方文档将 classes.dex 描述为以 DEX 格式编译、供 Dalvik 或 ART 使用的应用代码。
DEX 也是我们做包体积优化时非常值得重点关注的一部分。
它里面不仅仅有方法代码,还包括:
-
String
-
Type
-
Proto
-
Field
-
Method
-
Class Definition
-
Code
-
Debug Information
等等。
所以一个项目:
代码越来越多
↓
第三方依赖越来越多
↓
各种 SDK 越来越多
↓
最终都会反映到 DEX 体积上。
DEX 具体结构以及 R8、第三方依赖如何影响 DEX,我们放到这个系列第二篇详细讲。
五、resources.arsc 是什么?
很多刚接触 APK 分析的人第一次看到:
resources.arsc
都会比较陌生。
它本质上是:
编译后的 Android Resource Table。
例如我们写:
strings.xml
colors.xml
styles.xml
以及各种不同 configuration 下的资源。
经过 AAPT2 编译和链接之后,一部分资源信息最终会进入:
resources.arsc
Android 官方说明,resources.arsc 包含编译后的资源,包括 res/values/ 不同 configuration 中的 XML 内容,以及指向布局、图片等其他资源的路径信息。
所以如果一个大型 App:
支持几十种语言
↓
大量 string resource
↓
大量 style
↓
大量 configuration
那么:
resources.arsc
也可能变得比较明显。
六、res/:图片、布局和 Android 资源
res/
里面就是我们非常熟悉的:
-
drawable
-
mipmap
-
layout
-
anim
-
xml
-
raw
-
font
等等。
这里经常出现包体积大户:
图片。
特别是项目发展多年以后,很容易出现:
-
大尺寸 PNG
-
重复图片
-
已经不用的图片
-
多份不同 Density 图片
-
动画帧图
-
第三方 SDK Resource
所以:
res/
通常是包体积分析必须重点检查的目录。
不过这里有一个非常重要的原则:
看到 res 大,再做资源优化。
如果分析发现整个 APK:
DEX 占 80%
Resource 只占 5%
那么这个阶段疯狂压图片,收益必然有限。
七、assets/:最容易被忽略的体积大户之一
assets/
和 res/ 有一个很大的区别:
Assets 通常会以更接近原始文件的形式打包,并由 AssetManager 访问。Android 官方也将 assets/ 定义为应用可以通过 AssetManager 获取的资源目录。
这里经常出现:
-
JSON
-
HTML
-
字体
-
数据库
-
音视频
-
Web 前端资源
-
ML 模型
-
游戏资源
-
第三方 SDK 数据文件
而且:
Assets 很容易被开发者忘记。
比如一个 SDK:
代码本身只有几百 KB,
但它可能附带:
2 MB 数据文件。
你如果只盯着 DEX:
根本发现不了。
这也是为什么包体积分析一定应该从:
APK 整体结构
开始,而不是直接钻进 DEX。
八、lib/:Native 项目最容易爆炸的地方
如果项目使用:
C / C++
或者某些第三方 SDK 使用 Native Code,
APK 里面会出现:
lib/
例如:
lib/arm64-v8a/xxx.so
lib/armeabi-v7a/xxx.so
lib/x86_64/xxx.so
这些:
.so
就是 Native Dynamic Library。
不同 ABI 可能包含一套独立的 SO。
例如一个库:
arm64:
10 MB
armeabi-v7a:
9 MB
x86_64:
12 MB
如果全部打进一个 APK:
体积可能一下增加几十 MB。
Android 官方列出的 APK lib/ 目录也是按照 CPU/ABI 分目录存放编译后的 Native Code。
所以:
对于使用大量 Native SDK 的项目,SO 往往比 Java/Kotlin 代码更值得首先检查。
Native 包体积优化我们会放到第三篇专门展开。
九、META-INF 是什么?
APK 中还经常存在:
META-INF/
其中可能包含:
MANIFEST.MF
CERT.SF
CERT.RSA
以及其他 metadata。
需要特别注意:
我们不能简单认为:
"APK 所有签名数据都在 META-INF。"
这是不准确的。
传统的 APK Signature Scheme v1 与 ZIP/JAR 的 META-INF 结构关系比较直接,但现代 Android APK 还存在 v2/v3 等不同签名方案。
关于 APK 签名我们后面的工程化篇会单独讲。
这一篇只需要知道:
META-INF也是 APK 结构的一部分,但通常不是包体积优化的大头。
十、所以一个 APK 的体积可以先怎么拆?
实际做包体积分析时,我习惯先做一级分类:
APK
↓
DEX
↓
Resources
↓
Native
↓
Assets
↓
META-INF
↓
Other
也就是先回答:
钱花在哪了?
而不是马上回答:
怎么省钱?
这两个问题的顺序非常重要。
十一、文件大小其实还有"压缩前"和"压缩后"
APK 是 ZIP 格式,因此里面一个 Entry 通常可以存在:
原始大小
和:
压缩后的大小
两个概念。
Android Studio APK Analyzer 中会看到:
Raw File Size
以及:
Download Size
当前官方定义中,Raw File Size 表示该实体对 APK 总文件大小的贡献,而 Download Size 是 Google Play 传输时估算的压缩下载大小。
所以:
一个文件:
Raw:
2 MB
不代表用户一定:
下载 2 MB。
如果它非常容易压缩:
最终传输大小可能小很多。
十二、为什么这个区别很重要?
假设两个文件:
A:
Raw = 5 MB
压缩后 = 500 KB
B:
Raw = 3 MB
压缩后 = 2.9 MB
如果只看:
Raw Size
你会觉得:
A 更值得优化。
但站在:
下载体积
角度,
B 可能反而更值得关注。
所以包体积优化以后会逐渐涉及三个视角:
APK 文件视角
最终 APK 本身多大。
Download Size 视角
用户真正需要下载多少数据。
Install Size 视角
安装以后真正占设备多少空间。
不要把这几个数字混为一谈。
十三、Android Studio APK Analyzer 怎么用?
Android Studio 自带了非常好用的:
APK Analyzer。
目前可以通过:
Build → Analyze APK
选择 APK 或 App Bundle 进行分析。
打开以后,可以看到:
-
APK 内部文件
-
文件大小
-
相对占比
-
Manifest
-
Resources
-
DEX
-
Class
-
Package
-
Method Reference
-
两个 APK/AAB 的差异
Android Studio 还支持直接对两个 APK 或 App Bundle 做 Side-by-side Compare。
所以:
包体积优化第一阶段,完全没必要一上来就找复杂第三方工具。
先把 APK 扔进 APK Analyzer。
很多问题其实已经非常明显了。
十四、DEX 还能继续往下看
APK Analyzer 不只是:
看 classes.dex 有多大。
点开 DEX 以后,还可以继续分析:
-
Package
-
Class
-
Defined Methods
-
Referenced Methods
官方特别区分:
Referenced Methods
和:
Defined Methods。
简单理解:
Defined:
这个 DEX 中真正定义的方法。
Referenced:
这个 DEX 引用的方法。
Referenced Methods 还可能包括:
-
自己代码的方法
-
第三方依赖的方法
-
Java / Android API
这也是为什么:
Method Reference 数量不能直接等价成"项目源码写了多少个方法"。
这一点在 MultiDex 和 DEX 优化里非常重要。
十五、除了 Android Studio,还有 apkanalyzer
如果不想打开 Android Studio:
Android SDK 还提供:
apkanalyzer
命令行工具。
它支持分析:
-
apk
-
files
-
manifest
-
dex
-
resources
并且可以比较 APK。
它一般位于 Android SDK Command-Line Tools 的:
cmdline-tools/<version>/bin/apkanalyzer
下面。
这个工具对于:
脚本化
和:
CI
特别有意义。
因为我们后面真正做工程化包体积治理的时候,不可能要求:
每次 CI Build 完,都让一个开发者打开 Android Studio 人工看一下。
最终一定会走向:
自动分析。
十六、实战:一个 12.56 MB APK,应该先优化什么?
下面看一个真实测试。
我做了一个用于包体积实验的 Android 项目,故意加入:
-
FastUtil
-
Guava
-
BouncyCastle
-
Protobuf
-
Tink
等依赖。
最终 Debug APK:
约:
12.56 MB
包含:
7 个 DEX。
如果只看:
12.56 MB
其实你不知道从哪里开始。
但是把 APK 分类分析以后发现:
DEX 占 APK:
约 89.21%。
这个结论一下就改变优化方向。
说明这个 APK:
主要矛盾不是:
图片。
不是:
SO。
也不是:
普通 Resources。
而是:
DEX。
所以接下来应该优先调查:
-
第三方依赖
-
Transitive Dependency
-
R8
-
无用代码
-
DEX Layout
-
Method Reference
-
ReDex
而不是花大量时间压几张 PNG。
十七、继续检查 Largest Files
APK 分析还有一个非常实用的手段:
把 APK 里面最大的文件按大小排序。
很多隐藏问题:
一下就出来了。
例如这个测试 APK 中:
BouncyCastle Picnic 相关数据文件大约:
1.16 MB。
这意味着:
即使后面把 DEX 优化得再好:
这 1.16 MB 数据文件也不会因为 DEX Optimizer 自动消失。
正确方向反而应该是:
这个 Dependency 是否必要?
能不能换轻量版本?
这份数据文件到底有没有真正使用?
这也引出一个非常重要的包体积优化原则:
不同类型的体积问题,需要不同的优化工具。
十八、我现在分析一个 APK,通常先看什么?
可以总结成一套比较简单的流程。
第一步:看 APK 总大小
先建立:
Baseline。
比如:
12.56 MB。
第二步:看一级分类
分别统计:
DEX
Resource
Native
Assets
Other
占多少。
第三步:找占比最大的 Category
例如:
DEX = 89%
那么:
第一优化方向就是 DEX。
第四步:看 Top Largest Files
查:
单文件大户。
例如:
SO
图片
模型
数据库
第三方数据文件。
第五步:继续钻入大类
如果:
DEX 大
↓
看 Dependency / R8 / Method Reference。
如果:
Native 大
↓
看 ABI / SO。
如果:
Resource 大
↓
看图片 / Density / Language。
如果:
Assets 大
↓
看模型 / 数据 / 字体 / Web Resource。
第六步:建立 Baseline
例如记录:
APK Size
DEX Size
DEX Count
Native Size
Resources Size
Assets Size
Largest Files
以后所有优化:
都必须和这个 Baseline 对比。
十九、为什么一定要建立 Baseline?
比如你做一次优化:
原来:
12.56 MB
优化以后:
12.10 MB。
你可以明确知道:
减少:
460 KB。
但是还不够。
继续问:
460 KB 从哪里减少的?
可能:
DEX:
-800 KB
但是:
Resources:
+340 KB。
最后总包:
只减少:
460 KB。
如果只看最终 APK Size:
这个信息就看不到。
所以成熟的包体积治理:
不能只有:
Total Size。
还应该跟踪:
Size Composition。
二十、包体积优化最容易犯的几个错误
错误一:APK 大就开始压图片
先看 Resource 到底占多少。
错误二:DEX 多就认为一定有问题
DEX Count 是一个信号,不是最终结论。
真正还要看:
-
每个 DEX 大小
-
Method Reference
-
Class Distribution
-
R8
-
MultiDex 策略
错误三:只看总包大小
总大小下降:
不代表所有维度都改善。
最好同时观察:
DEX
Resources
SO
Assets。
错误四:把 Method Reference 当作源码方法数量
Referenced Method:
不只是你自己定义的方法。Android Studio APK Analyzer 官方也明确区分 Referenced Methods 和 Defined Methods。
错误五:只优化,不对比
如果没有 Baseline:
你甚至无法准确回答:
这个方案到底优化了多少?
二十一、Android 包体积优化应该形成什么思维?
看到一个大 APK:
不要马上问:
怎么把它变小?
先问:
1. 大在哪里?
DEX?
SO?
Resources?
Assets?
2. 为什么大?
代码?
Dependency?
图片?
ABI?
语言?
SDK?
3. 哪种工具能解决?
R8?
Resource Shrinker?
图片优化?
ABI Split?
Dependency Governance?
ReDex?
4. 优化以后减少了什么?
不是只说:
APK 减少 2 MB。
而应该能回答:
DEX 减少多少?
Resource 减少多少?
SO 减少多少?
5. 优化有没有副作用?
比如:
资源有没有丢?
Class 有没有被错误删除?
SO ABI 是否完整?
优化后的 APK 是否还能正常运行?
这才是一套完整的包体积优化思路。
二十二、总结
这一篇其实没有真正开始:
"瘦身"。
因为我认为:
包体积优化最重要的第一步,就是先不要优化。
先把 APK 分析清楚。
APK 本质上是一个包含 Android 特定内容的 ZIP 容器,里面主要包括:
-
DEX
-
Resources
-
Native Library
-
Assets
-
Manifest
-
Metadata
不同项目:
体积主要来源完全可能不同。
所以正确流程应该是:
APK
↓
Analyze
↓
Size Composition
↓
Find Bottleneck
↓
Choose Optimization
而不是:
APK 大
↓
随便找一个"Android 包体积优化大全"
↓
所有配置全部打开。
真正有效的包体积优化,一定是:
数据驱动。
先知道:
APK 到底大在哪里。
下一步才讨论:
为什么会这么大,以及怎么把它真正减下来。
下一篇
《Android 包体积优化(二):深入 DEX、R8 与第三方依赖优化》
下一篇会重点讨论:
-
DEX 文件到底是什么
-
DEX Header 有什么
-
Method ID 是什么
-
64K 限制到底限制了什么
-
MultiDex 为什么出现
-
D8 和 R8 的关系
-
R8 如何 Shrink Code
-
minifyEnabled到底发生了什么 -
第三方依赖为什么是 DEX 体积大户
-
Transitive Dependency 怎么排查
-
为什么"少引一个 SDK"有时候比压几十张图片更有效
-
如何系统治理大型 Dependency
从第二篇开始,真正进入:
代码体积优化。