Google 2016 年公布的研究报告显示,包体积每上升 6MB 就会带来下载转化率降低 1%,当包体积增大到 100MB 时就会有断崖式的下跌。对于应用商店,普遍有一个流量自动下载的最大阈值,如应用宝,下载的app超过100M,用流量下载时,会 弹窗 提示用户是否继续下载,这对下载转化率影响是比较大的。
虽然时间已经过去了7年,手机的内存已经从16g/32g扩展到256g/512g甚至1T,流量也不再成为制约我们下载大体积apk的因素,但是包体积或直接或间接地影响着下载转化率、安装时间、运行内存、磁盘空间等重要指标,所以投入精力扫除积弊、发掘更深层次的体积优化项是十分有必要的。
Android APK结构
APK主要由三个部分组成,分别是:
- 代码相关: 我们在项目中所编写的 java 文件,经过编译之后会生成一个 .class 文件,而这些所有的 .class 文件呢,它最终会经过 dx 工具编译生成一个 classes.dex, 当超过65525方法数后会打包成多个Dex文件。
- 资源相关: res、assets、编译后的二进制资源文件 resources.arsc 和 清单文件 等等。res 和 assets 的不同在于 res 目录下的文件会在 .R 文件中生成对应的资源 ID,而 assets 不会自动生成对应的 ID,而是通过 AssetManager 类的接口来获取。此外,每当在 res 文件夹下放一个文件时,aapt 就会自动生成对应的 id 并保存在 .R 文件中,但 .R 文件仅仅只是保证编译程序不会报错,实际上在应用运行时,系统会根据 ID 寻找对应的资源路径,而 resources.arsc 文件就是用来记录这些 ID 和 资源文件位置对应关系 的文件。
- so相关:lib目录下的文件,存放so库
- META-INF: APK签名有关的信息
APK 解析
以我们CSD设置为例,解析APK内部如下图所示:
浅分析一波包的内容:
成分 | 体积 | 备注 |
---|---|---|
assets文件夹 | 418.4MB | |
res | 35.4MB | |
lib | 76.6MB | |
dex | 26.4MB | |
resources.arsc | 1.7MB | 资源映射问题 |
其他 | 几十K或者更小 | 忽略不计 |
根据上午的分析,我们体积优化的重点应该放在 assets目录,res目录,lib目录和dex文件中。
APK 体积优化方法
针对上述APK的体积情况我们可以按照几个部分进行优化
资源优化
- 无用资源文件清理
可能在版本迭代过程中,UI的样式经过几次更新,但之前存放在项目中的资源图片没有删除,可以通过Lint检查出未使用的资源文件,然后进行删除。
- 重复资源优化
在大型App的开发过程中,可能涉及到多个团队和多个人员进行开发,每个人员在提交代码时可能会导致资源名称相同,导致了资源文件覆盖,然后根据模块名称添加前缀,解决覆盖的问题,可能导致了相同的资源文件导入了多分,从而增大了应用的体积。这时需要将公共资源提取出来并去重。
- 图片压缩,转webp
图片压缩可以使用TinyPng,AndroidStudio中也可以直接使用,官方术语就是:
使用智能的无损压缩技术来减少图片文件的大小,通过智能的选择颜色的数量,减少存储的字节,但是效果基本是和压缩前一样的。
- 图片着色器
相同的图片只是颜色不同的话,完全可以只放一个图片,可以在xml中操作或者在内存里操作Drawable,完成颜色替换。
<ImageView
android:id="@+id/iv_select_music"
android:layout_width="12dp"
android:layout_height="12dp"
android:src="@drawable/ic_select_course_subject_arr"
app:layout_constraintBottom_toBottomOf="@+id/tv_select_music_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/tv_select_music_text"
app:tint="@color/c_line_gray" />
val drawableRes = ResourcesCompat.getDrawable(
resources,
resId, null
) ?: return
val tintIcon = DrawableCompat.wrap(drawableRes)
DrawableCompat.setTint(tintIcon, hintColor)
setImageDrawable(tintIcon)
- 图片动态下发
如果本地有大图,且使用要求为未压缩,或者压缩之后仍然很大,可以适当的选择动态下载该图。
so库优化
针对不同的平台我们可以分架构打包
- 架构 配置
只配置armeabi-v7a
android {
defaultConfig {
ndk {
abiFilters "armeabi-v7a"
}
}
}
通过productFlavors配置动态包
flavorDimensions "default" productFlavors {
arm32{
dimension "default"
ndk {
abiFilters "armeabi-v7a"}
}
arm64{
dimension "default"
ndk {
abiFilters "armeabi-v8a"}
}}
}
- 分 架构 打包
分架构打包能减少libs文件夹体积,libs文件夹里会包含不同架构的 so 库集合。
splits {
// 基于不同的abi架构配置不同的apk
abi {
// 必须为true,打包才会为不同的abi生成不同的apk
enable true
// 默认情况下,包含了所有的ABI。
// 所以使用reset()清空所有的ABI,再使用include指定我们想要生成的架构armeabi-v7a、arm-v8a
reset()
// 逗号分隔列表的形式指定 Gradle 应针对哪些 ABI 生成 APK。只与 reset() 结合使用,以指定确切的 ABI 列表。
include "armeabi-v7a", "arm64-v8a"
// 是否生成通用的apk,也就是包含所有ABI的apk。如果设为 true,那么除了按 ABI 生成的 APK 之外,Gradle 还会生成一个通用 APK。
universalApk true
}
}
- so压缩
分架构打包是减少so的数量,so压缩是减少so的单个体积。
android:extractNativeLibs="true"
android:extractNativeLibs = true时,gradle打包时会对工程中的so库进行压缩,最终生成apk包的体积会减小。
但用户在手机端进行apk安装时,系统会对压缩后的so库进行解压,从而造成用户安装apk的时间变长。
代码优化
- 类优化,方法优化
通过Lint扫描,删除没有用到的类和方法,从而减小Dex的体积。
- 使用ProGuard
混淆器的 作用 不仅仅是 保护 代码,它也有 精简编译后程序大小 的作用,其 通过缩短变量和函数名以及丢失部分无用信息等方式,能使得应用包体积减小。
- Dex分包优化
当我们的 APK 过大时,Dex 的方法数就会超过65536个,因此,必须采用 mutildex 进行分包,但是此时每一个 Dex 可能会调用到其它 Dex 中的方法,这种 跨 Dex 调用的方式会造成许多冗余信息,具体有如下两点:
- 1)、多余的 method id :跨 Dex 调用会导致当前dex保留被调用dex中的方法id,这种冗余会导致每一个dex中可以存放的class变少,最终又会导致编译出来的dex数量增多,而dex数据的增加又会进一步加重这个问题。
- 2)、其它跨dex调用造成的信息冗余:除了需要多记录被调用的method id 之外,还需多记录其所属类和当前方法的定义信息,这会造成 string_ids、type_ids、proto_ids 这几部分信息的冗余。
为了减少跨 Dex 调用的情况,我们必须 尽量将有调用关系的类和方法分配到同一个 Dex 中。但是各个类相互之间的调用关系是非常复杂的,所以很难做到最优的情况。所幸的是,ReDex 的 CrossDexDefMinimizer 类分析了类之间的调用关系,并 使用了 贪心算法 去计算局部的最优解(编译效果和dex优化效果之间的某一个平衡点)。使用 "InterDexPass" 配置项 可以把互相引用的类尽量放在同个 Dex,增加类的 pre-verify,以此提升应用的冷启动速度。