小编说:随着移动端产品功能的逐渐增加,APP 的体积也不可避免地呈现上升趋势,如果不加以重视,几个版本迭代下来,可能你的 APP 体积会达到用户不能忍受的程度。
如果你是 SDK 开发者,你的 SDK 包大小是用户决定是否采用的关键因素;如果你的APP 想要预装到某款手机或者某款 Android 系统中,APP 的体积也会受到很严格的限制。
因此,APP 的瘦身是每个移动端产品都会遇到的一个普遍问题,本文选自《Android高级进阶》将从不同的角度切入,全面介绍APP 瘦身相关知识。。
- APP 为什么变胖了
在 Android 出现的最初几年时间里,除了游戏,我们很难看到动辄十兆或者几十兆的APP,但现在你只要到应用市场上去走一圈,就能发现十兆以上大小的应用比比皆是,究其原因,主要有以下几种。
>>随着 Android 系统版本的碎片化发展以及手机类型的极大丰富,每个 APP 要支持的主流 dpi 分类越来越多,从最初的 ldpi、mdpi、hdpi,到后来的 xhdpi、xxhdpi、xxxhdpi、tvdpi 等。
>>随着 Android 生态系统的不断发展成熟,出现了很多方便开发者的函数库和 SDK,随着引入的函数库和 SDK 数量的增多,不可避免的引入很多重复功能代码以及资源文件。
>>用户对 APP 视觉要求的不断提高,APP 提供的资源细节越来越丰富,占用的体积也不断上升。
- 从 APK 文件的结构说起
到应用市场上面随便下载一个 APK 文件,由于 APK 本身是一个压缩文件,因此我们可以将后缀名由 .apk 改为 .zip,然后解压该文件,一般情况下,如果开发者在发布 APK 时没有对其进行加固等特殊处理,我们应该能够得到如下图所示的文件夹内容。
在进一步分析如何给 APP 瘦身之前,我们首先来了解下一个 APK 文件所包含的东西,这样才能有的放矢地进行优化。
>> AndroidManifest.xml:Android 项目的系统清单文件,用来控制 Android 应用的名称、桌面图标、访问权限等全局属性。此外,Android 应用的四大组件 Activity、Service、BroadcastReceiver 和 ContentProvider 也需要在这个文件中声明和配置。
>> assets:该目录用来存放需要打包到 Android 应用程序的静态资源文件,例如图片资源文件、JSON 配置文件、渠道配置文件、二进制数据文件、HTML5离线资源文件等。与res/raw 目录不同的是,assets 目录支持任意深度的子目录,同时该目录下面的文件不会生成资源ID。下图所示是某个 APP 的 assets 目录内容,可以一窥该目录的作用。
>> classes.dex:应用程序的可执行文件。Android 代码都打包在这种类型的文件中,可以通过反编译工具反编译后进行查看,在上图中可以看到,这个 APP 有 classes.dex、classes2.dex 和 classes3.dex 三个 dex 文件,这是因为这个 APP 的方法数已经超过 65K 的限制,需要进行分包,对于一般的 APP 来说,方法数目没有超过 65K,那么打包后只会存在一个 classes.dex 文件。
>> lib:该目录存放的是应用程序依赖的不同 ABI 类型的 .so 文件,如图所示。
>> res:该目录存放的都是应用的资源文件,包括图片资源、字符串资源、颜色资源、尺寸资源等,这个目录下面的资源都会出现在资源清单文件 R.java 的索引中。
>> resources.arsc:资源索引表,用来描述具有ID值的资源的配置信息。
>>META-INF:该目录存放的是签名相关的信息,用于验证 APK 包的完整性以及保证系统的安全。主要包含三个文件:
① MANIFEST.MF:主要存放 APK 包中每个文件的名字及每个文件的 SHA1 哈希值,内容如下。
② CERT.SF:通常每个 APP 会有一个特定的名字,例如 BDMOBILE.SF、NETDISK_.SF等, 它保存的是 MANIFEST.MF 的哈希值以及 MANIFEST.MF 文件中每一个哈希项的哈希值。
③ CERT.RSA:这个文件保存了 APK 包的签名和证书的公钥信息。
从上面的分析可以看出,一个 APK 包的组成中,影响到最终的 APK 包大小的文件可分为三种类型,分别如下。
>> Java 代码文件:classes*.dex。
>> Native 代码文件:lib目录下面的 .so 文件。
>> 资源文件:包括 assets 目录、res 目录以及 resources.arsc 索引表文件。
下面介绍的 APP 瘦身方法都是基于如何减少以上三种类型文件的大小得出的方案。
- 优化图片资源占用的空间
目前移动端 Android 平台原生支持的图片格式主要有:JPEG、PNG、GIF、BMP 和 WebP ( 自从 Android 4.0 开始支持),但是在 Android 应用开发中能够使用的编解码格式只有其中的三种:
JPEG、PNG、WebP,这可以通过查看 Bitmap 类的 CompressFormat 枚举值来确定。
代码语言:javascript复制public static enum CompressFormat {
JPEG,
PNG,
WEBP;
private CompressFormat() {
}
}
如果要在应用层使用 GIF 格式图片,那么需要自己引入第三方函数库进行支持。在对应用中的图片资源进行压缩和优化之前,我们有必要对这几种常见的图片格式做一个简单的了解和区分。
JPEG:JPEG(发音为/jay-peg/)是一种广== 泛使用的有损压缩图像的标准格式,它不支持透明和多帧动画,一般摄影类作品最终都是以 JPEG 格式展示。通过控制压缩比,可以调整图片的大小。
>> PNG:PNG 是一种无损压缩图片格式,它支持完整的透明通道,从图像处理领域讲,JPEG 只有 RGB 三个通道,而 PNG 有 ARGB 四个通道。由于是无损压缩,因此 PNG 图片一般占用空间比较大,会无形中增加最终 APP的大小,我们在做 APP 瘦身时一般都要对 PNG 图片进行处理以降低其大小。
>> GIF:GIF 是一种古老的图片格式,它诞生于 1987 年,随着初代互联网流行开来。它的特点是支持多帧动画。大家对这种格式肯定不陌生,社交平台上面发送的各种动态表情,大部分都是基于 GIF 来实现的。
>> WebP:相比前面几种图片格式,WebP(发音为 /weppy/)算是一个初生儿了,它由Google在 2010 年发布,它支持有损和无损压缩、支持完整的透明通道、也支持多帧动画,是一种比较理想的图片格式。目前国内很多主流 APP 都已经应用了 WebP,例如微信、微博、淘宝等。在既保证图片质量又要限制图片大小的需求下,WebP 应该是首选。
目前无论 Android 平台还是 iOS 平台,大多数 APP 在搭建界面时使用的几乎都是 PNG 格式图片资源,除非你的项目已经全面支持 WebP 格式,否则你都会面临对 PNG 图片瘦身的要求。
在这里,我们可以通过几个工具对 PNG 图片进行压缩来达到瘦身的目的。
1 . 无损压缩 [ImageOptim]
ImageOptim 是一个无损的压缩工具,它通过优化 PNG 压缩参数,移除冗余元数据以及非必需的颜色配置文件等方式,在不牺牲图片质量的前提下,既减小了 PNG 图片占用的空间,又提高了加载的速度。
2 . 有损压缩 [ImageAlpha]
ImageAlpha 是 ImageOptim 作者的一个有损的 PNG 压缩工具,相比较而言,图片大小得到极大的减低,当然同时图片质量也会受到一定程度的影响,经过该工具压缩的图片,需要经过设计师的检视才能最终上线,否则可能会影响到整个 APP 的视觉效果。
3 . 有损压缩 [TinyPNG]
TinyPNG 也是比较知名的有损 PNG 压缩工具,它以 Web 站点的形式提供,没有独立的APP 安装包,同所有的有损压缩工具一样,经过压缩的图片,需要经过设计师的检视才能最终上线,否则可能会影响到整个 APP 的视觉效果。
当然还有很多无损压缩工具,例如 [JPEGMini]4、[MozJPEG]5 等,大家可以从中选择适合自己项目的一个就行,主要是在图片大小和图片质量之间找到一个折衷点。
4 . PNG/JPEG 转换为 WebP
如果你的APP 最低支持到 Android 4.0,那么可以直接使用系统提供的能力来支持 WebP,如果是 4.0 以下的系统,也可以通过在 APP 中集成第三方函数库,例如 [webp-android-backport]来实现对 WebP 的支持。
根据 Google 的测试,无损压缩后的 WebP 比 PNG 文件少了 45% 的文件大小,即使这些PNG 文件经过其他压缩工具例如 ImageOptim 压缩之后,WebP 依然可以减少约 28% 的文件大小。
需要注意的是,对于具有 Alpha 通道的 PNG 图片来说,如果需要在 Android 4.2.1 之前的系统上运行,那么不能转换成 WebP 格式,因为只有在 Android 4.2.1 以上的系统中,才能解析具有 Alpha 通道的 WebP 图片。
WebP 转换的工具可以选择 [智图] 和 [iSparta]等。
5 . 尽量使用 NinePatch 格式的 PNG 图
.9.png 图片格式简称 NinePatch 图,本质上仍然是 PNG 格式图片,它是针对 Android 平台的一种特殊 PNG 图片格式,可以在图片指定位置拉伸或者填充内容。NinePatch 图的优点是体积小、拉伸不变形、能够很好的适配 Android 各种机型。Android SDK 自带了 NinePatch 图的编辑工具,位于 sdk/tools/draw9patch 中,点击即可启动。当然,Android Studio 也集成了 PNG转 NinePatch 的功能,我们只需右键点击某个需要转换的PNG 图片,在弹出的对话框中选择Create 9-Patch File... 即可自动完成转换。
最后我们以 TinyPng 为例来直观地观察压缩工具对图片的压缩效果。
- 使用 Lint 删除无用资源
Proguard 只会对 Java 代码起作用,对于 res/drawable* 目录中的图片,如果没有使用到,Proguard 只会移除该图片在 R 类中的引用,不会删除该图片。这时就需要用到 Android Lint了。Android Lint 天然集成在Android Studio 中,它会分析 res 目录下面的资源文件,但不会分析 assets 目录下面的资源文件。具体使用方法可以参考本书第49 章的相关介绍。对工程执行Android Lint,结果出来后,查看 Android Lint-Unused resources 选项即可得到哪些资源是多余的。当然,我们不能过度依赖工具,还需要人工确认是否真的是多余的,例如某些资源是通过 Java 反射机制来使用的,这时 Android Lint 还是会检查出该资源没有被使用到。
- 利用 Android Gradle 配置
在 Android Studio 工程的 app/build.gradle 文件中进行一些配置可以进一步缩减最终生成的APK 大小,它们分别如下。
1 . minifyEnable
标识是否开启 Proguard 混淆,设置为 true 时需要同时设置 Proguard 配置文件名和规则。Proguard 的作用不仅仅是混淆,它还具有压缩、优化等功能。它会遍历所有代码并找出没有引用到的代码,这些无用代码在生成最终的 APK 文件之前会被剔除掉。同时 Proguard 会使用简短的字母组合替换原来的类名、属性名等,这些都能在一定程度上减少 APP 的大小。
2 . shrinkResources
标识是否去除无用的resource 文件。需要注意的是,shrinkResources 依赖于minifyEnable, 需要 minifyEnable 设置为 true 时才会生效。同时,shrinkResources 需要慎重使用,某些资源是通过反射获取的,这类资源可能也会被删除掉,从而在运行时会报Resources$NotFoundException 异常。
以上两种方式的代码示例如下。
代码语言:javascript复制android {
// 省略其他
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile(‘proguard-android.txt’),
‘proguard-rules.pro’
}
}
}
3 . resConfigs
Android 开发过程中不可避免的会引入第三方开源函数库或者 SDK,在不修改它们的前提下,在最终生成的 APK 中,我们可能会引入很多其实不需要使用到的资源文件,主要可以分为两种。
>> DPI 目录:Android 从出现到现在,历经了多个版本,支持多种不同类型的设备,屏幕密度、屏幕形状、屏幕大小等都差别很大,支持的屏幕密度就有 ldpi、mdpi、hdpi、xhdpi、xxhdpi、xxxhdpi 等多种类型。在实际项目开发中,我们当然不可能为每一种屏幕密度提供对应的一套资源文件,这不仅没必要而且会显著增加 APP 的体积,我们需要调研产品的目标用户以及目前市场上主流的手机设备屏幕密度,满足这些用户和设备即可。因此需要剔除引用的第三方函数库或者 SDK 中可能存在的不需要的 DPI 目录及其文件。
>> 国际化文件:Android 开发中不可避免地会引用外部的函数库,这些函数库是为了全世界开发人员服务的,不可避免地会存在很多国际化文件,而对于普通的 APP 来说,可能只需要支持本国语言就可以了。因此,也需要剔除无用的国际化文件。
在 Android Studio 中,我们可以通过在 Gradle 中进行配置,来选择最终打包到 APK 中的资源,这一步是通过指定 resConfig 或者 resConfigs 的取值,通过如下 DSL(Domain SpecificLanguage)防止 AAPT(Android Asset Packaging Tool)打包不需要的资源。
代码语言:javascript复制android {
// 省略其他
defaultConfig {
// 省略其他
resConfigs “en”, “fr”
resConfigs “nodpi”, “hdpi”, “xhdpi”, “xxhdpi”, “xxxhdpi”
}
4 . ndk.abiFilters
在工程的 build.gradle 文件中增加 ndk.abiFilters 配置,可以指定我们需要的 ABI 类型,从而可以过滤掉不需要的 ABI 类型的 .so 文件。
代码语言:javascript复制android {
// 省略其他
defaultConfig {
// 省略其他
ndk {
abiFilters “armeabi-v7a”, “x86”
}
}
6 . 重构和优化代码
保持良好的编码习惯,对你的代码进行持续的优化或者重构,减少重复代码,实现代码复用,这方面可以多读一读 Martin Fowler 的《重构- 改善既有代码的设计》 一书。在项目开发中,建议抽出一个基础库,提供基础的功能,例如网络、数据库、加解密、utils 工具包等,实现不同模块间复用基础的功能,甚至在公司层面维护一个公共库,在不同产品线之间共享。这样如果不同产品之间需要相互集成,复用一套公共库,能在很大程度上减少重复的代码。
7 . 资源混淆
资源文件的混淆方案目前有 [ 美团] 和 [ 微信] 两种,前者是通过修改 AAPT 在处理资源文件相关的源码达到资源文件名的替换;后者是通过直接修改 resources.arsc 文件达到资源文件名的混淆。相比之下,微信的方案更优,而且微信已经将这个方案开源出来,地址在[AndResGuard],感兴趣的同学通过上面的 README.md 介绍可以很容易集成到自己的项目中。
8 . 插件化
插件化开发也是减少 APP 体积的一个可行的途径,不过首先你需要实现一个插件化的框架,用来在线动态的下载并加载各个插件。
最后,连APP都开始瘦身了,你还有什么理由不努力~