Home APK 大小优化之资源优化
Post
Cancel

APK 大小优化之资源优化

原文链接

作者 Jake Wharton

写作时间 01/09/2020

布局文件会在 Android APK 文件中出现多少次?我们可以用一个布局文件构建一个最小的 APK 来计算发生的次数。

用 Gradle 构建一个 Android 应用程序只需要一件事:一个带有AndroidManifest.xml文件的包。我们可以添加一个虚拟的页面布局,内容只是<merge/>,因为我们只关心它的名称。

1
2
3
4
5
6
7
8
.
├── build.gradle
└── src
    └── main
        ├── AndroidManifest.xml
        └── res
            └── layout
                └── home_view.xml

运行 gradle assemblerrelease 将生成一个 2,118 字节的 APK。我们可以使用 xxd 转储它的内容,然后查找 home_view 字节序列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ xxd build/outputs/apk/release/app-release-unsigned.apk
   ⋮
000004c0: 0000 0074 0000 0018 0000 0072 6573 2f6c  ...t.......res/l
000004d0: 6179 6f75 742f 686f 6d65 5f76 6965 772e  ayout/home_view.
000004e0: 786d 6c63 66e0 6028 6160 6060 6490 61d0  xmlcf.`(a```d.a.
   ⋮
00000570: 0000 0000 0000 0000 1818 7265 732f 6c61  ..........res/la
00000580: 796f 7574 2f68 6f6d 655f 7669 6577 2e78  yout/home_view.x
00000590: 6d6c 0000 0002 2001 f801 0000 7f00 0000  ml.... .........
   ⋮
00000700: 0000 0000 0909 686f 6d65 5f76 6965 7700  ......home_view.
00000710: 0202 1000 1400 0000 0100 0000 0100 0000  ................
   ⋮
00000870: 0000 ad04 0000 7265 732f 6c61 796f 7574  ......res/layout
00000880: 2f68 6f6d 655f 7669 6577 2e78 6d6c 504b  /home_view.xmlPK
   ⋮

基于此命令的输出,在APK中出现了三个未压缩的路径和一个只有名称的未压缩路径。

如果你没有读过我关于计算zip条目大小的文章,或者不熟悉zip文件的结构,那么zip文件就是一个文件条目列表,后面跟着一个包含所有可用条目的目录。每个条目都包含文件路径和目录。xxd 命令的输出结果中的第一条(条目头)和最后一条(目录记录)说明了这一点。

输出结果中中间出现的两条记录来自 resources.arsc 文件。此文件是一个资源分类的数据库文件。它的内容是可见的,因为该文件在APK中是未经压缩的。运行 aapt dump——values resources build/outputs/apk/release/app-release-unsigned.apk 显示 home_view 记录及其映射到路径:

1
2
3
4
5
6
7
8
Package Groups (1)
Package Group 0 id=0x7f packageCount=1 name=com.example
  Package 0 id=0x7f name=com.example
    type 0 configCount=1 entryCount=1
      spec resource 0x7f010000 com.example:layout/home_view: flags=0x00000000
      config (default):
        resource 0x7f010000 com.example:layout/home_view: t=0x03 d=0x00000000 (s=0x0008 r=0x00)
          (string8) "res/layout/home_view.xml"

APK包含在 classes.dex 文件中第五次出现的名称。它不会出现在 xxd 输出中,因为文件被压缩了。运行 baksmali dump <(unzip -p build/outputs/apk/release/app-release-unsigned.apk classes.dex) 显示了dex文件的字符串表,其中包含一个 home_view 的条目:

1
2
3
4
                           |[10] string_data_item
000227: 09                 |  utf16_size = 9
000228: 686f 6d65 5f76 6965|  data = "home_view"
000230: 7700               |

这是 R.layout 类中的字段,它将布局名称映射到一个唯一的整数值。顺便说一下,这个整数是resource.arsc 数据库中资源的索引,以查找相关的文件名和读取其XML内容。

