阿里某淘Android体积优化方案(上)

1,009 阅读6分钟

背景

随着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-optimizeproguard-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 文件拥有相同或者是更好的运行时性能; image.png

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.13.0.1引入 D8
3.23.2.0引入 R8、D8 脱糖默认开启
3.43.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-inlinebytex/shrink-r-plugin

1.4 科学删除无用代码

lint

步骤:点击菜单栏 Analyze -> Run Inspection by Name -> unused declaration -> Moudule ‘app’

我们通过lint扫描无用代码,在删除代码的时候风险较高,因为业务复杂,可能涉及到反射的不确定性和jar包依赖无法搜索问题,那我们需要建立一个机制降低删除代码的风险。
对于部分下线业务,代码引用链依旧存在,lint无法扫码,但是可以通过代码覆盖率发现。

代码覆盖率

删除代码没办法和其他优化一样可以通过AB切流,对于上述lint工具存在部分缺陷,所以我们想建立代码覆盖率的平台,统计运行时类的使用情况,针对这个痛点,我们实现代码覆盖率统计,并通过代码覆盖率实现模块责任制,对于lib module多个版本覆盖率低的问题将推进业务方解决。

image.png

Android代码覆盖率分为测试和线上两种模式

  • 线下模式
    线下模式是通过jacoco针对MR的覆盖率。其一主要是看测试场景覆盖、第二是避免无用代码合入。
  • 线上模式
    线上模式统计的是全域的类文件,主要是通过类静态代码块插桩的方式统计类加载。插装的方式会导致apk文件增加,预计dex文件增大1%。
    这里针对统计做部分流程优化,将类通过int值映射,打包的时候生成mapping文件,降低运行时内存占用和上传流量。

image.png

1.5 ByteX插件代码优化

总结

本章主要介绍Dex大小优化,主要从基础工具和无用代码两种方式入手,基础工具接入成本和风险都极低,删除无用代码也从本地和线上多种方式介绍了科学的方法,保障线上稳定性。

阿里某淘Android体积优化方案(下)

如果能帮助到你 帮忙点个star Github掘金