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

概述

用户一般都不肯意去下载一个比较大的程序,特别是不在 WIFI 的状况下。若是你的安装包很小,用户仍是愿意下载安装体验下的。如今市面上知足某种需求的 App 一般都会有不少款,如何让用户愿意下载你的 App 来体验?安装包越小,在 WIFI 状况下,极速下载安装,开始体验。在移动网络状况下,包体积越小,用户安装的的可能性越大。因此安装包大小对用户的转换率有很大的影响。接下来就和你们分享下我在实际中工做中对包体积优化的一些经验。android

APK 文件结构

既然是要优化 Android APK 安装文件的大小,首要须要了解下 APK 文件的结构。将 APK 文件拖进 AndroidStudio 能够清楚的看到 APK 文件组成部分。APK 主要由如下几部分组成:git

  • 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),为后面作的优化好有一个对比,看看具体的优化幅度。github

经过 AndroidStudio 移除未使用的资源

手动移除资源有两个好处:一个是减小安装包的体积,另外一个是减小源代码的体积。web

在 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、Dagger二、Retrofit、OkHttp、ButterKnife、Router 的原理分析 等,持续更新,欢迎 star。