如果我们当前使用的是 Android Studio 3.4 或 Android Gradle 插件 3.4.0 及其更高版本,R8 会作为默认编译器。否则,我们 必须要在 gradle.properties 中配置如下代码让 App 的混淆去支持 R8,如下所示:
android.enableR8=true
android.enableR8.libraries=true
那么,R8 与混淆相比优势在哪里呢?
-
ProGuard 和 R8 都应用了基本名称混淆:它们 都使用简短,无意义的名称重命名类,字段和方法。他们还可以 删除调试属性。
但是,R8 在 inline 内联容器类中更有效,并且在删除未使用的类,字段和方法上则更具侵略性。例如,R8 本身集成在 ProGuard V6.1.1 版本中,在压缩 apk 的大小方面,与 ProGuard 的 8.5% 相比,使用 R8 apk 尺寸减小了约 10%。并且,随着 Kotlin 现在成为 Android 的第一语言,R8 进行了 ProGuard 尚未提供的一些 Kotlin 的特定的优化。 -
从表面上看,ProGuard 和 R8 非常相似。它们都使用相同的配置,因此在它们之间进行切换很容易。放大来看的话,它们之间也存在一些差异。R8 能更好地内联容器类,从而避免了对象分配。但是 ProGuard 也有其自身的优势,具体有如下几点:
1)、ProGuard 在将枚举类型简化为原始整数方面会更加强大。它还传递常量方法参数,这通常对于使用应用程序的特定设置调用的通用库很有用。
ProGuard 的多次优化遍历通常可以产生一系列优化。例如,第一遍可以传递一个常量方法参数,以便下一遍可以删除该参数并进一步传递该值。删除日志代码时,多次传递的效果尤其明显。ProGuard 在删除所有跟踪(包括组成日志消息的字符串操作)方面更有效。
2)、ProGuard 中应用的模式匹配算法可以识别和替换短指令序列,从而提高代码效率并为更多优化打开了机会。在优化遍历的顺序中,尤其是数学运算和字符串运算可从中受益。
3、最后,ProGuard 具有独特的能力来优化使用 GSON 库将对象序列化或反序列化为 JSON 的代码。该库严重依赖反射,这很方便,但效率低下。而 ProGuard 的优化功能可以 通过更高效,直接的访问方式 来代替它。
R8 优化实战
接下来,我们就来看看 Awesome-WanAndroid 使用 R8 后,APK 体积的变化,如下图所示:
-
可以看到,相较于仅使用混淆后的 APK 而言,大小减少了 0.1MB,Dex 部分的优化效果大概为 5%,APK 整体的压缩效果也有 1.5% 左右。
-
虽然从减少的 APK 大小来看,0.1MB 很少,但是比例并不小,如果你负责的是一个像微信、淘宝等规模的 App,它们的体积一般都将近 100MB,使用 R8 后也能减小 1.5MB 的大小。
-
D8 与 R8 的作用非常强大,而 Jake Wharton 大神最近一年多也在研究 D8 与 R8 的知识,如果想对 D8 与 R8 的实现细节有更多地了解,可以看看他的 个人博客。
4、去除 debug 信息与行号信息
在讲解什么是 deubg 信息与行号信息之前,我们需要先了解 Dex 的一些知识。
我们都知道,JVM 运行时加载的是 .class 文件,而 Android 为了使包大小更加紧凑、运行时更加高效就发明了 Dalvik 和 ART 虚拟机,两种虚拟机运行的都是 .dex 文件,当然 ART 虚拟机还可以同时运行 oat 文件。
所以 Dex 文件里的信息内容和 Class 文件包含的信息是一样的,不同的是 Dex 文件对 Class 中的信息做了去重,一个 Dex 包含了很多的 Class 文件,并且在结构上有比较大的差异,Class 是流式的结构,Dex 是分区结构,Dex 内部的各个区块间通过 offset 来进行索引。
为了在应用出现问题时,我们能在调试的时候去显示相应的调试信息或者上报 crash 或者主动获取调用堆栈的时候能通过 debugItem 来获取对应的行号,我们都会在混淆配置中加上下面的规则:
-keepattributes SourceFile, LineNumberTable
这样就会保留 Dex 中的 debug 与行号信息,此时的 Dex 结构图 如下所示:
大牛耗时一年:深入探索 Android 包体积优化,共三万字建议收藏上
从图中可以看到,Dex 文件的结构主要分为 四大块:header 区,索引区,data 区,map 区。而我们的 debug 与行号信息就保存在 data 区中的 debugItems 区域。
而 debug_items 里面主要包含了 两种信息,如下所示:
调试的信息:包含函数的参数和所有的局部变量。
排查问题的信息:包含所有的指令集行号与源文件行号的对应关系。
根据 Google 官方的数据,debugItem 一般占 Dex 的比例有 5% 左右,如果我们能去除 debug 与行号信息,就能更进一步对 Dex 进行瘦身,但是会失去调试信息的功能,那么,有什么方式可以去掉 debugItem,同时又能让 crash 上报的时候能拿到正确的行号呢?
我们可以尝试直接修改 Dex 文件,保留一小块 debugItem,让系统查找行号的时候指令集行号和源文件行号保持一致,这样任何监控上报的行号都直接变成了指令集行号。
每一个方法都会有一个 debugInfoItem,每一个 debuginfoItem 里面都有一个指令集行号和源文件行号的映射关系,这了我们直接把多余的 debugInfoItem 全部删掉,只保留了一个 debugInfoItem,这样所有的方法都会指向同一个 debugInfoItem,并且这个 debugInfoItem 中的指令集行号和源文件行号保持一致,这样不管用什么方式来查找行号,拿到的都是指令集行号。
需要注意的是,采用这种方案 需要兼容所有虚拟机的查找方式,因此 仅仅保留一个 debugInfoItem 是不够的,需要对 debugInfoItem 进行分区,并且 debugInfoItem 表不能太大。
关于如何去除 Dex 中的 Debug 信息是通过 ReDex 的 StripDebugInfoPass 来完成的,其配置如下所示:
{
“redex” : {
“passes” : [
“StripDebugInfoPass”,
“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
},
“RegAllocPass” : {
“live_range_splitting”: false
}
}
关于 debuginfo 的实战我们下面马上会开始,在此之前,我们先讲讲 Dex 分包中的另一个优化点。
5、Dex 分包优化
Dex 分包优化原理
当我们的 APK 过大时,Dex 的方法数就会超过65536个,因此,必须采用 mutildex 进行分包,但是此时每一个 Dex 可能会调用到其它 Dex 中的方法,这种 跨 Dex 调用的方式会造成许多冗余信息,具体有如下两点:
多余的 method id:跨 Dex 调用会导致当前dex保留被调用dex中的方法id,这种冗余会导致每一个dex中可以存放的class变少,最终又会导致编译出来的dex数量增多,而dex数据的增加又会进一步加重这个问题。
其它跨dex调用造成的信息冗余:除了需要多记录被调用的method id之外,还需多记录其所属类和当前方法的定义信息,这会造成 string_ids、type_ids、proto_ids 这几部分信息的冗余。
为了减少跨 Dex 调用的情况,我们必须 尽量将有调用关系的类和方法分配到同一个 Dex 中。但是各个类相互之间的调用关系是非常复杂的,所以很难做到最优的情况。
所幸的是,ReDex 的 CrossDexDefMinimizer 类分析了类之间的调用关系,并 使用了贪心算法去计算局部的最优解(编译效果和dex优化效果之间的某一个平衡点)。
https://github.com/facebook/redex/blob/master/opt/interdex/CrossDexRefMinimizer.cpp
使用 “InterDexPass” 配置项可以把互相引用的类尽量放在同个 Dex,增加类的 pre-verify,以此提升应用的冷启动速度。
在 ReDex 中使用 Dex 分包优化跨 dex 调用造成的信息冗余的配置代码如下所示:
{
“redex” : {
“passes” : [
“InterDexPass”,
“RegAllocPass”
]
},
“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”
}
为了衡量优化效果,我们可以使用 Dex 信息有效率 这个指标,公式如下所示:
git clone https://github.com/facebook/redex.git
cd redex
如果 Dex 有效率在 80% 以上,就说明基本合格了。
使用 ReDex 进行分包优化、去除 debug 信息及行号信息
下面,我们就使用 Redex 来对上一步生成的 app-release-proguardwithr8.apk 进行进一步的优化。(macOS 环境下)
https://fbredex.com/docs/installation
1、首先,我们需要输入一下命令去去安装 Xcode 命令行工具
xcode-select --install
2、然后,使用 homebrew 安装 redex 项目使用到的依赖库
ANDROID_SDK=/Users/quchao/Library/Android/sdk redex --sign -s wan-android-key.jks -a wanandroid -p wanandroid -c ~/Desktop/interdex_stripdebuginfo.config -P app/proguard-rules.pro -o ~/Desktop/app-release-proguardwithr8-stripdebuginfo-interdex.apk ~/Desktop/app-release-proguardwithr8.apk
需要注意的是吗,2020年2月10号版本源码的 redex 需要的 boost 版本为 V1.71 及以上,当你使用 brew install boost 安装 boost 时可能获取到的 boost 版本会低于 V1.71,此时可能是 brew 版本需要更新,使用 brew upgrade 去更新 brew 仓库的版本 或者可以直接从 boost 官网下载最新的 boost 源码 至 /usr/local/Cellar/ 目录下,我当前使用的是 boost V1.7.2源码下载地址 中的 boost_1_72_0.zip。
https://dl.bintray.com/boostorg/release/1.72.0/source/
从 深入探索 Android 启动优化 时就提及到了 Redex 的类重排优化,当时卡在这一步,所以一直没法真正完成类的重排优化。
3、接着,从 Github 上获取 ReDex 的源码并切换到 redex 目录下
git clone https://github.com/facebook/redex.git
cd redex
4、下一步,使用 autoconf 和 make 去构建 ReDex
如果你使用的是 gcc, 请使用 gcc-5
autoreconf -ivf && ./configure && make -j4
sudo make install
5、然后,配置 Redex 的 config 代码
在 Redex 在运行的时候,它是根据 redex/config/default.config 这个配置文件中的通道 passes 中添加不同的优化项来对 APK 的 Dex 进行处理的,我们可以参考 redex/config/default.config 这个默认的配置,里面的 passes 中不同的配置项都有特定的优化。
为了优化 App 的包体积,我们再加上 interdex_stripdebuginfo.config 中的配置项去删除 debugInfo 和减少跨 Dex 调用的情况,最终的 interdex_stripdebuginfo.config 配置代码 如下所示:
{
“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”
}
6、最后,执行相应的 redex 优化命令
这里我们使用 Redex 命令对上一 Dex 优化中得到的 app_release-proguardwithr8.apk 进行 Dex 分包优化和去除 debugInfo,它使用了贪心这种局部最优解的方式去减少跨 Dex 调用造成的信息冗余,命令如下所示(注意,在 redex 的前面可能需要加上 Android sdk 的路径,因为 redex 中使用到了sdk下的zipalign工具):
ANDROID_SDK=/Users/quchao/Library/Android/sdk redex --sign -s wan-android-key.jks -a wanandroid -p wanandroid -c ~/Desktop/interdex_stripdebuginfo.config -P app/proguard-rules.pro -o ~/Desktop/app-release-proguardwithr8-stripdebuginfo-interdex.apk ~/Desktop/app-release-proguardwithr8.apk
上述 redex 命令的 关键参数含义 如下所示:
–sign:对生成的apk进行签名。
-s:配置应用的签名文件。
-a: 配置应用签名的 key_alias。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
最后
总而言之,Android开发行业变化太快,作为技术人员就要保持终生学习的态度,让学习力成为核心竞争力,所谓“活到老学到老”只有不断的学习,不断的提升自己,才能跟紧行业的步伐,才能不被时代所淘汰。
在这里我分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司19年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。需要的朋友可以私信我【资料】或者 点这里 免费领取
还有高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。 领取地址: Android学习PDF+架构视频+最新面试文档+源码笔记
片转存中…(img-OWFuA2gs-1710693502488)]
[外链图片转存中…(img-VxnJr0lm-1710693502489)]
还有高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。 领取地址: Android学习PDF+架构视频+最新面试文档+源码笔记