本文主要记载
- 1 为什么要优化包体积?
- 2 包体积与应用性能有关系吗?
- 3 根据 APK 组成分析优化
- 3.1 代码相关
- 3.1.1 Dex 探秘
- 3.1.2 Proguard
- 3.1.3 D8 与 R8 优化
- 3.1.4 去除 debug 信息与行号信息
- 3.1.5 Dex 分包
- Redex Ubuntu 具体实践
- 3.1.6 使用 XZ Utils 进行 Dex 压缩
- 3.1.7 其他
- 3.2 资源相关
- 3.2.1 使用 Lint 的 Remove Unused Resource
- 3.2.2 shrinkResources
- 3.2.3 AndResGuard 工具
- 实践
- 3.3 Native Library 相关
- 3.3.1 Library 压缩
- 3.3.2 Library 合并与裁剪
- 3.1 代码相关
1 为什么要优化包体积?
下载转化率。
APK刷脂在 初创项目 中优先级是比较低的,相对于 启动优化、卡顿优化 它的好处不是那么明显,包体积优化最主要的好处是对应用 下载转化率 的影响,它是App业务运营的重要指标之一,在项目精细化运营的阶段是非常重要的。在2018年的Google I/O,Google透露了Google Play上安装包体积与下载转化率的关系图。
从这张图上看,大体来说,安装包越小,转化率越高这个结论成立。包体积越小,用户下载等待的时间也会越短,所以下载转换成功率也就越高,一个
100MB的应用,用户即使点了下载,也可能因为网络速度慢、突然反悔下载失败。对于一个10MB的应用,用户点了下载之后,在犹豫要不要下的时候已经下载完了。但是正如上图的数据,安装包大小与转化率的关系是非常微妙的。10MB跟15MB可能差距不大,但是10MB跟40MB的差距还是非常明显的。而且,现在很多大型的
App一般都会有一个极速版本的App,这个也是出于下载转化率方面的考虑。推广成本。一般来说,包体积对渠道推广和厂商预装的单价会有非常大的影响。特别是厂商预装,这主要是因为厂商留给预装应用的总空间是有限的。如果你的包体积非常大,那就会影响厂商预装其他应用。
应用市场。
Google Play要求超过100MB的应用只能使用 APK 扩展文件方式 上传,由此可见应用包体积对应用市场的服务器带宽成本还是会有一点压力的。如果想避免使用扩展文件,并且想要应用程序的下载大小大于100 MB,则应该使用Android App Bundles上传应用程序。
2 包体积与应用性能有关系吗?
包体积除了转化率的影响,主要还会对 App 三个方面 的性能有一定的影响
- 安装时间:文件拷贝、
Library解压,并且,在编译ODEX的时候,特别是对于Android 5.0和6.0系统来说,耗费的时间比较久,而Android 7.0之后有了 混合编译,所以还可以接受。最后,App变大后,其签名校验的时间也会变长。微信13个Dex光是编译ODEX的时间可能就要5分钟。 - 运行内存:在内存优化的时候我们就说过,
Resource资源、Library以及Dex类加载这些都会占用不少的内存。 - ROM 空间:如果应用的安装包大小为
50MB,那么启动解压之后很可能就已经超过100MB了。并且,如果 闪存空间不足,很可能出现 “写入放大” 的情况。具体情况将在I/O 优化中讲解。
3 根据 APK 组成分析优化
我们都知道,Android 项目最终会编译成一个 .apk 后缀的文件,实际上它就是一个 压缩包。因此,它内部还有很多不同类型的文件,这些文件,按照大小,共分为 代码优化、资源优化、So 优化。
3.1 代码相关
classes.dex,我们在项目中所编写的 java 文件,经过编译之后会生成一个 .class 文件,而这些所有的 .class 文件呢,它最终会经过 dx 工具编译生成一个 classes.dex。
对于大部分应用来说,Dex 都是包体积中的大头。日常开发中 Dex 数量从 1 个增长到 10 多个,我们的代码量真的增长了那么多倍吗?而且 Dex 的数量对用户安装时间也是一个非常大的挑战,在不砍功能的前提下,我们看看有哪些方法可以减少这部分空间。
与传统 jar 文件相比,Dex 文件的大小能够缩减 50% 左右。关于 Class 文件与 Dex 文件的结果对比图如下所示:
3.1.1 Dex 探秘
Dex 是 Android 系统的可执行文件,包含 应用程序的全部操作指令以及运行时数据。因为 Dalvik 是一种针对嵌入式设备而特殊设计的 Java 虚拟机,所以 Dex 文件与标准的 Class 文件在结构设计上有着本质的区别。
当 Java 程序被编译成 class 文件之后,还需要使用 dx 工具将所有的 class 文件整合到一个 dex 文件中,这样 dex 文件就将原来每个 class 文件中都有的共有信息合成了一体,这样做的目的是 保证其中的每个类都能够共享数据,这在一定程度上降低了信息冗余,同时也使得文件结构更加紧凑。
Dex 一般在应用包体积中占据了不少比重,并且 Dex 数量越多,App 的安装时间也会越长。所以,优化它们可以说是重中之重。下面就来看看有哪些方式可以优化 Dex 这部分的体积。
3.1.2 ProGuard
代码混淆的形式
目前,代码混淆的形式主要有三种,如下所示:
- 将代码中的各个元素,比如类、函数、变量的名字改变成无意义的名字。例如将
hasValue转换成单个的字母a。这样,反编译阅读的人就无法通过名字来猜测用途。 - 重写代码中的部分逻辑,将它变成功能上等价,但是又难以理解的形式。比如它会改变循环的指令、结构体。
- 打乱代码的格式,比如多加一些空格或删除空格,或者将一行代码写成多行,将多行代码改成一行。
- 将代码中的各个元素,比如类、函数、变量的名字改变成无意义的名字。例如将
Proguard的作用在
Android SDK里面集成了一个工具 —Proguard,它是一个免费的Java类文件压缩、优化、混淆、预先校验的工具。它的主要作用大概可以概括为两点,如下所示:瘦身:它可以检测并移除未使用到的类、方法、字段以及指令、冗余代码,并能够对字节码进行深度优化。最后,它还会将类中的字段、方法、类的名称改成简短无意义的名字。
压缩:默认开启,以减小应用体积,移除未被使用的类和成员,并且会在优化动作执行之后再次执行,因为优化后可能会再次暴露一些未被使用的类和成员。我们可以使用如下规则来关闭压缩:
1
2# 关闭压缩
-dontshrink优化:默认开启,在字节码级别执行优化,让应用运行的更快。使用如下规则可进行优化相关操作:
1
2
3
4# 关闭优化
-dontoptimize
# 表示proguard对代码进行迭代优化的次数,Android一般为5
5优化细节包括:内联、修饰符、合并类、方法、
Gson库的使用、把类都标记为final、把枚举类型简化为常量、把一些类都垂直合并进当前类的结构中、把一些类都水平合并进当前类的结构中、移除write-only字段、把类标记为私有的、把字段的值跨方法地进行传递、把一些方法标记为私有、静态或final、解除方法的synchronized标记、移除没有使用的方法参数等 30 多种优化项
安全:增加代码被反编译的难度,一定程度上保证代码的安全。
混淆:默认开启,增大反编译难度,类和类成员会被随机命名,除非用 优化字节码 等规则进行保护。使用如下规则可以关闭混淆:
1
2# 关闭混淆
-dontobfuscate
所以说,混淆不仅是保障 Android 程序源码安全 的 第一道门槛,还可以优化字节码的大小
Proguard的配置混淆之后,默认会在工程目录
app/build/outputs/mapping/release下生成一个mapping.txt文件,这就是混淆规则,所以我们可以根据这个文件把混淆后的代码反推回原本的代码。要使用混淆,我们只需配置如下代码即可:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28buildTypes {
release {
// 1、是否进行混淆
minifyEnabled true
// 2、开启zipAlign可以让安装包中的资源按4字节对齐,这样可以减少应用在运行时的内存消耗
zipAlignEnabled true
// 3、移除无用的resource文件:当ProGuard 把部分无用代码移除的时候,
// 这些代码所引用的资源也会被标记为无用资源,然后
// 系统通过资源压缩功能将它们移除。
// 需要注意的是目前资源压缩器目前不会移除values/文件夹中
// 定义的资源(例如字符串、尺寸、样式和颜色)
// 开启后,Android构建工具会通过ResourceUsageAnalyzer来检查
// 哪些资源是无用的,当检查到无用的资源时会把该资源替换
// 成预定义的版本。主要是针对.png、.9.png、.xml提供了
// TINY_PNG、TINY_9PNG、TINY_XML这3个byte数组的预定义版本。
// 资源压缩工具默认是采用安全压缩模式来运行,可以通过开启严格压缩模式来达到更好的瘦身效果。
shrinkResources true
// 4、混淆文件的位置,其中 proguard-android.txt 为sdk默认的混淆配置,
// 它的位置位于android-sdk/tools/proguard/proguard-android.txt,
// 此外,proguard-android-optimize.txt 也为sdk默认的混淆配置,
// 但是它默认打开了优化开关。并且,我们可在配置混淆文件将android.util.Log置为无效代码,
// 以去除apk中打印日志的代码。而 proguard-rules.pro 是该模块下的混淆配置。
// 如果想让各个 library 的 proguard-rules 也生效使用 consumerProguardFiles 'proguard-rules.pro'
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}在执行完
ProGuard之后,ProGuard都会在${project.buildDir}/outputs/mapping/${flavorDir}/生成以下文件:文件名 描述 dump.txt APK 中所有类文件的内部结构 mapping.txt 提供原始与混淆过的类、方法和字段名称之间的转换,
可以通过 proguard.obfuscate.MappingReader 来解析seeds.txt 列出未进行混淆的类和成员 usage.txt 列出从 APK 移除的代码
混淆的基本规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31# * 表示仅保持该包下的类名,而子包下的类名还是会被混淆
class com.json.chao.wanandroid.*
# ** 表示把本包和所含子包下的类名都保持
class com.json.chao.wanandroid.**
# 既保持类名,又保持里面的内容不被混淆
class com.json.chao.wanandroid.* {*;}
# 也可以使用Java的基本规则来保护特定类不被混淆,比如extend,implement等这些Java规则
public class * extends android.app.Activity
# 保留MainPagerFragment内部类JavaScriptInterface中的所有public内容不被混淆
class com.json.chao.wanandroid.ui.fragment.MainPagerFragment$JavaScriptInterface {
public *;
}
# 仅希望保护类下的特定内容时需使用匹配符
//匹配所有构造器
//匹配所有字段
//匹配所有方法
# 还可以在上述匹配符前面加上private 、public、native等来进一步指定不被混淆的内 容
class com.json.chao.wanandroid.app.WanAndroidApp {
public <fields>;
}
# 也可以加入参数,以下表示用java.lang.String作为入参的构造函数不会被混淆
class com.json.chao.wanandroid.app.WanAndroidApp {
public <init>(java.lang.String);
}
# 不需要保持类名,仅需要把该类下的特定成员保持不被混淆时使用keepclassmembers
# 如果拥有某成员,要保留类和类成员使用-keepclasseswithmembers添加在
AndroidMainfest中的类默认不会被混淆,所以四大组件和Application的子类和Framework层下所有的类默认不会进行混淆,自定义的View默认也不会被混淆。所以下面代码加不加都可以1
2
3
4
5
6public class * extends android.app.Activity
public class * extends android.app.Application
public class * extends android.app.Service
public class * extends android.content.BroadcastReceiver
public class * extends android.content.ContentProvider
public class * extends android.view.View“十个
ProGuard配置九个坑”,特别是各种第三方SDK。我们需要仔细检查最终合并的ProGuard配置文件,是不是存在过度keep的现象。可以通过下面的方法输出
ProGuard的最终配置,尤其需要注意各种的keep *,很多情况下我们只需要keep其中的某个包、某个方法,或者是类名就可以了。1
configuration.txt
3.1.3 D8 与 R8 优化
D8 优化
1,
Dex编译时间更短2,
.dex文件大小更小3,
D8编译的.dex文件将拥有相同或者是更好的运行时性能4, 包含
Java 8语言支持的处理
在 gradle.properties 文件中新增如下代码。Android Studio 3.1 或之后的版本 D8 将会被作为默认的 Dex 编译器。
1 | android.enableD8 = true |
- R8 优化
如果我们当前使用的是 Android Studio 3.4 或 Android Gradle 插件 3.4.0 及其更高版本, R8 会作为默认编译器。否则,我们必须要在 gradle.properties 中配置如下代码让 App 的混淆去支持 R8,如下所示:
1 | =true |
- R8 与混淆相比优势在哪里呢
ProGuard 和 R8 都应用了基本名称混淆:它们都使用简短,无意义的名称重命名类,字段和方法。他们还可以删除调试属性。但是,R8 在 inline 内联容器类中更有效,并且在删除未使用的类,字段和方法上则更具侵略性。例如,R8 本身集成在 ProGuard V6.1.1 版本中,在压缩 apk 的大小方面,与 ProGuard 的 8.5% 相比,使用 R8 apk 尺寸减小了约 10%。并且,随着 Kotlin 现在成为 Android 的第一语言,R8 进行了 ProGuard 尚未提供的一些 Kotlin 的特定的优化。
想对 D8 与 R8 的实现细节有更多地了解前往 Jake Wharton 个人博客。
3.1.4 去除 debug 信息与行号信息
在讲解什么是 deubg 信息与行号信息之前,我们需要先了解 Dex 的一些知识。
我们都知道,JVM 运行时加载的是 .class 文件,而 Android 为了使包大小更加紧凑、运行时更加高效就发明了 Dalvik 和 ART 虚拟机,两种虚拟机运行的都是 .dex 文件,当然 ART 虚拟机还可以同时运行 oat 文件。
所以 Dex 文件里的信息内容和 Class 文件包含的信息是一样的,不同的是 Dex 文件对 Class 中的信息做了去重,一个 Dex 包含了很多的 Class 文件,并且在结构上有比较大的差异,Class 是流式的结构,Dex 是分区结构,Dex 内部的各个区块间通过 offset 来进行索引。
某个应用通过相同的 ProGuard 规则生成一个 Debug 包和 Release 包,其中 Debug 包的大小是 4MB,Release 包只有 3.5MB。
既然它们 ProGuard 的混淆与优化的规则是一样的,那它们之间的差异在哪里呢?那就是 DebugItem。

从图中可以看到,Dex 文件的结构主要分为四大块:header 区,索引区,data 区,map 区。而我们的 debug 与行号信息就保存在 data 区中的 DebugItems 区域。
DebugItem 里面主要包含两种信息:
- 调试的信息。函数的参数变量和所有的局部变量。
- 排查问题的信息。所有的指令集行号和源文件行号的对应关系。
为了在应用出现问题时,我们能在调试的时候去显示相应的调试信息或者上报 crash 或者主动获取调用堆栈的时候能通过 debugItem 来获取对应的行号,我们都会在混淆配置中加上下面的规则:
1 | SourceFile, LineNumberTable |
对于去除 debuginfo 以及行号信息更详细的分析,看一下支付宝的一篇文章《Android 包大小极致压缩》 。通过这个方法,我们可以实现既保留行号,但是又可以减少大约 5% 的 Dex 体积。
事实上,支付宝参考的是 Facebook 的一个开源编译工具 ReDex 。ReDex 除了没有文档之外,绝对是客户端领域非常硬核的一个开源库,非常值得去认真研究。ReDex 这个库去除 Debug 信息是通过 StripDebugInfoPass 完成。
1 | { |
3.1.5 Dex 分包
当我们的 APK 过大时,Dex 的方法数就会超过 65536 个,因此,必须采用 mutildex 进行分包,但是此时每一个 Dex 可能会调用到其它 Dex 中的方法,这种跨 Dex 调用的方式会造成许多冗余信息,
简单来说,如下图所示如果将 Class A 与 Class B 分别编译到不同的 Dex 中,由于 method a 调用了 method b,所以在 classes2.dex 中也需要加上 method b 的 id。

因为跨 Dex 调用造成的这些冗余信息,它对我们 Dex 的大小会造成下如下两点:
- method id 爆表:每个
Dex的method id需要小于65536,因为method id的大量冗余导致每个Dex真正可以放的Class变少,这是造成最终编译的Dex数量增多。 - 信息冗余:因为需要记录跨
Dex调用的方法的详细信息,所以在classes2.dex我们还需要记录Class B以及method b的定义,造成string_ids、type_ids、proto_ids这几部分信息的冗余。
为了进一步减少 Dex 的数量,我们希望每个 Dex 的方法数都是满的,即分配了 65536 个方法。最好保证 Dex 有效率应该在 80% 以上。
1 | Dex信息有效率 = define methods数量/reference methods数量 |
define methods 与 reference methods 可以通过 Android Studio 查看 APK 选择一个 dex 文件查看。define classes and methods 是指真正在这个 Dex 中定义的类以及它们的方法。而 reference methods 指的是 define methods 以及 define methods 引用到的方法。

关于 Dex 的格式以及各个字段的定义,可以参考 Dex文件格式详解
那如何实现 Dex 信息有效率提升呢?
关键在于我们需要将有调用关系的类和方法分配到同一个 Dex 中,即减少跨 Dex 的调用的情况。但是由于类的调用关系非常复杂,我们不太可能可以计算出最优解,只能得到局部的最优解。
所幸的是,ReDex 的 CrossDexDefMinimizer 类分析了类之间的调用关系,并使用了贪心算法去计算局部的最优解(编译效果和 dex 优化效果之间的某一个平衡点)。使用 InterDexPass 配置项可以把互相引用的类尽量放在同个 Dex,增加类的 pre-verify,以此提升应用的冷启动速度。
1 | { |
Redex Ubuntu 具体实践
ubuntu 下载 我试过 16、18、20 的 Ubuntu 最终 20 的编译成功了。
首先我们安装必要东西
1
2
3
4
5
6
7
8
9
10
11
12
13
14sudo apt-get install \
g++ \
automake \
autoconf \
autoconf-archive \
libtool \
liblz4-dev \
liblzma-dev \
make \
zlib1g-dev \
binutils-dev \
libjemalloc-dev \
libiberty-dev \
libjsoncpp-dev然后拉取代码
1
2git clone https://github.com/facebook/redex.git
cd redex一般情况下都需要升级
boost,可以先执行以下编译试试~-j4表示使用4个线程编译。1
2
3# if you're using gcc, please use gcc-5
autoreconf -ivf && ./configure && make -j4
sudo make install如果提示
error boost版本的问题,升级boost,在redex目录内输入如下命令,然后重新执行第三步1
sudo ./get_boost.sh
在第三步编译过程中 18 版本的 `Ubuntu` 一直报错,各种办法未解决,`16` 版本 `boost` 升级配置不上。config配置在
Redex在运行的时候,它是根据redex/config/default.config这个配置文件中的通道passes中添加不同的优化项来对APK的Dex进行处理的,我们可以参考redex/config/default.config这个默认的配置,里面的passes中不同的配置项都有特定的优化。为了优化App的包体积,我们再加上interdex_stripdebuginfo.config中的配置项去删除debugInfo和减少跨Dex调用的情况,最终的interdex_stripdebuginfo.config配置代码 如下所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33{
"redex" : {
"passes" : [
"StripDebugInfoPass",
"InterDexPass",
"RegAllocPass"
]
},
"StripDebugInfoPass" : {
"drop_all_dbg_info" : false,
"drop_local_variables" : true,
"drop_line_numbers" : false,
"drop_src_files" : false,
"use_whitelist" : false,
"cls_whitelist" : [],
"method_whitelist" : [],
"drop_prologue_end" : true,
"drop_epilogue_begin" : true,
"drop_all_dbg_info_if_empty" : true
},
"InterDexPass" : {
"minimize_cross_dex_refs": true,
"minimize_cross_dex_refs_method_ref_weight": 100,
"minimize_cross_dex_refs_field_ref_weight": 90,
"minimize_cross_dex_refs_type_ref_weight": 100,
"minimize_cross_dex_refs_string_ref_weight": 90
},
"RegAllocPass" : {
"live_range_splitting": false
},
"string_sort_mode" : "class_order",
"bytecode_sort_mode" : "class_order"
}最后,执行相应的
redex优化命令它使用了贪心这种局部最优解的方式去减少跨
Dex调用造成的信息冗余,命令如下所示(注意,在redex的前面可能需要加上Android sdk的路径,因为redex中使用到了sdk下的zipalign工具):1
ANDROID_SDK=/usr/lib/android-sdk redex --sign -s <xxx>.jks -a <alias> -p 123456 -c redex/config/interdex_stripdebuginfo.config -P proguard-rules.pro <youapk>.apk -o redexSign.apk
上述
redex命令的关键参数含义如下所示:- –sign: 对生成的
apk进行签名。 - -s: 配置应用的签名文件。
- -a: 配置应用签名的
key_alias。 - -p: 配置应用签名的
key_password。 - -c: 指定
redex进行Dex处理时需要依据的CONFIG配置文件。 - -o: 指定生成
APK的全路径。 - -P: 是混淆文件
- –sign: 对生成的
补充:
Linux中下载android-sdk1
sudo apt update && sudo apt install android-sdk
我最后在
/usr/lib/android-sdk中找到运行 test 需要在
redex目录下执行./test/setup.sh文件中有个pushd test,还需要安装curl和unzip如果报错
fatal error:Killed signal terminated program cc1plus是内存不足了,请分配足够大的内存,还有可能cpu太高,请适当减少运行线程,autoreconf -ivf && ./configure && make -j4这句话中-j4表示开启4个线程,如果不是多核的开线程没有用。我最后用make -j2给了4G内存执行成功。
经过三天努力,终于编译成功,但是在 release 包下 体积并没有减少, debug 有变少。
通过搜索,可能是没有做到压缩,只做了 dex 重排加速了启动,所以体积可能不小反而变大一点点。
也有可能项目太小发挥不了作用。
还会再继续探究 ~
3.1.6 使用 XZ Utils 进行 Dex 压缩
XZ Utils 和 7-Zip 一样,内部使用的都是 LZMA 算法。LZMA 提供了高压缩比和快速解压缩,因此非常适合嵌入式应用。对于 Dex 格式来说,XZ 的压缩率可以比 Zip 高 30% 左右。
在 FaceBook 的 App 中就使用了 Dex 压缩 的方式,而且它将 Dex 压缩后的文件都放在了 assets 目录中


我们先看到上图中的 classes.dex,其中仅包含了启动时要用到的类,这样可以为 Dex 压缩文件 secondary.dex.jar.xzs 的解压争取时间。
但是,这套方案还存在一些问题:
- 首次启动解压:应用首次启动的时候,需要将
secondary.dex.jar.xzs解压缩,Facebook使用多线程解压的方式,这个耗时在高端机是几百毫秒左右,在低端机可能需要 3~5 秒。 - ODEX 文件生成:当
Dex非常多的时候会增加应用的安装时间,如果还使用了压缩Dex的方式,那么首次生成ODEX的时间可能就会超过 1 分钟。Facebook为了解决这个问题,使用了ReDex另外一个超级硬核的方法,那就是 oatmeal 。- 对于正常的流程,我们需要
fork进程来生成dex2oat,这个耗时一般都比较大。通过oatmeal,我们直接在本进程生成ODEX文件。一个10MB的Dex,如果在Android 5.0生成一个ODEX的耗时大约在10秒以上,在Android 8.0使用speed模式大约在 1 秒左右,而通过oatmeal这个耗时大约在100毫秒左右。
- 对于正常的流程,我们需要
ReDex 提供的功能:
- Interdex:类重排和文件重排、
Dex分包优化。 - Oatmeal: 直接生成
Odex文件。 - StripDebugInfo: 去除
Dex中的Debug信息。 - 源码中 access-marking 模块: 删除
Java access方法 。 - 源码中 type-erasure 模块:类型擦除。
3.1.7 其他
使用 Lint 检测无效代码
步骤:点击菜单栏 Analyze -> Run Inspection by Name -> unused declaration -> Moudule ‘app’ -> OK避免产生 access 方法的方式
编译阶段去除
access方法已集成的插件
- access-inline-plugin(
access方法内联) - shrink-r-plugin(
R文件瘦身和无用资源检查) - closeable-check-plugin(文件流的
close检查) - const-inline-plugin(常量内联)
- field-assign-opt-plugin(优化多余赋值指令)
- getter-setter-inline-plugin (
getter和setter方法内联) - method-call-opt-plugin(干净地删除某些方法调用,如
Log.d) - coverage-plugin(线上代码覆盖率)
- refer-check-plugin(检查是否有调用不存在的方法和引用不存在的字段)
- serialization-check-plugin(序列化检查)
- SourceFileKiller(删除
SourceFile和行号属性,作为示例) - ButterKnifeChecker(检测跨
module使用ButterKnife可能导致的问题) - RFileKnife(修复
R.java太大编译报code too large的问题)
- access-inline-plugin(
3.2 资源相关
res、assets、编译后的二进制资源文件 resources.arsc 和 清单文件 等等。res 和 assets 的不同在于 res 目录下的文件会在 .R 文件中生成对应的资源 ID,而 assets 不会自动生成对应的 ID,而是通过 AssetManager 类的接口来获取。此外,每当在 res 文件夹下放一个文件时,aapt 就会自动生成对应 id 并保存在 .R 文件中,但 .R 文件仅仅只是保证编译程序不会报错,实际上在应用运行时,系统会根据 ID 寻找对应的资源路径,而 resources.arsc 文件就是用来记录这些 ID 和 资源文件位置对应关系 的文件。
3.2.1 使用 Lint 的 Remove Unused Resource
Refactor -> Remove Unused Resource -> preview 可以预览找到的无用资源
需要注意的,Android Lint 不会分析 assets 文件夹下的资源,因为 assets 文件可以通过文件名直接访问,不需要通过具体的引用,Lint 无法判断资源是否被用到。
3.2.2 shrinkResources
Android shrinkResources 资源压缩功能,它需要配合 ProGurad 的 minifyEnabled 功能同时使用。如果 ProGuard 把部分无用代码移除,这些代码所引用的资源也会被标记为无用资源,然后通过资源压缩功能将它们移除。
1 | android { |
是不是看起来很完美,但是目前的 shrinkResources 实现起来还有几个缺陷。
- 没有处理 resources.arsc 文件。 这样导致大量无用的
String、ID、Attr、Dimen等资源并没有被删除。 - 没有真正删除资源文件。 对于
Drawable、Layout这些无用资源,shrinkResources也没有真正把它们删掉,而是仅仅替换为一个空文件。为什么不能删除呢?主要还是因为resources.arsc里面还有这些文件的路径,具体你可以查看这个 issues 。
如果想解答系统为什么不能直接把这些资源删除,我们需要先回过头来重温一下 Android 的编译流程。
- 由于
Java代码需要用到资源的R.java文件,所以我们就需要把R.java提前准备好。 - 在编译
Java代码过程,已经根据R.java文件,直接将代码中资源的引用替换为常量,例如将R.String.sample替换为0x7f0c0003。 .ap_资源文件的同步编译,例如resources.arsc、XML 文件的处理等。
如果我们在这个过程强行把无用资源文件删除,resources.arsc 和 R.java 文件的资源 ID 都会改变(因为默认都是连续的),这个时候代码中已经替换过的 0x7f0c0003 就会出现资源错乱或者找不到的情况。
因此系统为了避免发生这种情况,采用了折中的方法,并没有二次处理 resources.arsc 文件,只是仅仅把无用的 Drawable 和 Layout 文件替换为空文件。
那怎么样才能真正实现无用资源的删除功能呢?我们可以利用 resources.arsc 中 Public ID 的机制,实现非连续的资源 ID。
简单来说,就是 keep 住保留资源的 ID,保证已经编译完的代码可以正常找到对应的资源。

但是重写 resources.arsc 的方法会比资源混淆更加复杂,我们既要从这个文件中抹去所有的无用资源相关信息,还要 keep 住所有保留资源的 ID,相当于把整个文件都重写了。
正因为异常复杂,所以目前 Android 还没有提供这套方案的完整实现。
深入探索 Android 包体积优化 优化 shrinkResources 流程真正去除无用资源 一文中给出一个方法。
matrix 也实现了相关内容 UnusedAssetsTask
3.2.3 AndResGuard 工具
在我们的安装包中,资源相关的文件具体有下面这几个,它们都是我们需要优化的目标文件。

AndResGuard 工具它主要有两个功能,一个是资源混淆,一个是资源的极限压缩。
资源混淆
资源混淆的思路其实非常简单,就是把资源和文件的名字混淆成短路径:
1
2
3Proguard -> Resource Proguard
R.string.name -> R.string.a
res/drawable/icon -> res/s/a那么这样的实现究竟对哪些资源文件有优化作用呢?
- resources.arsc。因为资源索引文件
resources.arsc需要记录资源文件的名称与路径,使用混淆后的短路径res/s/a,可以减少整个文件的大小。 - metadata 签名文件。签名文件 MF 与 SF 都需要记录所有文件的路径以及它们的哈希值,使用短路径可以减少这两个文件的大小。
- ZIP 文件索引。
ZIP文件格式里面也需要记录每个文件Entry的路径、压缩算法、CRC、文件大小等信息。使用短路径,本身就可以减少记录文件路径的字符串大小。
- resources.arsc。因为资源索引文件
极限压缩
AndResGuard的另外一个优化就是极限压缩,它的极限压缩功能体现在两个方面:更高的压缩率。虽然我们使用的还是
Zip算法,但是利用了7-Zip的大字典优化,APK的整体压缩率可以提升3%左右。压缩更多的文件。
Android编译过程中,下面这些格式的文件会指定不压缩;在AndResGuard中,我们支持针对resources.arsc、PNG、JPG以及GIF等文件的强制压缩。1
2
3
4
5
6
7
8
9/* these formats are already compressed, or don't compress well */
static const char* kNoCompressExt[] = {
".jpg", ".jpeg", ".png", ".gif",
".wav", ".mp2", ".mp3", ".ogg", ".aac",
".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet",
".rtttl", ".imy", ".xmf", ".mp4", ".m4a",
".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2",
".amr", ".awb", ".wma", ".wmv", ".webm", ".mkv"
};
为什么
Android系统会专门选择不去压缩这些文件呢?- 压缩效果并不明显。这些格式的文件大部分本身已经压缩过,重新做
Zip压缩效果并不明显。例如PNG和JPG格式,重新压缩只有3%~5%的收益,并不是十分明显。 - 读取时间与内存的考虑。如果文件是没有压缩的,系统可以利用
mmap的方式直接读取,而不需要一次性解压并放在内存中。
AndResGuard 实践
配置根据 AndResGuard 走就可以
编译在 Gradle -> app -> Tasks -> andresguard-> resguardRelease 可以进行编译打包,结果生成在 app -> build -> outputs -> apk -> release -> AndResGuard_xxx 下。
此外,抖音 Android 团队还开源了针对于海外市场 App Bundle APK 的 AabResGuard 资源混淆工具
在美团的一篇文章《Android App 包瘦身优化实践》 中,也讲到了很多资源优化相关的方法,例如 WebP 和 SVG、R 文件、无用资源、资源混淆以及语言压缩等。
3.3 Native Library 相关
对于 Native Library,传统的优化方法可能就是去除 Debug 信息、使用 c++_shared 这些。那我们还有没有更好的优化方法呢?
3.3.1 Library 压缩
跟 Dex 压缩一样,Library 优化最有效果的方法也是使用 XZ 或者 7-Zip 压缩。
在默认的 lib 目录,我们只需要加载少数启动过程相关的 Library,其他的 Library 我们都在首次启动时解压。对于 Library 格式来说,压缩率同样可以比 Zip 高 30% 左右,效果十分惊人。
Facebook 有一个 So 加载的开源库 SoLoader ,它可以跟这套方案配合使用。和 Dex 压缩一样,压缩方案的主要缺点在于首次启动的时间,毕竟对于低端机来说,多线程的意义并不大,因此我们要在包体积和用户体验之间做好平衡。
3.3.2 Library 合并与裁剪
对于 Native Library,Facebook 中的编译构建工具 Buck 也有两个比较硬核的高科技。当然在官方文档中是完全找不到的,它们都隐藏在 源码 中。
- Library 合并。在
Android 4.3之前,进程加载的Library数量是有限制的。在编译过程,我们可以自动将部分Library合并成一个。具体思路可以参考文章《Android native library merging》 以及 Demo 。 - Library 裁剪。
Buck里面有一个 relinker 的功能,原理就是分析代码中JNI方法以及不同Library的方法调用,找到没有无用的导出symbol,将它们删掉。这样linker在编译的时候也会把对应的无用代码同时删掉,这个方法相当于实现了Library的ProGuard Shrinking功能。
感谢
以及上文中的链接







