用户通常都不愿意去下载一个比较大的程序,特别是不在 WIFI 的情况下。如果你的安装包很小,用户还是愿意下载安装体验下的。现在市面上满足某种需求的 App 通常都会有很多款,如何让用户愿意下载你的 App 来体验?安装包越小,在 WIFI 情况下,极速下载安装,开始体验。在移动网络情况下,包体积越小,用户安装的的可能性越大。所以安装包大小对用户的转换率有很大的影响。接下来就和大家分享下我在实际中工作中对包体积优化的一些经验。
APK 文件结构既然是要优化 Android APK 安装文件的大小,首要需要了解下 APK 文件的结构。将 APK 文件拖进 AndroidStudio 可以清楚的看到 APK 文件组成部分。APK 主要由以下几部分组成:
META-INF/: 该文件夹下主要包含 CERT.SF 和 CERT.RSA 签名文件, 以及 MANIFEST.MF 清单文件 assets/: 该文件夹主要包含 app 中的资产文件,在程序中通过 AssetManager 对象来获取 res/: 该文件夹主要包含没有被编译进 resources.arsc 的文件 lib/: 该文件夹包含一些平台的 so 库, 如 armeabi, armeabi-v7a, arm64-v8a, x86, x86_64, and mips. resources.arsc: 该文件主要存放着编译后的资源。主要存放着 res/values 目录下的文件内容,打包工具会将该目录下的 XML 内容(string、style)提取出来编译成二进制格式。 classes.dex: 该文件主要包含能够被 Dalvik/ART 虚拟机理解的 DEX 格式的 class 文件 AndroidManifest.xml: 该文件主要核心的 Android 清单文件,该文件使用 Android 的二进制 XML 格式。 优化手段其实 APK 最核心的就两个内容,图片资源和代码。所以包体积优化主要是从这两方面入手。例如检查
assets
目录下是否有没有用到的资源。一般来说很少会在 assets 目录放一些没用的资源,主要是集成第三方 SDK (如高德、Baidu地图等)的时候需要放一些资源进去,比如图片、音频文件等。随着项目的迭代,界面 UI 的风格和以前相比发生了很大的变化,那么以前很多图片资源也就不可用了,所以在 res
目录下的可能会存在很多不用的图片,这是我们清理未使用资源最重要的一个文件夹。除了图片,然后就是 classes.dex
文件 了,一般我们自己的程序的业务代码不会对包体积产生很大的影响,主要是使用了大量的第三方库,以及集成公司内部其他团队的一些 module ,可能这些 module 包含了大量我们用不到的代码或者资源。
在优化之前,来看下我所做项目的安装包大小为 73437KB(71.7MB),为后面做的优化好有一个对比,看看具体的优化幅度。
通过 AndroidStudio 移除未使用的资源手动移除资源有两个好处:一个是减少安装包的体积,另一个是减少源代码的体积。
在 AndroidStudio 中有两种方式帮我们找到未使用的资源:
Analyze -> Inspect Code,实际上就是通过 lint 工具帮我们找不用的资源,除了图片资源,还会帮我找到代码中存在的潜在问题,运行效果如下图所示:
双击 shift,输入 Remove Unused Resources,然后回车。由于上面的方式不仅找出未使用到的资源,还会检测代码,所以运行的比较耗时。如果你仅仅只想找出未使用的资源,可以使用双击 shift 的方式,它们检测的结果都是一样的。
上面的工具在使用的过程中有两个坑:
用到的资源,依然报没有引用。如一些 drawable 文件的 xml 资源
它还会移除很多布局中的id,如果项目中使用了 ButterKnife,是通过 R2 来应用 id 的,该工具无法检测这种情况
所以,在针对 drawable 目录下的资源我们可以通过 git 将其 revert,因为我们的 icon 很少会放进 drawable 目录的。对于布局中声明的 id 被移除,我们可以将 layout 文件夹 revert。
通过上面的操作,成功将包体积减少了 2.3M:
操作 | 体积 | 减少 |
---|---|---|
优化前 | 73437KB(71.7MB) | - |
Inspect Code | 71054KB(69.3MB) | 2383KB(2.3M) |
在手动移除未使用的资源的过程中,发现了另一个问题。现在都是模块化工程了,我们项目有几十个 module,很多 module 中尽然包含了系统默认的 ic_launcher 图标,新建 module 默认生成的,而我们项目的图标名字改为了
app_icon
,也就是里面的 ic_launcher 是没有用的。每个 module 下关于 ic_launcher 就 8 个文件夹:
drawable
-> ic_launcher_background.xml
drawable-v24
-> ic_launcher_foreground.xml
mipmap-anydpi-v26
-> ic_launcher.xml
-> ic_launcher_round.xml
mipmap-hdpi
-> ic_launcher.png
-> ic_launcher_round.png
mipmap-mdpi
-> ic_launcher.png
-> ic_launcher_round.png
mipmap-xhdpi
-> ic_launcher.png
-> ic_launcher_round.png
mipmap-xxhdpi
-> ic_launcher.png
-> ic_launcher_round.png
mipmap-xxxhdpi
-> ic_launcher.png
-> ic_launcher_round.png
有的时候,这些 module 可能需要这些 launcher,虽然在发布的时候不需要,但是我们可能需要单独是运行这个组件,一般会有一个 debug manifest 和 release manifest,然后通过一个标记来判断是 library 还是 application。其实也可以用过其他方式来实现这种 debug 和 release 的情况(可以在 module 工程外 套一层工程,该工程包含这个 module,作为可以运行的 application)。通过这种方式,module 就不需要存在 application 的情况,也就不需要 launcher 图标了。
其实这也是开发者非常容易忽略的问题,例如,我们依赖的很多其他部门的内部库,通过 ctrl+shift+r 查找 ic_launcher,会发现很多 aar 会有 launcher 资源。甚至有些不规范的第三方开源库也同样存在这些问题。
操作 | 体积 | 减少 |
---|---|---|
优化前 | 73437KB(71.7MB) | - |
Inspect Code | 71054KB(69.3MB) | 2383KB(2.3M) |
Remove Launcher | 71035KB(69.3MB) | 19KB |
为什么移除了这么多的 launcher 图片,为什么 apk 的大小只是减少了 19KB?(具体哪些地方减少了,可以通过 Compare with previous APK 功能进行对比)。
由于最终生成 APK 的时候,同名文件只会使用一个资源,也即是只会存在一份,所以优化的幅度不大(关于多个 module 相同路径存在相同文件名,打包时会有优先级,大家可以查看官方文档)。但是清理我们项目中一些垃圾资源。
开启 shrink resource其实,在我们工程的 app/build.gradle 中配置了开启 shrink resource 了:
minifyEnabled true
shrinkResources true
我们使用的程序的图标名字使用的不是 ic_launcher,而是 app_icon,我们通过 APK Analyze 分析我们的 APK 发现 ic_launcher 资源还在,ic_launcher 名字的图标上在程序中应该没有被用到,为什么没有被 shrink 呢?有两种可能:
有某个地方隐形用到了 ic_launcher 文件。 shrink 没有生效我们先来项目中的 shrink 有没有生效。 我放一个新的资源(abc.webp)到工程中去,然后重新打包,如果该文件被shrink了说明 shrink 是生效的(也就间接说明了程序中某个地方用到了 ic_launcher),如果没有被 shrink 说明上面的配置没有使得 shrink 生效,想办法让其生效即可。
通过 APK Analyze 打开新生成 APK 文件,发现新加入的
abc.webp
文件依然存在:
如果开启了 shrink resource,当 shrinkMode = safe 时,打包的时候会主动寻找那些可能被引用的资源,如通过 resources.getIdentifier() 方式获取资源,该资源不会被缩减,当 shrinkMode = strict 严格模式时该资源不会被缩减。
我在做实验的时候发现,如果一个资源被 shrink 了,它可能还在 APK 中,只不过该资源的体积变得非常小。
如果你将 shrinkMode 设置为 safe,那么可能没有被用到的也被保留了,因为检测可能没有那么精准。
你可以将 shrinkMode 设置为 strict,这个时候需要将通过 resources.getIdentifier(A)方式获取的资源 keep 起来。可以在 keep.xml 中配置要保留的文件:
更多关于混淆相关的知识,可以查看 AndroidAll
png 转成 webpAndroid4.0 开始支持 webp,但是只有在 Android4.3 才支持透明度、无损 webp。所以如果你的 app 最低支持 4.3 的话,可以使用 webp 代替 png。
在 AndroidStudio 中支持一键转化,可以选择转码的质量比,还可以选择如果转成的 webp 反而比原来的 png 还要大,可以跳过。
操作 | 体积 | 减少 |
---|---|---|
优化前 | 73437KB(71.7MB) | - |
Inspect Code | 71054KB(69.3MB) | 2383KB(2.3M) |
Remove Launcher | 71035KB(69.3MB) | 19KB |
ShrinkResources | 67576KB(65.9MB) | 3459KB(3.37M) |
Png2webp | 64505KB(62.9M) | 3071KB(3M) |
由于之前 R8 还不是很稳定,所以我们将其关闭了。现在都 AndroidStudio 3.6 了,我们将其打开:
android.enableR8=true
虽然官网上说 R8 支持现有 ProGuard 规则文件,但是在实际使用的时候还是会有些问题,解决一些混淆配置上的问题,重新打一个 release 包,发现减少了 0.9M:
操作 | 体积 | 减少 |
---|---|---|
优化前 | 73437KB(71.7MB) | - |
Inspect Code | 71054KB(69.3MB) | 2383KB(2.3M) |
Remove Launcher | 71035KB(69.3MB) | 19KB |
ShrinkResources | 67576KB(65.9MB) | 3459KB(3.37M) |
Png2webp | 64505KB(62.9M) | 3071KB(3M) |
R8 | 63506KB(62M) | 999KB(0.97M) |
上面是 R8 的普通模式,R8 还有完全模式,还会做一些额外的优化操作,R8 开启完全模式,但是目前还是实验性质的:
android.enableR8.fullMode=true
重新打一个 release 包,发现减少了 0.16M:
操作 | 体积 | 减少 |
---|---|---|
优化前 | 73437KB(71.7MB) | - |
Inspect Code | 71054KB(69.3MB) | 2383KB(2.3M) |
Remove Launcher | 71035KB(69.3MB) | 19KB |
ShrinkResources | 67576KB(65.9MB) | 3459KB(3.37M) |
Png2webp | 64505KB(62.9M) | 3071KB(3M) |
R8 | 63506KB(62M) | 999KB(0.97M) |
R8 FullMode | 63333KB(61.8M) | 173KB(0.16M) |
我们还可以通过自定义 View 来代替一些状态图标,比如订单状态、退款状态等。如下所示:
类似这些图标都是可以使用自定义 View 来完成,可以减少大量的图片资源。如果状态很多,就会需要很多的状态图标,如果支持国际化的话,还需要为每个国家生成对应的状态图标。
经过自定义 View 替换状态图标后,包体积减少了 0.366M:
操作 | 体积 | 减少 |
---|---|---|
优化前 | 73437KB(71.7MB) | - |
Inspect Code | 71054KB(69.3MB) | 2383KB(2.3M) |
Remove Launcher | 71035KB(69.3MB) | 19KB |
ShrinkResources | 67576KB(65.9MB) | 3459KB(3.37M) |
Png2webp | 64505KB(62.9M) | 3071KB(3M) |
R8 | 63506KB(62M) | 999KB(0.97M) |
R8 FullMode | 63333KB(61.8M) | 173KB(0.16M) |
CustomView | 62958KB(61.4M) | 173KB(0.36M) |
微信使用的 AndResGuard 可以对资源资源路径以及资源名字进行混淆,资源名字全部改成类似 abc 的样子。可以大大减少名字字符占用的空间大小。
特别是模块化后,为了防止资源重名,我们都会在资源的加上模块前缀,这样导致资源的名称就更长了。使用 AndResGuard 的时,程序中通过 getIdentifier 方式获取资源,一定要加入白名单,这个可以在程序中全局查找。
通过 AndResGuard 混淆后,包体积减少了 3.54M:
操作 | 体积 | 减少 |
---|---|---|
优化前 | 73437KB(71.7MB) | - |
Inspect Code | 71054KB(69.3MB) | 2383KB(2.3M) |
Remove Launcher | 71035KB(69.3MB) | 19KB |
ShrinkResources | 67576KB(65.9MB) | 3459KB(3.37M) |
Png2webp | 64505KB(62.9M) | 3071KB(3M) |
R8 | 63506KB(62M) | 999KB(0.97M) |
R8 FullMode | 63333KB(61.8M) | 173KB(0.16M) |
CustomView | 62958KB(61.4M) | 173KB(0.36M) |
AndResGuard | 59323KB(57.9M) | 3635KB(3.54M) |
在主流的手机CPU架构都是 ARM,基本上只要支持这一种架构就可以了。更多关于这方面的知识可以查看 Android NDK ~ 基础入门指南
我们来看下市面上主流的 app 支付宝和微信的 CPU 架构:
armeabi-v7a 是向下兼容 armeabi,arm64-v8a 能兼容 armeabi-v7a 和 armeabi
我们项目中也是只支持一种 armeabi-v7a 架构,减少 so 文件体积大小
release {
ndk {
abiFilters 'armeabi-v7a'
}
//...
}
小结
到此,就介绍完了我这次包体积优化相关内容了,差不多了减少了 20% 的包体积大小。当然优化是无止尽的,除了上面的一些优化手段还有 app Bundles 的方式(需要结合 Google Play 一起);还可以考虑通过 BackgroundLibrary 替换程序中大量的 shape、selector 文件,减少包体积,但是该库对性能有一定的影响,所以我还没有使用,后面可以考虑是否还有更好的方案;还可以找出程序中重复的图片(图片内容一致,名字不同);当然还有插件化,插件也需要瘦身,减少下发消耗的流量。
另外本文涉及到的代码都在我的 AndroidAll GitHub 仓库中。该仓库除了
性能优化
,还有 Android 程序员需要掌握的技术栈,如:程序架构、设计模式、性能优化、数据结构算法、Kotlin、Flutter、NDK,以及常用开源框架 Router、RxJava、Glide、LeakCanary、Dagger2、Retrofit、OkHttp、ButterKnife、Router 的原理分析 等,持续更新,欢迎 star。
作者:Chiclaim