背景
随着Android移动开发的需求越来越复杂,我们不可避免apk越来越臃肿,体积越来越大。
作为一个非心智成熟型App,Apk大小影响拉新用户转化率,拉新用户面对的是真金白银,配合用增团队在业务快速发展期快速增长是我们面对的核心需求。
同时谷歌官方也给出了一个很详细的数据,包体大小每上升 6MB,应用下载转化率就会下降 1%。
apk的组成
lib/ 存放so文件,现阶段市面上有armeabi、armeabi-v7a、arm64-v8a、x86、x86_64几种cpu架构。
res/ 存放编译后的资源文件,例如:drawable、layout等等
assets/ 应用程序的资源
META-INF/ 该文件夹一般存放于已经签名的APK中,它包含了APK中所有文件的签名摘要等信息
classes(n).dex classes文件是Java Class,被DEX编译后可供Dalvik/ART虚拟机所理解的文件格式
resources.arsc 编译后的二进制资源映射文件表。
AndroidManifest.xml Android的清单文件,格式为XML,用于描述应用程序的名称、版本、所需权限、注册的四大组件
1 Dex优化
Dex大小优化核心关注点有两个
- 删除无用的代码。这个方式侵入成本较高,需要建立科学和安全的方案,是一个长期治理的过程。
- 利用编译工具实现通用的优化方案。
1.1 Proguard
在Android构建流程中的一个工具 — Proguard,它是一个免费的 Java 类文件 压缩、优化、混淆、预先校验 的工具。它的 主要作用 大概可以概括为 两点,如下所示:
- 1)、瘦身:它可以检测并移除未使用到的类、方法、字段以及指令、冗余代码,并能够对字节码进行深度优化。最后,它还会将类中的字段、方法、类的名称改成简短名字。
- 2)、安全:增加代码被反编译的难度,一定程度上保证代码的安全。
Android默认混淆提供proguard-android-optimize
和proguard-android
两种方式的混淆规则,其中proguard-android-optimize
开启了optimize
选项,在混淆上更加激进,开启后混淆效果收益提升5%-10%。
proguard 字段
- assumenosideeffects 移除相关的调用方法
-assumenosideeffects class android.util.Log {
public static boolean isLoggable(java.lang.String, int);
public static int d(...);
public static int w(...);
public static int v(...);
public static int i(...);
}
1.2 D8 与 R8 优化
D8
编译器特点是:
- 编译更快、时间更短;
DEX
编译时占用内容更小;.dex
文件大小更小;D8
编译的.dex
文件拥有相同或者是更好的运行时性能;
ProGuard和R8都应用了基本名称混淆:它们都使用简短,无意义的名称重命名类,字段和方法。他们还可以删除调试属性。但是,R8 在 inline 内联容器类中更有效,并且在删除未使用的类,字段和方法上则更具侵略性。例如,R8本身集成在 ProGuard V6.1.1 版本中,在压缩apk的大小方面,与ProGuard的8.5% 相比,使用 R8 apk尺寸减小了约10%。并且,随着Kotlin现在成为Android的第一语言,R8进行了ProGuard尚未提供的一些Kotlin的特定的优化。
开启D8和R8后,dex的大小能缩减5%左右 在高版本的AGP插件中已经默认开启D8和R8
Android Studio 版本 | Android Gradle Plugin 版本 | 变更 |
---|---|---|
3.1 | 3.0.1 | 引入 D8 |
3.2 | 3.2.0 | 引入 R8、D8 脱糖默认开启 |
3.4 | 3.4.0 | 默认开启 R8 |
1.3 R文件内联
Android在构建过程中会根据资源生成R文件,里面包含了资源索引,使用该索引可以在最终生成的resources.arsc资源映射表中找到对应资源,对于开发者来说在代码中引用资源很方便。
在library工程中引用的R资源索引不是final的,所以我们在Library工程不能在switch - case。由于引用的资源不是final的,所以library的产物aar中包含的class中使用的资源索引还是会以包名存在。
在App工程中构建时会将依赖的AAR资源进行合并,根据合并的结果生成最终的R资源索引,这时的资源索引已经确定,所以全部是final的,java编译器在编译时会将final常量进行inline内联操作,也就是App工程中的java源码编译后的class中使用的R资源索引全部会替换为常量值。
由于library中的R文件不是final类型,所以没有优化,导致文件冗余,我们可以编译时全局将将int id 值直接改到代码中,library中的R文件类就可以删除。
开源方案:r-inline 、 bytex/shrink-r-plugin
1.4 科学删除无用代码
lint
步骤:点击菜单栏 Analyze -> Run Inspection by Name -> unused declaration -> Moudule ‘app’
我们通过lint扫描无用代码,在删除代码的时候风险较高,因为业务复杂,可能涉及到反射的不确定性和jar包依赖无法搜索问题,那我们需要建立一个机制降低删除代码的风险。
对于部分下线业务,代码引用链依旧存在,lint无法扫码,但是可以通过代码覆盖率发现。
代码覆盖率
删除代码没办法和其他优化一样可以通过AB切流,对于上述lint工具存在部分缺陷,所以我们想建立代码覆盖率的平台,统计运行时类的使用情况,针对这个痛点,我们实现代码覆盖率统计,并通过代码覆盖率实现模块责任制,对于lib module多个版本覆盖率低的问题将推进业务方解决。
Android代码覆盖率分为测试和线上两种模式
- 线下模式
线下模式是通过jacoco针对MR的覆盖率。其一主要是看测试场景覆盖、第二是避免无用代码合入。 - 线上模式
线上模式统计的是全域的类文件,主要是通过类静态代码块插桩的方式统计类加载。插装的方式会导致apk文件增加,预计dex文件增大1%。
这里针对统计做部分流程优化,将类通过int值映射,打包的时候生成mapping文件,降低运行时内存占用和上传流量。
1.5 ByteX插件代码优化
- 1、编译期间 内联常量字段:const_inline。
- 2、编译期间 移除多余赋值代码:field_assign_opt。
- 3、编译期间 移除 Log 代码:method_call_opt。
- 4、编译期间 内联 Get / Set 方法:getter-setter-inline-plugin。
总结
本章主要介绍Dex大小优化,主要从基础工具和无用代码两种方式入手,基础工具接入成本和风险都极低,删除无用代码也从本地和线上多种方式介绍了科学的方法,保障线上稳定性。