Android修炼系列(22),我的 apk 瘦身知识点

3,086 阅读14分钟

前言

7月真的忙疯了,博客也没时间写,争取在8月多输出几篇。

本节主要说下缩包的一些知识点,缩减 APK 大小是在 App 开发中,一个很重要的优化专项了。从用户角度来说,安装包占用内存当然是越小越好,从应用本身来说,安装包太大还会影响 App 的加载速度、耗电量等,所以一款应用做到一定阶段,是一定会考虑缩减包体积的。

APK结构

再说缩包方案前,依照惯例,都要把 apk 的结构拿出来晾下:

bbb.png

  • META-INF/:内有 CERT.SF 、 CERT.RSA 、MANIFEST.MF 文件。其中 CERT.RSA 是开发者利用私钥对 APK 进行签名的签名文件,CERT.RSA 、MANIFEST.MF 记录了文件的 SHA-1 哈希值。
  • assets/:包含一些配置文件、资源文件,可以使用 AssetManager 获取。
  • resources.arsc:已编译的二进制资源文件。包括 res/values/ 文件夹的所有 xml 配置文件、语言字符串和样式,以及未直接包含在 resources.arsc 文件中的内容(例如布局文件和图片)的路径。
  • res/:一些没有被编译到 resources.arsc 中的资源文件,如 drawable、layout、anim 等 xml 文件。
  • lib/:包含各平台类型的库文件。
  • classes.dex: 由Java 字节码文件编译成的虚拟机能够理解的 dex 文件
  • AndroidManifest.xml:Android 清单文件,包括应用名称、版本、访问权限和引用的库文件。

构建流程

说完 apk 结构后,再来看下构建流程,一个 apk 是如何被编译出来的呢?先来一张吓人的官网流程图

Android Build Process (1).png

