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 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 文件依然存在:

abc.webp

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

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

-dontshrink

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

操作 体积 减少
优化前 73437KB(71.7MB) -
Inspect Code 71054KB(69.3MB) 2383KB(2.3M)
Remove Launcher 71035KB(69.3MB) 19KB
ShrinkResources 67576KB(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 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)

Enable R8

由于之前 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 来完成,可以减少大量的图片资源。如果状态很多,就会需要很多的状态图标,如果支持国际化的话,还需要为每个国家生成对应的状态图标。

经过自定义 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

微信使用的 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)

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。

发布了168 篇原创文章 · 获赞 1161 · 访问量 128万+

猜你喜欢

转载自blog.csdn.net/johnny901114/article/details/105189854
今日推荐