总结一下我们的问题的答案,对于每个资源文件,完整路径出现三次,名称出现两次。

资源优化

Android Gradle插件4.2版本介绍了 enableResourceOptimizations=true 标志,它将针对资源进行优化。这将在合并资源时和 resources.arsc 文件打包到APK文件之前调用 aapt optimize 命令。无论 minifyEnabled 是否设置为true,资源优化只适用于release版本。

将该标志添加到 gradle.properties 中。我们可以用diffuse比较两个APK文件,看看它的效果。输出结果有点长,我们将按部分对其进行分解。

1
2
3
4
5
6
7
8
9
10
11
12
          │       compressed        │       uncompressed
          ├─────────┬───────┬───────┼─────────┬─────────┬───────
 APK      │ old     │ new   │ diff  │ old     │ new     │ diff
──────────┼─────────┼───────┼───────┼─────────┼─────────┼───────
      dex │   695 B │ 695 B │   0 B │ 1,016 B │ 1,016 B │   0 B
     arsc │   682 B │ 674 B │  -8 B │   576 B │   564 B │ -12 B
 manifest │   535 B │ 535 B │   0 B │ 1.1 KiB │ 1.1 KiB │   0 B
      res │   185 B │ 157 B │ -28 B │   116 B │   116 B │   0 B
    asset │     0 B │   0 B │   0 B │     0 B │     0 B │   0 B
    other │    22 B │  22 B │   0 B │     0 B │     0 B │   0 B
──────────┼─────────┼───────┼───────┼─────────┼─────────┼───────
    total │ 2.1 KiB │ 2 KiB │ -36 B │ 2.7 KiB │ 2.7 KiB │ -12 B

首先是APK中内容的不同。”compressed” 列是APK内部的大小,而 “uncompressed” 列是解压后时的大小。

res 表示单一资源文件的大小减少28字节。arscresources.arsc 文件减小8个字节。我们将很快看到这些变化的原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
DEX      │ old │ new │ diff
─────────┼─────┼─────┼───────────
   files │   1 │   1 │ 0
 strings │  15 │  15 │ 0 (+0 -0)
   types │   8 │   8 │ 0 (+0 -0)
 classes │   2 │   2 │ 0 (+0 -0)
 methods │   3 │   3 │ 0 (+0 -0)
  fields │   1 │   1 │ 0 (+0 -0)


 ARSC    │ old │ new │ diff
─────────┼─────┼─────┼──────
 configs │   1 │   1 │  0
 entries │   1 │   1 │  0

这两个部分表示资源数据库的代码和内容。没有任何变化,因此我们可以推断优化没有影响 R.layout.home_viewhome_view 资源条目。

1
2
3
4
5
6
7
8
9
10
11
12
13
=================
====   APK   ====
=================

   compressed   │  uncompressed  │
───────┬────────┼───────┬────────┤
 size  │ diff   │ size  │ diff   │ path
───────┼────────┼───────┼────────┼────────────────────────────
       │ -185 B │       │ -116 B │ - res/layout/home_view.xml
 157 B │ +157 B │ 116 B │ +116 B │ + res/eA.xml
 674 B │   -8 B │ 564 B │  -12 B │ ∆ resources.arsc
───────┼────────┼───────┼────────┼────────────────────────────
 831 B │  -36 B │ 680 B │  -12 B │ (total)

最后,文件更改的粒度差异显示了优化的效果。我们的布局资源的文件名被明显截断,并被移出了 layout/ 文件夹!

在Gradle项目中,XML的文件夹和文件名是有意义的。文件夹是资源类型,其名称对应于 .arsc 文件中生成的字段和资源条目。然而,一旦这些文件在APK中,文件路径就没有任何意义了。通过尽可能的缩短名称来实现资源优化 (1)。

aapt dump 的输出也能确认这一点,资源数据库中文件的更改:

