应用安装包大小的重要性已经不需要多说,安装包大小直接影响用户的下载,留存,甚至部分运营商上线要求必须小于一定的值。但是随着业务的迭代开发,应用会越来越大,安装包体积不断增长。而随着安装包体积的增大,还会导致应用的安装时间,运行内存和 rom 空间也因此增大,因此 APK 的瘦身已经是不得不考虑的事情。
Apk的构成
可以通过Android studio 自带的 Analyze APK 工具分析apk。
直接将电脑上的 apk 拖进 Android Studio 中就可以自动使用 Analyze APK 打开 apk。然后,我们就可以看到 APK 文件的绝对大小以及各组成文件的百分占比,如下图所示:
可以看到APK主要由以下部分组成:
文件/目录 | 描述 |
---|---|
lib/ | 存放so文件,可能会有armeabi、armeabi-v7a、arm64-v8a、x86、x86_64、mips,大多数情况下只需要支持armabi,与x86的架构即可,如果非必需,可以考虑拿掉x86的部分 |
res/ | 存放编译后的资源文件,例如:drawable、layout等等 |
assets/ | 应用程序的资源,应用程序可以使用AssetManager来检索该资源 |
META-INF/ | 该文件夹一般存放于已经签名的APK中,它包含了APK中所有文件的签名摘要等信息 |
classes(n).dex | classes文件是Java Class,被DEX编译后可供Dalvik/ART虚拟机所理解的文件格式 |
resources.arsc | 编译后的二进制资源文件 |
AndroidManifest.xml | Android的清单文件,格式为AXML,用于描述应用程序的名称、版本、所需权限、注册的四大组件 |
在了解了APK各个组成部分以及它们的作用后,现在准备从以下几个方面对apk进行"瘦身“:
DEX优化
Dex 是 Android 系统的可执行文件,包含 应用程序的全部操作指令以及运行时数据。Dex 一般在应用包体积中占据了不少比重,并且,Dex 数量越多,App 的安装时间也会越长。所以,优化它们可以说是 重中之重。下面,我们就来看看有哪些方式可以优化 Dex 这部分的体积。
配置ProGuard
可以通过开启ProGuard来实现代码压缩,可以在build.gradle文件相应的构建类型中添加minifyEnabled true
。
代码压缩会拖慢构建速度,因此应该尽可能避免在调试构建中使用。不过一定要为用于测试的最终APK启用代码压缩,如果不能充分地自定义要保留的代码,可能会引入错误。
配置如下:
buildTypes {
release {
// 1、是否进行混淆
minifyEnabled true
// 2、开启zipAlign可以让安装包中的资源按4字节对齐,这样可以减少应用在运行时的内存消耗
zipAlignEnabled true
// 3、移除无用的resource文件:当ProGuard 把部分无用代码移除的时候,
// 这些代码所引用的资源也会被标记为无用资源,然后系统通过资源压缩功能将它们移除。
// 需要注意的是目前资源压缩器目前不会移除values/文件夹中定义的资源(例如字符串、尺寸、样式和颜色)
shrinkResources true
// 4、混淆文件的位置,其中 proguard-android.txt 为sdk默认的混淆配置,
// 它的位置位于android-sdk/tools/proguard/proguard-android.txt,
// 此外,proguard-android-optimize.txt 也为sdk默认的混淆配置,
// 但是它默认打开了优化开关。并且,我们可在配置混淆文件将android.util.Log置为无效代码,
// 以去除apk中打印日志的代码。而 proguard-rules.pro 是该模块下的混淆配置。
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
添加完这个配置以后,每次在build release包时,会在${project.buildDir}/outputs/mapping/${flavorDir}/
生成以下文件:
文件名 | 描述 |
---|---|
dump.txt | APK中所有类文件的内部结构 |
mapping.txt | 提供原始与混淆过的类、方法和字段名称之间的转换,可以通过proguard.obfuscate.MappingReader 来解析 |
seeds.txt | 列出未进行混淆的类和成员 |
usage.txt | 列出从APK移除的代码 |
我们需要仔细检查最终合并的 ProGuard 配置文件,是不是存在过度 keep 的现象。
可以在混淆配置中添加下列规则输出 ProGuard 的最终配置,尤其需要注意各种的 keep *,很多情况下我们只需要 keep 其中的某个包、某个方法,或者是类名就可以了。
# 输出 ProGuard 的最终配置
-printconfiguration configuration.txt
输出的配置中部分结果:
...
-keep class com.lutongnet.kalaok2.helper.RequestStatus {
<fields>;
<methods>;
}
-keep class com.lutongnet.kalaok2.util.RxView {
<fields>;
<methods>;
}
-keep class okhttp3.internal.** {
<fields>;
<methods>;
}
-keep class retrofit2.** {
<fields>;
<methods>;
}
...
移除无用代码
随着项目的不断迭代更新,代码量也越来越多,随之而产生的无用代码也会增加,在删除无用代码时,我们常常会因为代码太多而不敢删除,这是我们就可以使用Android studio 自带的 Lint 工具来检测无效代码,使用步骤如下:
- 点击菜单栏 Analyze
- 选择 Run Inspection by Name
- 在弹出的输入框中输入 unused declaration
- 选择 Moudule ‘app’
- 点击ok
就会自动扫描除无用代码,选择删除即可。同时,也可以通过在线下使用 Simian工具 来检查重复代码.除此之外,还有一些其他小技巧,比如:减少ENUM的使用(详情可以参考:Remove Enumerations),每减少一个ENUM可以减少大约1.0到1.4 KB的大小;移除掉所有无用或者功能重复的依赖库等。
资源瘦身优化
上面说到,在一个apk当中,占用最大的就是DEX 和资源文件,因此,资源文件的优化,就显得尤为总要,针对资源的优化,可以从以下几个方面进行:
冗余资源优化
应用通过长时间的迭代,总会有一些无用的资源,尽管它们在程序运行过程不会被使用,但是依然占据着安装包的体积。
1.使用 Lint 删除无用资源
Android Studio自带的 Lint 这个静态代码扫描工具,它里面就支持 Unused Resources 扫描。具体使用如下:
- 点击菜单栏 Analyze
- 选择 Run Inspection by Name
- 在弹出的输入框中输入 unused resources
- 选择 Moudule ‘app’ 或者 Whole project
- 点击ok
扫描出来的结果如下:
点击 **“Remove All Unsed Resources” ** 就可以将无用资源删除了,而且使用lint,还可以删除xml资源文件中部分未使用的资源,比如colors.xml中未使用的颜色等。Lint 作为一个静态扫描工具,它最大的问题在于没有考虑到 ProGuard 的代码裁剪。在 ProGuard 过程我们会 shrink 掉大量的无用代码,但是 Lint 工具并不能检查出这些无用代码所引用的无用资源。
2.优化 shrinkResources
Android的编译工具链中提供了一款资源压缩的工具,可以通过该工具来压缩资源,如果要启用资源压缩,可以在build.gradle文件中将shrinkResources true
。例如:
android {
...
buildTypes {
release {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
}
需要注意的是目前资源压缩器目前不会移除values/文件夹中定义的资源(例如字符串、尺寸、样式和颜色),所以,可以配合Lint一起使用。
图片压缩
一般来说,1000行代码在APK中才会占用 5kb 的空间,而图片呢,一般都有几十到几百kb,所以说,对图片做压缩,它的收益明显是更大的,而往往UI 设计师或开发人员如果忘记了添加图片时进行压缩,添加的就是原图,那么包体积肯定会增大很多。对于图片压缩,我们可以在 tinypng 这个网站进行图片压缩,但是如果 App 的图片过多,一个个压缩也是很麻烦的。因此,我们可以 使用 McImage、TinyPngPlugin 或 TinyPIC_Gradle_Plugin 来对图片进行自动化批量压缩。但是,需要注意的是,在 Android 的构建流程中,AAPT 会使用内置的压缩算法来优化 res/drawable/ 目录下的 PNG 图片,但这可能会导致本来已经优化过的图片体积变大,因此,可以通过在 build.gradle 中 设置 cruncherEnabled 来禁止 AAPT 来优化 PNG 图片,代码如下所示:
aaptOptions {
cruncherEnabled = false
}
此外,我们还要注意对图片格式的选择,对于我们普遍使用更多的 png 或者是 jpg 格式来说,相同的图片转换为 webp 格式之后会有大幅度的压缩。对于 png 来说,它是一个无损格式,而 jpg 是有损格式。jpg 在处理颜色图片很多时候根据压缩率的不同,它有时候会去掉我们肉眼识别差距比较小的颜色,但是 png 会严格地保留所有的色彩。所以说,在图片尺寸大,或者是色彩鲜艳的时候,png 的体积会明显地大于 jpg。
关于 Webp 图片的转化与压缩,具体可以参考 这篇文章 。
So库优化
So 是 Android 上的动态链接库,在我们 Android 应用开发过程中,有时候 Java 代码不能满足需求,比如一些 加解密算法或者音视频编解码功能,这个时候就必须要通过 C 或者是 C++ 来实现,之后生成 So 文件提供给 Java 层来调用,在生成 So 文件的时候就需要考虑生成市面上不同手机 CPU 架构的文件。目前,Android 一共 支持7种不同类型的 CPU 架构,比如常见的 armeabi、armeabi-v7a、X86 等等。理论上来说,对应架构的 CPU 它的执行效率是最高的,但是这样会导致 在 lib 目录下会多存放了各个平台架构的 So 文件,所以 App 的体积自然也就更大了。
因此,我们就需要对 lib 目录进行缩减,我们 在 build.gradle 中配置这个 abiFiliters 去设置 App 支持的 So 架构,其配置代码如下所示:
defaultConfig {
//so库兼容,大部分用armeabi即可,如需要其他再去增加
ndk {
abiFilters "armeabi"
}
}
资源混淆
同代码混淆类似,资源混淆将 资源路径混淆成单个资源的路径,这里可以使用 AndroidResGuard,它可以使冗余的资源路径变短,例如将 res/drawable/bg 变为 r/d/a。
AndroidResGuard 的使用方式如下:
-
在项目的根 build.gradle 文件下加入下面的插件依赖:
classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.17'
-
在项目 module 下的 build.gradle 文件下引入其插件:
apply plugin: 'AndResGuard'
-
加入 AndroidResGuard 的配置项,如下是默认设置好的配置:
andResGuard { // mappingFile = file("./resource_mapping.txt") mappingFile = null use7zip = true useSign = true // 打开这个开关,会keep住所有资源的原始路径,只混淆资源的名字 keepRoot = false // 设置这个值,会把arsc name列混淆成相同的名字,减少string常量池的大小 fixedResName = "arg" // 打开这个开关会合并所有哈希值相同的资源,但请不要过度依赖这个功能去除去冗余资源 mergeDuplicatedRes = true whiteList = [ // for your icon "R.drawable.icon", // for fabric "R.string.com.crashlytics.*", // for google-services "R.string.google_app_id", "R.string.gcm_defaultSenderId", "R.string.default_web_client_id", "R.string.ga_trackingId", "R.string.firebase_database_url", "R.string.google_api_key", "R.string.google_crash_reporting_api_key" ] compressFilePattern = [ "*.png", "*.jpg", "*.jpeg", "*.gif", ] sevenzip { artifact = 'com.tencent.mm:SevenZip:1.2.17' //path = "/usr/local/bin/7za" } /** * 可选: 如果不设置则会默认覆盖assemble输出的apk **/ // finalApkBackupPath = "${project.rootDir}/final.apk" /** * 可选: 指定v1签名时生成jar文件的摘要算法 * 默认值为“SHA-1” **/ // digestalg = "SHA-256" }
-
点击右边的项目 module/Tasks/andresguard/resguardRelease 即可生成资源混淆过的 APK。如下图所示:
生成的apk如下:
将优化过后的apk 拖入Android Studio 进行分析,结果如下图所示:
从上图可以看出,经过优化后,apk从原先的 22.1MB 减少到 14.2MB ,减少了 7.9MB ,幅度达到 35.75% ,而且,经过优化后,不仅对代码进行了混淆加密,而且对资源文件也进行了混淆压缩,增加了apk的反编译难度,加强了应用的安全性,由此可见,apk优化的工作还是非常有必要的。
总结
上述就是总结的目前在APK瘦身方面的做的一些尝试和积累,可以根据项目和自身情况取舍使用。当然除此之外,还有一些其他的发难比如说插件化来减少安装包的体积。最后提一点,砍掉不必要的功能才是安装包瘦身的超级大招。一个好的App的标准有很多方面,但提供尽可能小的安装包是其中一个重要的方面,这也是对我们Android开发者人员自身的提出的基本要求,要时刻保持良好的编程习惯和对包体积敏锐的嗅觉。