通过上图可以看到ManifestResourcesAssets的资源经过AAPT处理后生成R.javaProguard ConfigurationCompiled Resources。其中R.java大家都比较熟悉,这里就不过多介绍了。我们来重点看看Proguard ConfigurationCompiled Resources都是做什么的呢?

  • Proguard Configuration是AAPT工具为Manifest中声明的四大组件以及布局文件中(XML layouts)使用的各种Views所生成的ProGuard配置,该文件通常存放在${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/proguard-rules/${flavorName}/${buildType}/aapt_rules.txt,下面是项目中该文件的截图,红框标记出来的就是对AndroidManifest.xmlXML Layouts中相关Class的ProGuard配置。

  • Compiled Resources是一个Zip格式的文件,这个文件的路径通常为${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/res/resources-${flavorName}-${buildType}-stripped.ap_。 通过下面经过Zip解压后的截图,可以看出这个文件包含了resAndroidManifest.xmlresources.arsc的文件或文件夹。结合 Build Workflow 中的描述,可以看出这个文件(resources-${flavorName}-${buildType}-stripped.ap_)会被apkbuilder打包到APK包中,它其实就是APK的“资源包”(resAndroidManifest.xmlresources.arsc)。

更多可以去看下 美团技术团队-App包瘦身优化实践。

结合流程图和最终的 apk 的结构,我们先来思考下,aapt (aapt2)、proguard(R8)、apkbuilder 都是做什么用的?

  • aapt2 编译res/文件,生成编译后的二进制资源文件(.ap_文件)、R.java文件。

  • aidl 工具,根据 aidl 文件生成对应的 Java 接口文件

  • Javac 工具,将 R.java.java文件Aidl 接口文件编译成 .class 文件

  • R8 将上一步产生的 .class 文件和第三方依赖中的 .class文件 编译成 .dex 文件

  • apkbuilder 将编译后的资源(.ap_文件)、dex文件及其他资源文件(例如:so文件、asset文件等),压缩成一个 .apk 文件。

  • Jarsigner 读取签名文件,对上一步中产生的 apk 文件进行签名,生成一个已签名的apk文件。

  • zipalign 对已签名的 apk 文件进行体积优化,注意只 v1 签名才有这一步,v2 签名的 apk 会在 zipalign 后签名被破坏。

缩包方案

压缩、混淆和优化

当使用 Gradle 插件 3.4.0 或更高版本构建项目时,将不再使用 ProGuard 执行编译时代码优化,而是采用了 R8 编译器处理以下任务:

  • 代码缩减:从应用及其库依赖项中检测并安全地移除不使用的类、字段、方法和属性(这能有效规避规避 64k 引用限制问题)。例如,当我们仅使用某个库的少数几个 API时,缩减任务可以识别应用不使用的库代码并仅从应用中移除这部分代码。

  • 资源缩减:从应用中移除不使用的资源,包括应用库依赖项中不使用的资源。此功能可与代码缩减功能结合使用,这样一来,移除不使用的代码后,也可以安全地移除不再引用的所有资源。

  • 混淆:缩短类和成员的名称,从而减小 DEX 文件的大小。

  • 优化:检查并重写代码,以进一步减小应用的 DEX 文件的大小。例如,如果 R8 检测到从未采用过给定 if/else 语句的 else {} 分支,则会移除 else {} 分支的代码。

AS 为了更快的编译速度,有的优化任务默认是关闭的,需要我们可以在 gradle 中启用:

    buildTypes {
        release {
            // 混淆和代码缩减
            minifyEnabled true
            // 启用资源缩减
            shrinkResources true
        }
    }

在实际在使用 shrinkResources 配置时,还要注意,如:一个资源由 resources.getIdentifier 获取,而资源却被编译器错误的移除掉了的情况。这种情况可以通过 tools:keep 属性来手动保留这些资源:

<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@layout/l_used_a,@layout/l_used_b*,@layout/l_used*_c"
    /* 也支持指定舍弃某些资源 */
    tools:discard="@layout/unused2" />

R8 下的资源缩减默认只会移除未由应用代码引用的资源,这意味着,它不会移除用于不同设备配置的备用资源,所以我们可以使用 resConfigs 属性来剔除一些无关资源:

    android {
        defaultConfig {
            /* 只保留汉语的语言资源 */
            resConfigs "zh", "zh-rCN"
        }
    }

这里多说一嘴,R8 编译器是做什么的呢?它将项目里的 Java 字节码转换成了在 Android 平台上能运行的 dex 格式文件。所以在编译时,它能够对原字节码做上面列出的优化任务,从而生成一个优化后的 .dex 文件。

图片优化

Android 资源图片格式通常为:PNG、JPG 和 WebP,对于每种格式,都可以采取相应步骤来缩减图片大小。

矢量位图

矢量图形可以创建与分辨率无关的图标和其他可伸缩媒体,使用这些图形可以帮我们极大地减少 APK 占用的空间。矢量图片在 Android 中以 VectorDrawable 对象的形式表示,借助 VectorDrawable 对象,100 字节的文件可以生成与屏幕大小相同的清晰图片。

不过,系统渲染每个 VectorDrawable 对象需要花费大量时间,而较大的图片则需要更长的时间才能显示在屏幕上。因此,官方建议仅在显示小图片时使用这些矢量图形。

再多说一嘴,在创建逐帧动画时不要使用 AnimationDrawable,因为这样做需要为动画的每个帧添加单独的位图文件,而这会大大增加 APK 的大小。替代方案就是使用 AnimatedVectorDrawableCompat 创建矢量动画。方法可参考我的 Android修炼系列(十六),这些动画方式你都知道吗?

WebP

WebP 是 Android 4.2.1(API 级别 17)支持的图片格式。这种格式可为图片提供出色的无损压缩和有损压缩效果。使用 WebP,我们可以创建更小、更丰富的图片。WebP 无损图片文件比 PNG 平均缩小了 26%。这些图片文件还支持透明度,只需增加 22% 的字节。

WebP 有损图片比采用等效 SSIM 质量指标的 JPG 图片缩小 25-34%。对于可以接受有损 RGB 压缩的情况,有损 WebP 也支持透明度,生成的文件大小通常比 PNG 小 3 倍。

所以不管对 PNG,还是 JPG 来说, WebP 都是理想的替代选择。唯一需要注意的是,WebP 仅在 Android 4.2.1 及更高版本的设备上受到原生支持。

再多说一嘴,WebP 并不是在 Android 4.2.1 下就没办法用了,只是说不受原生支持了,我们依然可以通过三方库来支持 webp 的编码/解压操作的。

AndroidStudio 现在可以直接将 BMP、JPG、PNG 或静态 GIF 图片转换为 WebP 格式,不仅可转换单张图片,还可以转换包含多张图片的文件夹。具体就不在这里说了,可以直接文看档 WebP 格式转换

webp.png

PNG 和 JPG

JPG 和 PNG 的压缩过程截然不同,产生的结果也差异显著。PNG 和 JPG 之间的选择往往取决于图片本身的复杂程度。下图显示的两张图片因采用不同的压缩方案而出现了截然不同的结果。左侧的图片包含许多小细节,因此使用 JPG 进行压缩的效率更高。右侧的图片包含连续的相同颜色,使用 PNG 进行压缩的效率更高。

pngjpg.png

对于JPG 和 PNG 资源,我们可以筛选出比较大的图片,直接使用 tinypng 进行压缩,其是通过合并图片中相似的颜色,将24位的 png 图片压缩成8位色值图片,并去掉了图片 matadata 信息,大概能减少 30% 左右。针对不透明的 png 图片,我们可以转换为 jpg 图片,效果显著。

要压缩 JPEG 文件,可以使用 packJPGguetzli。这些工具能够在保持图片质量不变的情况下,压缩图片文件,如 guetzli 能将文件大小降低35%。

JPG 可以使用标量值来平衡图片质量和文件大小,75% 的画质对于大多数图片来说都还不错,缩略图的质量级别推荐 35% 。

如何选择?

那么不同格式的图片,我们该如何选择呢?google 已经帮我们提供了方案:如果支持 WebP 则优先用 WebP,而 PNG 主要用在展示透明或者简单的图片,而其它场景可以使用 JPG 格式。当然如果我们能用 VectorDrawable 的话则优先使用 VectorDrawable。

aaaa.png

还有一点要注意,aapt 工具本身就可以在编译过程中通过无损压缩来优化放置在 res/drawable/ 中的 png 图片资源。例如,aapt 工具可以通过调色板将不需要超过 256 种颜色的真彩色 png 转换为 8 位 png,来生成质量相同但内存占用量更小的图片。

但 aapt 工具可能会扩充我们已经压缩好的 png 文件。为防止出现这种情况,我们需要在 Gradle 中使用 cruncherEnabled:

    aaptOptions {
        // 标记为 PNG 文件停用此过程
        cruncherEnabled = false
    }

资源的混淆

与上面说的 R8 的混淆优化不一样,R8 的混淆优化是针对的类和成员的名称,而这里的资源混淆,只针对资源。

这里推荐使用微信提供的混淆工具 AndResGuard ,AndResGuard 的原理类似 Java Proguard,但是只针对资源。他会将原本冗长的资源路径变短,例如将 res/drawable/wechat 变为 r/d/a。

AndResGuard 不涉及编译过程,只需输入一个 apk (无论签名与否,debug 版,release 版均可,在处理过程中会直接将原签名删除),可得到一个实现资源混淆后的 apk (若在配置文件中输入签名信息,可自动重签名并对齐,得到可直接发布的 apk )以及对应资源 ID 的 mapping 文件。具体使用方法可以直接看 gitHub,很详细,这里就不多介绍了。

apply plugin: 'AndResGuard'

buildscript {
    ...
    dependencies {
        classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.21'
    }
}

这里多说一嘴,如果使用 getIdentifier 获取资源的,记得将资源添加进白名单。

so 优化

在实际的项目里,so 库文件一直都是包体积难以减小的杀手。通过上面的流程图可知,so 文件会被 apkbuilder 一起打包成 apk 文件,在这个过程里,系统会对 so 文件进行压缩,算法采用的是级别为 8 的 zip deflate 压缩算法。关于 so 的优化,常用的主要有几种:

  1. 对于 so 文件,我们可以根据需要,减少对于 cpu 平台的支持。需要注意的一点是,现在 google 要求app 适配 v8a 了,国内的 vivo、小米等商店也开始推进 v8a 的适配了:
    ndk {
        abiFilters 'armeabi-v7a', 'arm64-v8a'
    }
  1. 对于 so 的大小优化,我们还可以采用动态下发的方式,不过这种方案具有一定的开发量和难度,具体流程后续会单独写一节介绍。

  2. 相对于zip压缩,我们可以采用压缩率更好的7z lzma 算法压缩,基本单个文件的压缩率能提高约10%,但有点需要注意,使用7z压缩后的so,在加载之前,需要进行7z解压操作,这里会有一个耗时,建议此时项目加载so采用异步方式,(这里内容非常多,我计划着在后续的 gradle 插件模块文章里搞下)。

assets 优化

对于 assets 文件夹中的资源,像raw文件、字体包、插件文件,可以考虑采用动态下载的方式,对于必须预置的资源,我们可以采用 7zip 的压缩方式。

dex 优化

ReDex 是由 Facebook 开发的 Android 字节码 (dex) 优化器。它提供了一个用于读取、写入和分析 .dex 文件的框架,以及一组使用该框架改进字节码的优化通道。

具体使用方法可以直接查看 gitHub redex 教程

其它

仅支持特定密度

Android 支持多种设备,涵盖了各种屏幕密度。在 Android 4.4(API 级别 19)及更高版本中,框架支持各种密度:ldpimdpitvdpihdpi, xhdpixxhdpi 和 xxxhdpi,我们可以只按需支持。

重复使用资源

我们可以重复使用同一组资源,并在运行时根据需要对其进行自定义,例如官网示例:通过旋转,将资源图片从“拇指向上”变为“拇指向下”,这样的好处就是不需要两组图片资源了。

    <rotate xmlns:android="http://schemas.android.com/apk/res/android"
        android:drawable="@drawable/ic_thumb_up"
        android:pivotX="50%"
        android:pivotY="50%"
        android:fromDegrees="180" />

在图片色调调整、阴影设置、图片渲染等方面也一样,Android 提供了一些实用程序来更改资源的颜色。在 Android 5.0(API 级别 21)及更高版本上,使用 android:tint 和 tintMode 属性,对于较低版本的平台,则使用 ColorFilter 类。

将矢量图形用于动画图片

不要使用 AnimationDrawable 创建逐帧动画,因为这样做需要为动画的每个帧添加单独的位图文件,而这会大大增加 APK 的大小。我们应改为使用 AnimatedVectorDrawableCompat 创建 动画矢量可绘制资源

移除不必要的生成代码

确保了解自动生成的任何代码所占用的空间。例如,许多协议缓冲区工具会生成过多的方法和类,这可能会使应用的大小增加一倍或两倍。还有编译期注解、字节码操作的一些工具类,一定要对自动生成的类心里有数。

避免使用枚举

单个枚举会使应用的 classes.dex 文件增加大约 1.0 到 1.4KB 的大小。这些增加的大小会快速累积,产生复杂的系统或共享库。

如果可能,请考虑使用 @IntDef 注释和 代码缩减 移除枚举并将它们转换为整数。此类型转换可保留枚举的各种安全优势。

移除调试符号

编译发布版本时,可以使用 Android NDK 中提供的 arm-eabi-strip 工具从原生库中移除不必要的调试符号。

避免解压缩原生库

在构建应用的发布版本时,可以通过在应用清单的 <application> 元素中设置 android:extractNativeLibs="false",将未压缩的 .so 文件打包在 APK 中。停用此标记可防止 PackageManager 在安装过程中将 .so 文件从 APK 复制到文件系统,并具有减小应用更新的额外好处。(但这里和上面的so优化第3点有悖,需要看哪个收益高了)。

使用 Android Gradle 插件 3.6.0 或更高版本构建应用时,插件会默认将此属性设为 "false"

参考:

developer.android.com/topic/perfo…

developer.android.com/studio/buil…