对 App 包大小做优化的目的,就是节省用户流量,提高用户下载速度。
App Store 规定了安装包大小超过 150MB 的 App 不能使用 OTA(over-the-air)环境下载,也就是只能在 WiFi 环境下下载。所以,150MB 就成了 App 的生死线,一旦超越了这条线就很有可能会失去大量用户。
如果 App 要再兼容 iOS7 和 iOS8 的话,苹果官方还规定主二进制 text 段的大小不能超过 60MB。如果没有达到这个标准,甚至都没法提交 App Store。而实际情况是,业务复杂的 App 轻轻松松就超过了 60MB。虽然我们可以通过静态库转动态库的方式来快速避免这个限制,但是静态库转动态库后,动态库的大小差不多会增加一倍,这样 150MB 的限制就更难守住。
而实际情况是,业务复杂的 App 轻轻松松就超过了 60MB。虽然我们可以通过静态库转动态库的方式来快速避免这个限制,但是静态库转动态库后,动态库的大小差不多会增加一倍,这样 150MB 的限制就更难守住。另外,App 包体积过大,对用户更新升级率也会有很大影响。
官方 App ThinningApp Thinning 是由苹果公司推出的一项可以改善 App 下载进程的新技术,主要是为了解决用户下载 App 耗费过高流量的问题,同时还可以节省用户 iOS 设备的存储空间。现在的 iOS 设备屏幕尺寸、分辨率越来越多样化,这样也就需要更多资源来匹配不同的尺寸和分辨率。 同时,App 也会有 32 位、64 位不同芯片架构的优化版本。如果这些都在一个包里,那么用户下载包的大小势必就会变大。App Thinning 会专门针对不同的设备来选择只适用于当前设备的内容以供下载。比如,iPhone 6 只会下载 2x 分辨率的图片资源,iPhone 6plus 则只会下载 3x 分辨率的图片资源。
苹果公司使用 App Thinning 之前, 每个 App 包会包含多个芯片的指令集架构文件。以 Reveal.framework 为例,使用 du 命令查看到主文件在 Reveal.framework/Versions/A 目录下,大小有 21MB。
rhc$ du -h Reveal.framework/* 0B Reveal.framework/Headers 0B Reveal.framework/Reveal 16K Reveal.framework/Versions/A/Headers 21M Reveal.framework/Versions/A 21M Reveal.framework/Versions
后,再使用 file 命令,查看 Version 目录下的 Reveal 文件:
rhc$ file Reveal.framework/Versions/A/Reveal
Reveal.framework/Versions/A/Reveal: Mach-O universal binary with 5 architectures: [i386:current ar archive] [arm64]
Reveal.framework/Versions/A/Reveal (for architecture i386): current ar archive
Reveal.framework/Versions/A/Reveal (for architecture armv7): current ar archive
Reveal.framework/Versions/A/Reveal (for architecture armv7s): current ar archive
Reveal.framework/Versions/A/Reveal (for architecture x86_64): current ar archive
Reveal.framework/Versions/A/Reveal (for architecture arm64): current ar archive
可以看到, Reveal 文件里还有 5 个文件:
x86_64 和 i386,是用于模拟器的芯片指令集架构文件;
arm64、armv7、armv7s ,是真机的芯片指令集架构文件。
用 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 目录,然后将图片添加进来即可.
然后,按照 Asset Catalog 的模板添加图片资源即可,添加的 2x 分辨率的图片和 3x 分辨率的图片,会在上传到 App Store 后被创建成不同的变体以减小 App 安装包的大小。而芯片指令集架构文件只需要按照默认的设置, App Store 就会根据设备创建不同的变体,每个变体里只有当前设备需要的那个芯片指令集架构文件。
使用 App Thining 后,你可以将 2x 图和 3x 图区分开,从而达到减小 App 安装包体积的目的。如果我们要进一步减小 App 包体积的话,还需要在图片和代码上继续做优化。为了减小 App 安装包的体积,还能在图片上做些什么?
无用图片资源图片资源的优化空间,主要体现在删除无用图片和图片资源压缩这两方面。而删除无用图片,又是其中最容易、最应该先做的。像代码瘦身这样难啃的骨头,我们就留在后面吧。那么,我们是如何找到并删除这些无用图片资源的呢?删除无用图片的过程,可以概括为下面这 6 大步。
通过 find 命令获取 App 安装包中的所有资源文件,比如 find /Users/rhc/Project/ -name。
设置用到的资源的类型,比如 jpg、gif、png、webp。
使用正则匹配在源码中找出使用到的资源名,比如 pattern = @"@"(.+?)""。
使用 find 命令找到的所有资源文件,再去掉代码中使用到的资源文件,剩下的就是无用资源了。
对于按照规则设置的资源名,我们需要在匹配使用资源的正则表达式里添加相应的规则,比如 @“image_%d”。
确认无用资源后,就可以对这些无用资源执行删除操作了。这个删除操作,你可以使用 NSFileManger 系统类提供的功能来完成,
可以选择开源的工具直接使用,目前最好用的是 LSUnusedResources.
图片资源压缩
对于 App 来说,图片资源总会在安装包里占个大头儿。对它们最好的处理,就是在不损失图片质量的前提下尽可能地作压缩。目前比较好的压缩方案是,将图片转成 WebP。
图片压缩工具 cwebp来将其他图片转成 WebP。cwebp 使用起来也很简单,只要根据图片情况设置好参数就行。cwebp 语法如下
cwebp [options] input_file -o output_file.webp
你要选择无损压缩模式的话,可以使用如下所示的命令:
cwebp -lossless original.png -o new.webp
如果图片大小超过了 100KB,可以考虑使用 WebP;而小于 100KB 时,可以使用网页工具 TinyPng或者 GUI 工具ImageOptim进行图片压缩。这两个工具的压缩率没有 WebP 那么高,不会改变图片压缩方式,所以解析时对性能损耗也不会增加
代码瘦身
App 的安装包主要是由资源和可执行文件组成的,所以我们在掌握了对图片资源的处理方式后,需要再一起来看看对可执行文件的瘦身方法。可执行文件就是 Mach-O 文件,其大小是由代码量决定的。通常情况下,对可执行文件进行瘦身,就是找到并删除无用代码的过程。而查找无用代码时,我们可以按照找无用图片的思路,即:
首先,找出方法和类的全集;
然后,找到使用过的方法和类;
接下来,取二者的差集得到无用代码;
最后,由人工确认无用代码可删除后,进行删除即可
LinkMap 结合 Mach-O 找无用代码
怎么快速找到方法和类的全集。我们可以通过分析 LinkMap 来获得所有的代码类和方法的信息。获取 LinkMap 可以通过将 Build Setting 里的 Write Link Map File 设置为 Yes,然后指定 Path to Link Map File 的路径就可以得到每次编译后的 LinkMap 文件了
LinkMap 文件分为三部分:Object File、Section 和 Symbols
其中:Object File 包含了代码工程的所有文件;
Section 描述了代码段在生成的 Mach-O 里的偏移位置和大小;
Symbols 会列出每个方法、类、block,以及它们的大小。
通过 LinkMap ,你不光可以统计出所有的方法和类,还能够清晰地看到代码所占包大小的具体分布,进而有针对性地进行代码优化。
得到了代码的全集信息以后,我们还需要找到已使用的方法和类,这样才能获取到差集,找出无用代码。
通过 AppCode 找出无用代码那么,有什么好的工具能够找出无用的代码吗?当代码量过百万行时 AppCode 的静态分析会“歇菜”。但是,如果工程量不是很大的话,建议直接使用 AppCode 来做分析。毕竟代码量达到百万行的工程并不多。那些代码量达到百万行的团队,则会自己通过 Clang 静态分析来开发工具,去检查无用的方法和类。用 AppCode 做分析的方法很简单,直接在 AppCode 里选择 Code->Inspect Code 就可以进行静态分析。
运行时检查类是否真正被使用过即使你使用 LinkMap 结合 Mach-O 或者 AppCode 的方式,通过静态检查已经找到并删除了无用的代码,那么就能说包里完全没有无用的代码了吗?实际上,在 App 的不断迭代过程中,新人不断接手、业务功能需求不断替换,会留下很多无用代码。这些代码在执行静态检查时会被用到,但是线上可能连这些老功能的入口都没有了,更是没有机会被用户用到。也就是说,这些无用功能相关的代码也是可以删除的。
那么,我们要怎么检查出这些无用代码呢?通过 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)
flags 采用位方式记录布尔值的方式,易于扩展、所用存储空间小、检索性能也好
既然能够在运行中看到类是否初始化了,那么我们就能够找出有哪些类是没有初始化的,即找到在真实环境中没有用到的类并清理掉。我们可以在线下测试环节去检查所有类,先查出哪些类没有初始化,然后上线后针对那些没有初始化的类进行多版本监测观察,看看哪些是在主流程外个别情况下会用到的,判断合理性后进行二次确认,最终得到真正没有用到的类并删掉。