Android 性能优化 ~ 包体积优化实战

概述

用户通常都不愿意去下载一个比较大的程序,特别是不在 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 工具帮我们找不用的资源,除了图片资源,还会帮我找到代码中存在的潜在问题,运行效果如下图所示:

    Inspect Code

  • 双击 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 Code71054KB(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 Code71054KB(69.3MB)2383KB(2.3M)
Remove Launcher71035KB(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 文件依然存在:

abc.webp

说明 shrink 没有生效。明明已经配置了 minifyEnabled、shrinkResources,为什么没有生效呢。

经过一番查找,原来是在 proguard 文件中设置了不要 shrink :

-dontshrink

把这行注释,然后重新打包,发现减少了 3.37MB

操作体积减少
优化前73437KB(71.7MB)-
Inspect Code71054KB(69.3MB)2383KB(2.3M)
Remove Launcher71035KB(69.3MB)19KB
ShrinkResources67576KB(65.9MB)3459KB(3.37M)

shrinkMode 主要有两种:safe、strict,默认模式为 safe。

可以在 res/raw/keep.xml 文件中配置 shrinkMode:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:shrinkMode="safe"/>

如果开启了 shrink resource,当 shrinkMode = safe 时,打包的时候会主动寻找那些可能被引用的资源,如通过 resources.getIdentifier() 方式获取资源,该资源不会被缩减,当 shrinkMode = strict 严格模式时该资源不会被缩减。

我在做实验的时候发现,如果一个资源被 shrink 了,它可能还在 APK 中,只不过该资源的体积变得非常小。

如果你将 shrinkMode 设置为 safe,那么可能没有被用到的也被保留了,因为检测可能没有那么精准。

你可以将 shrinkMode 设置为 strict,这个时候需要将通过 resources.getIdentifier(A)方式获取的资源 keep 起来。可以在 keep.xml 中配置要保留的文件:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:shrinkMode="strict"
	tools:keep="@drawable/ic_get_by_identifier"/>

更多关于混淆相关的知识,可以查看 AndroidAll

png 转成 webp

Android4.0 开始支持 webp,但是只有在 Android4.3 才支持透明度、无损 webp。所以如果你的 app 最低支持 4.3 的话,可以使用 webp 代替 png。

在 AndroidStudio 中支持一键转化,可以选择转码的质量比,还可以选择如果转成的 webp 反而比原来的 png 还要大,可以跳过。

操作体积减少
优化前73437KB(71.7MB)-
Inspect Code71054KB(69.3MB)2383KB(2.3M)
Remove Launcher71035KB(69.3MB)19KB
ShrinkResources67576KB(65.9MB)3459KB(3.37M)
Png2webp64505KB(62.9M)3071KB(3M)

Enable R8

由于之前 R8 还不是很稳定,所以我们将其关闭了。现在都 AndroidStudio 3.6 了,我们将其打开:

android.enableR8=true

虽然官网上说 R8 支持现有 ProGuard 规则文件,但是在实际使用的时候还是会有些问题,解决一些混淆配置上的问题,重新打一个 release 包,发现减少了 0.9M:

操作体积减少
优化前73437KB(71.7MB)-
Inspect Code71054KB(69.3MB)2383KB(2.3M)
Remove Launcher71035KB(69.3MB)19KB
ShrinkResources67576KB(65.9MB)3459KB(3.37M)
Png2webp64505KB(62.9M)3071KB(3M)
R863506KB(62M)999KB(0.97M)

上面是 R8 的普通模式,R8 还有完全模式,还会做一些额外的优化操作,R8 开启完全模式,但是目前还是实验性质的:

android.enableR8.fullMode=true

重新打一个 release 包,发现减少了 0.16M:

操作体积减少
优化前73437KB(71.7MB)-
Inspect Code71054KB(69.3MB)2383KB(2.3M)
Remove Launcher71035KB(69.3MB)19KB
ShrinkResources67576KB(65.9MB)3459KB(3.37M)
Png2webp64505KB(62.9M)3071KB(3M)
R863506KB(62M)999KB(0.97M)
R8 FullMode63333KB(61.8M)173KB(0.16M)

通过自定义 View 来代替图标

我们还可以通过自定义 View 来代替一些状态图标,比如订单状态、退款状态等。如下所示:

在这里插入图片描述

类似这些图标都是可以使用自定义 View 来完成,可以减少大量的图片资源。如果状态很多,就会需要很多的状态图标,如果支持国际化的话,还需要为每个国家生成对应的状态图标。

经过自定义 View 替换状态图标后,包体积减少了 0.366M:

操作体积减少
优化前73437KB(71.7MB)-
Inspect Code71054KB(69.3MB)2383KB(2.3M)
Remove Launcher71035KB(69.3MB)19KB
ShrinkResources67576KB(65.9MB)3459KB(3.37M)
Png2webp64505KB(62.9M)3071KB(3M)
R863506KB(62M)999KB(0.97M)
R8 FullMode63333KB(61.8M)173KB(0.16M)
CustomView62958KB(61.4M)173KB(0.36M)

使用 AndResGuard

微信使用的 AndResGuard 可以对资源资源路径以及资源名字进行混淆,资源名字全部改成类似 abc 的样子。可以大大减少名字字符占用的空间大小。

特别是模块化后,为了防止资源重名,我们都会在资源的加上模块前缀,这样导致资源的名称就更长了。使用 AndResGuard 的时,程序中通过 getIdentifier 方式获取资源,一定要加入白名单,这个可以在程序中全局查找。

通过 AndResGuard 混淆后,包体积减少了 3.54M:

操作体积减少
优化前73437KB(71.7MB)-
Inspect Code71054KB(69.3MB)2383KB(2.3M)
Remove Launcher71035KB(69.3MB)19KB
ShrinkResources67576KB(65.9MB)3459KB(3.37M)
Png2webp64505KB(62.9M)3071KB(3M)
R863506KB(62M)999KB(0.97M)
R8 FullMode63333KB(61.8M)173KB(0.16M)
CustomView62958KB(61.4M)173KB(0.36M)
AndResGuard59323KB(57.9M)3635KB(3.54M)

so 文件

在主流的手机CPU架构都是 ARM,基本上只要支持这一种架构就可以了。更多关于这方面的知识可以查看 Android NDK ~ 基础入门指南

我们来看下市面上主流的 app 支付宝和微信的 CPU 架构:

alipay-arm

weixin-arm

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。

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Chiclaim

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值