1
2
3
4
5
6
7
8
Package Groups (1)
Package Group 0 id=0x7f packageCount=1 name=com.example
  Package 0 id=0x7f name=com.example
    type 0 configCount=1 entryCount=1
      spec resource 0x7f010000 com.example:layout/home_view: flags=0x00000000
      config (default):
        resource 0x7f010000 com.example:layout/home_view: t=0x03 d=0x00000000 (s=0x0008 r=0x00)
          (string8) "res/eA.xml"

在APK中出现的所有三次路径现在都更短了,这节省了36字节。虽然36字节是一个非常小的数字,但请记住,整个二进制只有2,118字节。36字节的节省相当于减少了1.7%的尺寸!

真实案例

一个实际应用程序的资源数量远远不止一个。这种优化应用到实际应用程序时是什么样子的?

Plaid

Nick Butcher的Plaid应用有734个资源文件。除了数量之外,资源文件的名称更具描述性(这是一种更长的说法)。与 home_view 不同,Plaid包含的名称是 searchback_stem_search_to_back.xmlattrs_elastic_drag_dismiss_frame_layoutdesigner_news_story_description.xml

在将项目更新到AGP 4.2之后,我使用 diffuse 来比较一个没有资源优化的版本和一个启用了资源优化的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
          │            compressed             │           uncompressed
          ├───────────┬───────────┬───────────┼───────────┬───────────┬───────────
 APK      │ old       │ new       │ diff      │ old       │ new       │ diff
──────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────
      dex │   3.8 MiB │   3.8 MiB │       0 B │   9.9 MiB │   9.9 MiB │       0 B
     arsc │ 316.7 KiB │ 292.5 KiB │ -24.2 KiB │ 316.6 KiB │ 292.4 KiB │ -24.2 KiB
 manifest │     3 KiB │     3 KiB │       0 B │  11.9 KiB │  11.9 KiB │       0 B
      res │ 539.2 KiB │ 490.7 KiB │ -48.5 KiB │ 617.2 KiB │ 617.2 KiB │       0 B
   native │   4.6 MiB │   4.6 MiB │       0 B │   4.6 MiB │   4.6 MiB │       0 B
    asset │       0 B │       0 B │       0 B │       0 B │       0 B │       0 B
    other │  83.6 KiB │  83.6 KiB │       0 B │ 128.6 KiB │ 128.6 KiB │       0 B
──────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────
    total │   9.4 MiB │   9.3 MiB │ -72.7 KiB │  15.6 MiB │  15.5 MiB │ -24.2 KiB

资源优化节省0.76%的APK文件大小。资源优化对 native 库的大小影响小于我的预期。

SeriesGuide

Uwe Trottmann的SeriesGuide应用程序有1044个资源文件。与Plaid不同的是,它不需要 native 库,而 native 库会增加优化的影响。

我再次将项目更新到AGP 4.2,并使用diffuse来比较两个版本:

1
2
3
4
5
6
7
8
9
10
11
12
         │            compressed             │           uncompressed
          ├───────────┬───────────┬───────────┼───────────┬───────────┬───────────
 APK      │ old       │ new       │ diff      │ old       │ new       │ diff
──────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────
      dex │   2.4 MiB │   2.4 MiB │       0 B │   5.7 MiB │   5.7 MiB │       0 B
     arsc │   1.7 MiB │   1.6 MiB │ -32.9 KiB │   1.7 MiB │   1.6 MiB │ -32.9 KiB
 manifest │   5.6 KiB │   5.6 KiB │       0 B │  28.3 KiB │  28.3 KiB │       0 B
      res │ 693.9 KiB │   628 KiB │   -66 KiB │ 992.2 KiB │ 992.2 KiB │       0 B
    asset │  39.9 KiB │  39.9 KiB │       0 B │ 100.4 KiB │ 100.4 KiB │       0 B
    other │ 118.1 KiB │ 118.1 KiB │       0 B │ 148.8 KiB │ 148.8 KiB │       0 B
