0%

Android App 包体积变化史

前言

探索如何压缩一个 Android App 的包体积

Hello World

原始配置

首先是创建一个 Hello World App,只有一个 Activity 的 App 有多大。

创建 v1.0.1 的一个 Hello World App。没有多余的配置,只有 release 的 key 。

build.gradle 配置细节
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
compileSdkVersion 29
buildToolsVersion "29.0.3"

defaultConfig {
applicationId "com.engineer.android.mini"
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

signingConfigs {
release {
keyAlias MYAPP_RELEASE_KEY_ALIAS
keyPassword MYAPP_RELEASE_KEY_PASSWORD
storeFile file(MYAPP_RELEASE_STORE_FILE)
storePassword MYAPP_RELEASE_STORE_PASSWORD
}
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}

}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

分别打出 debug 和 release 包。

  • debug

    1
    2
    3
    4
     Mode                LastWriteTime         Length Name
    ---- ------------- ------ ----
    -a---- 2020/5/6 21:12 2506450 app-debug.apk
    -a---- 2020/5/6 21:12 239 output.json
  • release

    1
    2
    3
    4
     Mode                LastWriteTime         Length Name
    ---- ------------- ------ ----
    -a---- 2020/5/6 21:08 2117330 app-release.apk
    -a---- 2020/5/6 21:08 247 output.json

看一下 release 包的内容

可以看到主要还是 classes.dex 和 res 文件夹(资源文件)占用较多。其他如 kotlin 、META-INF 均为中间生成的文件,无法简单的进行优化。再有 AndroidManifest 文件更是无从优化。下面就从最容易上手的步骤开始逐步优化。

首先是进行代码混淆和资源的压缩。

配置混淆、资源压缩等

1
2
3
4
5
6
7
8
9
10
11
12
13
 buildTypes {
release {
//混淆
minifyEnabled true
//所以尽可能的减少第三方的使用 也是可以降低混淆的难度
//Zipalign优化
zipAlignEnabled true
// 移除无用的resource文件
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}

完成上述配置后再次打 release 包

1
2
3
4
Mode                LastWriteTime         Length Name
---- ------------- ------ ----
-a---- 2020/5/6 22:25 1135908 app-release.apk
-a---- 2020/5/6 22:25 247 output.json

看一下 release 包的内容

对比之前的截图,可以看到经过混淆的处理,classes.dex 整体压缩比非常高,从 1.3 MB 讲到了 332 kb。但是 res 文件夹内的资源文件并没有显著的效果,下面看看这部分是否有压缩空间。

代码 v1.0.3

为了方便后面的分析,这里了解一下 Apk 包中各类文件的用处。

Apk 包分析

我们都知道,Android 项目最终会编译成一个 .apk 后缀的文件,实际上它就是一个压缩包,和我们常接触的xxx.zip 以及 xxx.rar 本质上没有区别。因此,它内部还有很多不同类型的文件,这些文件,按照大小,共分为如下四类:

  1. 代码相关:classes.dex,我们在项目中所编写的 java 文件,经过编译之后会生成一个 .class 文件,而这些所有的 .class 文件呢,它最终会经过 dx 工具编译生成一个 classes.dex。

  2. 资源相关:res、assets、编译后的二进制资源文件 resources.arsc 和 清单文件 等等。res 和 assets 的不同在于 res 目录下的文件会在 .R 文件中生成对应的资源 ID,而 assets 不会自动生成对应的 ID,而是通过 AssetManager 类的接口来获取。此外,每当在 res 文件夹下放一个文件时,aapt 就会自动生成对应的 id 并保存在 .R 文件中,但 .R 文件仅仅只是保证编译程序不会报错,实际上在应用运行时,系统会根据 ID 寻找对应的资源路径,而 resources.arsc 文件就是用来记录这些 ID 和 资源文件位置对应关系 的文件。

  3. So 相关:lib 目录下的文件,这块文件的优化空间其实非常大。

  4. 还有 META-INF,它存放了应用的 签名信息,其中主要有 3个文件,如下所示

    • MANIFEST.MF:其中每一个资源文件都有一个对应的 SHA-256-Digest(SHA1) 签名,MANIFEST.MF 文件的 SHA256(SHA1) 经过 base64 编码的结果即为 CERT.SF 中的 SHA256(SHA1)-Digest-Manifest 值。

    • CERT.SF:除了开头处定义的 SHA256(SHA1)-Digest-Manifest 值,后面几项的值是对 MANIFEST.MF 文件中的每项再次 SHA256(SHA1) 经过 base64 编码后的值。

    • CERT.RSA:其中包含了公钥、加密算法等信息。首先,对前一步生成的 CERT.SF 使用了 SHA256(SHA1)生成了数字摘要并使用了 RSA 加密,接着,利用了开发者私钥进行签名。然后,在安装时使用公钥解密。最后,将其与未加密的摘要信息(MANIFEST.MF文件)进行对比,如果相符,则表明内容没有被修改。

再回过头看一下 release 刚才生成的包内资源相关的详细内容。


res 内为了适配各类设备,包含各种类型的资源,比如 xxxh,xxh, v17,v4,v20 甚至是 watch 的适配。还会包含多语言的一些资源,大部分情况下,是不需要这么繁杂的适配的。因此,可以做如下配置

1
2
3
4
5
6
7
8
9
android {
...else...

defaultConfig {
...else...

resConfigs "zh-rCN","xxhdpi"
}
}

这样只会保留中文资源和 xxhdpi 的资源,当然,如果某个资源只在 xxxhdpi 下存在的话,那么也会智能的进行保留。

完成上述配置后,再次进行打包。

1
2
3
4
Mode                LastWriteTime         Length Name
---- ------------- ------ ----
-a---- 2020/5/7 21:31 885611 app-release.apk
-a---- 2020/5/7 21:31 247 output.json

可以看到 apk 的大小已经来到了 1 MB之内,只有 865 kb 了。关于 APK Size 和 Download Size 的区别,可以点击链接了解一下。总的来说,对国内用户似乎没有什么差异。

通过配置去除国家化和单一分辨率,res 减少了 33%,同时使的 resources.arsc 文件减小了将近一半的大小。

小结

至此,已经完成了一个基础项目的最小化的常规操作。865 kb 是最小值了吗?其实不然,res 文件夹下仍有可以优化的空间,同时可以通过自定义打包流程,用自己的算法压缩 apk ,或者用第三方工具对 dex 文件进行更精细化的处理。

其实,控制包体积是一个长期的任务,因为随着业务代码的增加,包体积一定会一天天的增长,因此需要在版本迭代的过程中不断就引入的资源、三方库和自己写的代码,进行分析、规划和记录,在恰当的时候删除冗余的代码和资源,同时也要有节制的进行代码的添加(这里主要还是三方库的引入和资源文件的添加)

参考文档

深入探索 Android 包体积优化(匠心制作)

加个鸡腿呗.