前言
周末跟朋友交流 app 新功能时,发现还没更新,于是打开 app store 准备更新,发现由于包太大了(下载大于 150M),无法使用蜂窝数据下载,周围又没有 WIFI,非常尴尬。这件事让我意识到包大小对 app 运营的影响。本篇文章基于这次优化安装包大小的实践,整理了几个的可行方案,并将图片的优化做了自动化脚本 AppThinning。
IPA 安装包构成
想要优化安装包,最直接的想法就是分析下安装包的构成,做针对性的优化。
整理归类后大致如下:
- 可执行文件(Mach-O)
- 资源文件
- 图片:png、jpg、bundle、Assets.car 等
- xib、storyboard
- 其他资源:音视频、文本、网页等
- Framework
App Thinning
了解完安装包构成,接下来我们应该找些方法来解决它。下面先介绍下苹果的方案 App Thinning。
App Thinning 是 iOS 9 之后引入的一项优化,主要包括三项功能:Slicing、Bitcode、On-Demand Resources。
Slicing
根据苹果官方文献的描述「Slicing 是为应用捆绑包创建、分发不同变体以适应不同目标设备的过程。一个变体只包含针对某个目标设备的可执行架构与资源。」 换句话说,App Slicing 仅向设备传送与之相关的资源(取决于屏幕分辨率,架构等等),具体如下图所示。
需要注意的是,图片资源需要放在 Asset Catalog 中才能实现 App Slicing。
Bitcode
Bitcode 是一种程序中间码。包含 Bitcode 配置的程序将会在 App store 上被编译和链接。Bitcode 使用最新的编译器自动编译 app 并且针对特定架构进行优化。Bitcode 不会下载应用针对不同架构的优化,而仅下载与特定设备相关的优化,同时与前文所述的 App Slicing 配合实现,使得下载量更小。这部分都是在服务端自动完成的,所以假如以后 Apple 推出了新的 CPU 架构或者以后 LLVM 推出了一系列优化,我们也不再需要为其发布新的安装包了,Apple Store 会为我们自动完成这步,然后提供对应的 variant 给具体设备。
需要注意的是,开启 Bitcode 需要全部支持,包括依赖的静态库、动态库。
On Demand Resources
On-Demand Resource,就是将一部分资源放置在苹果的服务器上,不随着 App 的下载而下载,直到用户真正进入到某个页面时才下载这些资源文件。这种模式非常适合游戏的解锁关卡。
关于 On Demand Resources,目前还未实践过,这里不深入介绍,感兴趣的可以查看苹果的文档。
包实际大小
当我们上传包到 App Store Connect 后,需要等到 completed processing 后才能查看 app。而这段时间,苹果服务器帮我们做 App Thinning 的一些操作,生成各类设备对应的包。在活动-》所有构建版本中可以看到实际的下载和安装大小。
优化方案
优化包大小问题,我们很自然能想到的几个方案是:
- ”删”:删除无用文件
- “压”:压缩大文件
- “改”:改变资源获取方式、存储方式等
图片资源
图片资源占用的空间是最大的, 应优先考虑处理图片资源。
“删”
FengNiao
LSUnusedResources
需要注意的是,这类工具存在一点问题,会出现误报,不过可以作为参考,帮助找出无用文件。
“压”
压缩工具
ImageOptim 支持 PNG/JPEG/GIF 动画,本质是各种影像优化工具的图形前端:AdvPNG、OptiPNG、Pngcrush、JpegOptim、jpegtran、Gifsicle 和 PNGOUT 素材。
- 优点:无损、 小巧
- 缺点:压缩比一般、压缩速度一般
另外 ImageOptim 提供了命令行工具 ImageOptim-CLI
在线图片压缩平台,特点是速度快,压缩比高。
- 优点:压缩比高(正常可达 50% ~ 70%)、压缩速度快
- 缺点:每次最多只能处理 20 张图片、每张图片不能超过 5MB、图层较多时有可能出现像素损失情况(但概率很低)
另外 tinypng 提供了 api 调用形式,每个月免费 500 张。
经实践,tinypng 的压缩比和速度都较优秀,强烈建议优先使用 tinypng 压缩。当 tinypng 不能满足要求时,可以选择 ImageOptim。
Bundle
优先压缩 Bundle 内的图片,因为 Bundle 里面的图片不支持 App Slicing。
Assets.car
安装包中,除了在 Bundle 中的图片,其他的图片主要包含在 Assets.car 中。
Assets.car 是 Assets.xcassets 经过编译后的文件。通过分析 Assets.car 文件,可以帮助我们找出大文件。
[图片上传失败...(image-d3d4a8-1568800701765)]
AssetCatalogTinkerer 是一款开源的查看和提取 car 文件中的图片工具。
导出所有图片后,就可以根据大小排序找到大文件,进行对应的压缩处理。
assetutil 是 xcode 自带的命令行工具。使用 assetutil 分析 Assets.car 可以获取到图片更详尽的数据,比如:AssetType、Colorspace、SizeOnDisk 等。
xcrun --sdk iphoneos assetutil --info Assets.car
[
{
"AssetStorageVersion" : "IBCocoaTouchImageCatalogTool-10.0",
"Authoring Tool" : "@(#)PROGRAM:CoreThemeDefinition PROJECT:CoreThemeDefinition-346.29\n",
"CoreUIVersion" : 498,
"DumpToolVersion" : 498.4,
"Key Format" : [
"kCRThemeAppearanceName",
"kCRThemeScaleName",
"kCRThemeIdiomName",
"kCRThemeSubtypeName",
"kCRThemeDeploymentTargetName",
"kCRThemeIdentifierName",
"kCRThemeElementName",
"kCRThemePartName",
"kCRThemeDimension1Name",
"kCRThemeDimension2Name"
],
"MainVersion" : "@(#)PROGRAM:CoreUI PROJECT:CoreUI-498.40.1\n",
"Platform" : "ios",
"PlatformVersion" : "9.0",
"SchemaVersion" : 2,
"StorageVersion" : 15
},
{
"AssetType" : "Vector",
"Colorspace" : "srgb",
"Height" : 500,
"Idiom" : "universal",
"Name" : "next-joystick-progress",
"RenditionName" : "next-joystick-progress",
"SizeOnDisk" : 9324,
"Width" : 1246
},
{
"AssetType" : "Vector",
"Colorspace" : "srgb",
"Height" : 100,
"Idiom" : "universal",
"Name" : "next-success-icon-1",
"RenditionName" : "next-success-icon",
"SizeOnDisk" : 4385,
"Width" : 100
}
...
若图片较多,可导出到文本查看
xcrun --sdk iphoneos assetutil Assets.car -I -o out.text
“改”
图片管理方式
因图片资源需要放在 Asset Catalog 中才能实现 App Slicing。因此,图片尽量使用 Asset Catalog 管理。
图片格式
细心的朋友在分析 Assert.car 的时候会发现,在 Assets.xcassets 放的 pdf 文件在编译后生成了 png 文件放在 Assert.car 中。
有些尺寸较大的 pdf 文件,本身文件大小很小,但是编译生成的 png 文件却很大。
下图样式的 pdf 文件,大小为 10K,编译生成后的 png 确是 2M 多。
在 Why I don’t use PDFs for iOS assets 中也讲述了 pdf 的一些坏处。
那 pdf 就一定不好吗?
答案是否定的。苹果之所以支持 pdf,是因为向后兼容的问题。图片使用 pdf 格式,编译后自动生成 1x 和 2x 以及 3x png 图片,若以后屏幕质量提升了,出现了 4x 设备,那使用 pdf 的 app 就会生成 4x 的图自动适配,跟 BitCode 的思想如出一辙。
那何时使用 pdf 何时使用 png 呢?
pdf 生成 png 的规则是,按 pdf 的原始尺寸作为 1x 的大小。因此,尺寸较大的图片不适合使用 pdf 格式。
经实践,发现若图片是使用矢量工具直接画出来的图片(可矢量化),且尺寸较小的图片,比较适合 pdf 格式;若图片是拍摄的渲染图则推荐使用 png。
存储方式
包内不适合存放大图片,大图尽量使用网络形式获取。
其他资源
“删”
参考处理图片资源的工具,找到无用的资源文件,进行删除。
“压”
网页资源
让前端人员使用 webpack 打包并压缩代码后,再加入 iOS 工程。
音频文件
视频文件
How To Compress a Video File Without Losing Quality
可执行文件(Mach-O)
这部分的优化,主要包含两部分:
- 文件优化
- 编译优化
文件优化
- 使用 FengNiao 和 LSUnusedResources 查找并删除无用类
- 删除无用方法、引用
- 删除重复代码
工具推荐:
- fui 帮助查找无用引用。
-
PMD 静态代码扫描工具帮助查找重复代码。
-
AppCode 帮助查找无用的类、无用的方法甚至是无用的 import ,但是无法扫描通过字符串拼接方式来创建的类和调用的方法。
-
LinkMap 帮助检查到底是哪个类、哪个第三方库占用了太多空间
这部分的优化非常有限,因为类文件都是文本,对于包的体积帮助很小,但是对于 Clean Code 却显得很重要,这里就不再赘述。
编译优化
- Architectures
若项目不需要再支持 32 位,可以去除 armv7,这样可执行文件以及库的大小,必然会大大减小。
- Generate Debug Symbols
Generate Debug Symbols 决定是否生成调试符号。开启时会生成 symbols 文件,关闭时 ipa 中不会生成 symbol 文件,可以减少 ipa 大小,但会影响到崩溃的定位。
推荐:保持默认开启或者 debug 关闭、release 开启。
-
Swift Compiler - Code Generation
- Optimization Level
分为 6 个级别:
- None[-O1]:代码没有优化,编译时间最快。
- Fast[-O,O1]:适度优化,没有显著的降低编译时间,在编译过程中使用更多的内存。
- Faster[-O2]:几乎全面优化,生成高度优化的代码,编译器不执行循环展开或函数内联,编译时间较慢。
- Fastest[-O3]:全面优化,编译器执行内联函数。**这个选项通常不推荐。有关更多信息,请参见[避免过度内联函数](https://developer.apple.com/library/archive/documentation/Performance/Conceptual/CodeFootprint/Articles/CompilerOptions.html#//apple_ref/doc/uid/20001861-131770)**。
- Fastest,Smallest[-Os]:全面优化,优化程序的空间使用,通常不增加代码大小,并且更小的内存占用。
- Fastest,Aggressive Optimizations[-Ofast]:苹果文档上无详细说明。
详见[苹果文档](https://developer.apple.com/library/archive/documentation/Performance/Conceptual/CodeFootprint/Articles/CompilerOptions.html#//apple_ref/doc/uid/20001861-CJBJFIDD)
推荐:debug 模式下选择 None 保证编译速度;release 模式下选择 Fastest,Smallest[-Os]。
- Compilation Mode
有两个选项:
- Incremental:逐个文件进行优化,它的好处是对于**增量编译的项目来说,它可以减少编译时间**,对没有更改的源文件,不用每次都重新编译。并且可以充分利用多核 CPU,并行优化多个文件,提高编译速度。但它的缺点就是对于一些需要**跨文件的优化操作,它没办法处理**。如果某个文件被多次引用,那么对这些引用方文件进行优化的时候,会反复的重新处理这个被引用的文件,如果你项目中类似的交叉引用比较多,就会影响性能。
- Whole Module:将项目所有的文件看做一个整体,不会产生 Incremental 模式对同一个文件反复处理的问题,并且可以进行最大限度的优化,包括跨文件的优化操作。缺点是,不能充分利用多核处理器的性能,并且对于增量编译,每次也都需要重新编译整个项目。
推荐:debug 模式下选择 Incremental;release 模式下选择 Whole Module。
其他选项,大家可以参考苹果文档的 Managing Code Size 章节。
Framework
在 Framework 中可以看到系统的 dylib 和第三方的 framework。这部分可以根据项目的功能,查找未使用的库并删除。
如果使用了 Swift,则项目会加入 swift 的 dylib。关于这点,有些 iOSer 放弃了 swift 回退到了 oc。个人觉得这个选择并不明智,swift 有许多优秀的特性,如果仅仅因为 swift 库的大小而转向 oc 太不值当了。
自动化
基于本次实践,本人写了一个脚本 AppThinning,帮助自动找到大文件,然后进行图片压缩(imageOptim、TinyPng 可选),目前只支持图片文件的压缩,后续会加上更多类型的压缩,以将安装包大小的问题自动化。当然,更推荐大家将脚本放到持续集成中,当成集成的一部分。
总结
至此,常见的优化安装包大小的方法都已介绍完毕。大家可以根据自己项目的特点,合理选择。从本次优化的实践,个人觉得,首先应该优化图片资源,这是最简单,也是最有效的。其次,删除无用的文件、方法等不但可以减少体积,而且可以让代码变得更 Clean。持续集成中有一句老话,“如果你遇到一件很痛苦的事情,似乎比较好的建议就是更频繁地做这件事情”。所以我们不妨把 Clean Code 作为习惯,这样代码永远都是干净的,也就不需要想着什么时候优化大小的问题。最后一点,不要做一个只会码代码的程序员,学点产品思维,或许砍需求是最有效的优化安装包大小的方法。