──────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────
    total │   4.9 MiB │   4.8 MiB │ -98.9 KiB │   8.6 MiB │   8.6 MiB │ -32.9 KiB

资源优化能够减少2.0%的APK大小!

Tivi

Chris Banes的Tivi应用使用Jetpack Compose编写了一个重要的部分,这意味着更少的资源。当前构建仍然包含776个资源文件。

由于使用了Compose, Tivi已经在使用最新的AGP4.2。通过两个快速构建,我们可以看到资源优化的影响:

1
2
3
4
5
6
7
8
9
10
11
12
          │            compressed             │           uncompressed
          ├───────────┬───────────┬───────────┼───────────┬───────────┬───────────
 APK      │ old       │ new       │ diff      │ old       │ new       │ diff
──────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────
      dex │     3 MiB │     3 MiB │       0 B │   6.8 MiB │   6.8 MiB │       0 B
     arsc │ 363.4 KiB │ 337.9 KiB │ -25.6 KiB │ 363.3 KiB │ 337.7 KiB │ -25.6 KiB
 manifest │   3.6 KiB │   3.6 KiB │       0 B │  16.1 KiB │  16.1 KiB │       0 B
      res │ 680.4 KiB │ 629.2 KiB │ -51.2 KiB │   1.2 MiB │   1.2 MiB │       0 B
    asset │  39.9 KiB │  39.9 KiB │       0 B │ 100.4 KiB │ 100.4 KiB │       0 B
    other │ 159.9 KiB │ 151.7 KiB │  -8.2 KiB │ 306.3 KiB │ 254.8 KiB │ -51.5 KiB
──────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────
    total │   4.2 MiB │   4.1 MiB │   -85 KiB │   8.8 MiB │   8.7 MiB │ -77.1 KiB

我们又一次达到了APK尺寸缩减2.0%的目标!

另外一种情况

到目前为止,这四个例子都没有使用签名的APK。APK签名有多个版本,如果你的 minSdkVersion 低于24,你需要在签名时包含版本1 (V1)签名。V1签名使用 Java 的 .jar 签名规范,该规范将每个文件作为 META-INF/MANIFEST 中的文本条目单独签名。

在为原始的单布局应用创建并配置keystore之后,使用 unzip -c build/outputs/apk/release/app-release.apk META-INF/MANIFEST.MF

MF的签名如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Manifest-Version: 1.0
Built-By: Signflinger
Created-By: Android Gradle 4.2.0-alpha08

Name: AndroidManifest.xml
SHA-256-Digest: HdoGVd8U3Zjtf2VkGLExAPCQ1fq+kNL8eHKjVQXGI60=

Name: classes.dex
SHA-256-Digest: BVA1ApPvECg56DrrNPgD3jgv1edcM8VKYjcJEAG4G44=

Name: res/eA.xml
SHA-256-Digest: nDn7UQex2OWB3/AT054UvSAx9pYNSWwERCLfgdM6J6c=

Name: resources.arsc
SHA-256-Digest: 6w7i2Z9+LjwqlXS7YhhjzP/XhgvJF3PUuyJM60t0Qbw=

每个文件的完整路径显示出来,使每个资源路径的总出现次数达到 4 次。由于更短的名称将再次导致该文件包含更少的字节,因此资源优化具有更大的影响。


谷歌内部邮件向我介绍这一功能时声称,APK 大小最终可以节省 1-3%。根据真实案例的测试,这个范围似乎是正确的。最终,节省的资源取决于 APK 中的资源文件的大小和数量。

如果你已经在使用 AGP 4.2 并添加了 android.enableResourceOptimizations=true 配置到你的gradle.properties 文件,你可以享受这个免费的缩小 APK 大小的功能。如果您还没有使用 AGP 4.2,请现在添加它,这样你下次升级时就不会忘记了!

This post is licensed under CC BY 4.0 by the author.

Android 应用性能指标

如何衡量和改进 Android 应用启动时间

Comments powered by Disqus.