
官方 App Thinning
无用图片资源
图片资源压缩
代码瘦身
通过 AppCode 找出无用代码
运行时检查类是否真正被使用过
对 App 包大小做优化的目的,就是节省用户流量,提高用户下载速度。
官方 App Thinning
App Thinning 是由苹果公司推出的一项可以改善 App 下载进程的新技术,主要是为了解决用户下载 App 耗费过高流量的问题,同时还可以节省用户 iOS 设备的存储空间。
现在的 iOS 设备屏幕尺寸、分辨率越来越多样化,这样也就需要更多资源来匹配不同的尺寸和分辨率。 同时,App 也会有 32 位、64 位不同芯片架构的优化版本。如果这些都在一个包里,那么用户下载包的大小势必就会变大。
App Thinning 会专门针对不同的设备来选择只适用于当前设备的内容以供下载。比如,iPhone 6 只会下载 2x 分辨率的图片资源,iPhone 6plus 则只会下载 3x 分辨率的图片资源。
使用 App Thinning 后,用户下载时就只会下载一个适合自己设备的芯片指令集架构文件。
App Thinning 有三种方式,包括:App Slicing、Bitcode、On-Demand Resources。
- App Slicing,会在你向 iTunes Connect 上传 App 后,对 App 做切割,创建不同的变体,这样就可以适用到不同的设备。
- On-Demand Resources,主要是为游戏多关卡场景服务的。它会根据用户的关卡进度下载随后几个关卡的资源,并且已经过关的资源也会被删掉,这样就可以减少初装 App 的包大小。
- Bitcode ,是针对特定设备进行包大小优化,优化不明显。
那么,如何在你项目里使用 App Thinning 呢?
其实,这里的大部分工作都是由 Xcode 和 App Store 来帮你完成的,你只需要通过 Xcode 添加 xcassets 目录,然后将图片添加进来即可。
无用图片资源
图片资源的优化空间,主要体现在删除无用图片和图片资源压缩这两方面。而删除无用图片,又是其中最容易、最应该先做的。像代码瘦身这样难啃的骨头,我们就留在后面吧。那么,我们是如何找到并删除这些无用图片资源的呢?
删除无用图片的过程,可以概括为下面这 6 大步。
- 通过 find 命令获取 App 安装包中的所有资源文件,比如 find /Users/daiming/Project/ -name。
- 设置用到的资源的类型,比如 jpg、gif、png、webp。
- 使用正则匹配在源码中找出使用到的资源名,比如 pattern = @"@"(.+?)""。
- 使用 find 命令找到的所有资源文件,再去掉代码中使用到的资源文件,剩下的就是无用资源了。
- 对于按照规则设置的资源名,我们需要在匹配使用资源的正则表达式里添加相应的规则,比如 @“image_%d”。
- 确认无用资源后,就可以对这些无用资源执行删除操作了。这个删除操作,你可以使用 NSFileManger 系统类提供的功能来完成。
如果你不想自己重新写一个工具的话,可以选择开源的工具直接使用。我觉得目前最好用的是 LSUnusedResources,特别是对于使用编号规则的图片来说,可以通过直接添加规则来处理。
图片资源压缩
对于 App 来说,图片资源总会在安装包里占个大头儿。对它们最好的处理,就是在不损失图片质量的前提下尽可能地作压缩。目前比较好的压缩方案是,将图片转成 WebP。WebP 是 Google 公司的一个开源项目。
首先,我们一起看看选择 WebP 的理由:
- WebP 压缩率高,而且肉眼看不出差异,同时支持有损和无损两种压缩模式。比如,将 Gif 图转为 Animated WebP ,有损压缩模式下可减少 64% 大小,无损压缩模式下可减少 19% 大小。
- WebP 支持 Alpha 透明和 24-bit 颜色数,不会像 PNG8 那样因为色彩不够而出现毛边。
代码瘦身
App 的安装包主要是由资源和可执行文件组成的,所以我们在掌握了对图片资源的处理方式后,需要再一起来看看对可执行文件的瘦身方法。
可执行文件就是 Mach-O 文件,其大小是由代码量决定的。通常情况下,对可执行文件进行瘦身,就是找到并删除无用代码的过程。而查找无用代码时,我们可以按照找无用图片的思路,即:
- 首先,找出方法和类的全集;
- 然后,找到使用过的方法和类;
- 接下来,取二者的差集得到无用代码;
- 最后,由人工确认无用代码可删除后,进行删除即可。
通过 AppCode 找出无用代码
用 AppCode 做分析的方法很简单,直接在 AppCode 里选择 Code->Inspect Code 就可以进行静态分析。静态分析完以后,我们可以在 Unused code 里看到所有的无用代码。
运行时检查类是否真正被使用过
通过 ObjC 的 runtime 源码,我们可以找到怎么判断一个类是否初始化过的函数
#define RW_INITIALIZED (1<<29)
bool isInitialized() {
return getMeta()->data()->flags & RW_INITIALIZED;
}
isInitialized 的结果会保存到元类的 class_rw_t 结构体的 flags 信息里,flags 的 1<<29 位记录的就是这个类是否初始化了的信息。而 flags 的其他位记录的信息,你可以参看 objc runtime 的源码,如下:
// 类的方法列表已修复
#define RW_METHODIZED (1<<30)
// 类已经初始化了
#define RW_INITIALIZED (1<<29)
// 类在初始化过程中
#define RW_INITIALIZING (1<<28)
// class_rw_t->ro 是 class_ro_t 的堆副本
#define RW_COPIED_RO (1<<27)
// 类分配了内存,但没有注册
#define RW_CONSTRUCTING (1<<26)
// 类分配了内存也注册了
#define RW_CONSTRUCTED (1<<25)
// GC:class 有不安全的 finalize 方法
#define RW_FINALIZE_ON_MAIN_THREAD (1<<24)
// 类的 +load 被调用了
#define RW_LOADED (1<<23)