iOS开发:Archive、ipa 和 App 包瘦身
iOS 开发的最后一步就是进行 App 的打包和分发,这里分为两个步骤:
-
Archive:对
Target
进行编译、归档,生成.xcarchive
文件。 -
Export:对 .xcarchive 归档文件进一步处理,生成不同渠道的
.ipa
包,进行分发。
作为最终会在用户手机上安装的 ipa 包,一个重要的属性就是它的占用体积,通过一些实践,我们可以有效缩减最终安装包的大小,节省下载流量,提高使用体验,有利于产品的推广。
下面就简单介绍下 archive 文件、ipa 文件的组成和分析方法,以及一些常见的 App 包瘦身思路。
了解 .xcarchive 归档
当我们在 Xcode 菜单中选择 Product -> Archive
后,编译系统就会对当前的 Xcode 工程进行分析、编译和打包,最终生成目标 Target
的一个 Archive(归档)
,我们可以在 Window -> Organizer -> Archives
页面查看到所有缓存的历史归档信息:
所谓的”归档“,就是对源码进行编译后,将此次编译生成的各种文件、资源、记录统一封装到一个地方,方便进行管理和回溯。
右键选择一个 archive,然后点击 Show in Finder
,可以看到它在 Finder 中表示为一个 .xcarchive
后缀的文件。
这个 .xcarchive 文件包含了我们的应用和它的符号表信息(symbol information)以其它的相关的资源,右键选择 显示包内容
,我们可以查看一个 Archive 归档中具体的文件结构:
其中每个文件夹的含义:
BCSymbolMaps
Xcode 对 BitCode 符号表进行混淆(Symbol Hiding)后生成的对照表,和 dSYM 文件会一一对应。
dSYMs
存储此次编译的符号表(debug symbols),用来符号化解析崩溃堆栈。
Products
存储此次编译生成的的 App 包(.app)。
要注意的是这个包虽然包括了 App 运行需要的可执行文件以及其它资源,但是和最终用户下载的版本会有所不同。后续的
export
操作会对其进行进一步处理。
SCMBlueprint
如果 Xcode 打开了版本管理(Preferences -> Source Control -> Enable Source Control),SCMBlueprint 文件夹会存储此次编译的版本控制信息,包括使用的 git 版本、仓库、分支等。
如果未来想要回溯此次编译的源码版本,可以从这个 SCMBlueprint 中找到必要的信息。
SwiftSupport
如果你在 Target 的 Build Settings
中打开了 ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES
,此次编译使用的 Swift 版本对应的标准库文件(.dylib)会被放到这个文件夹中。
发布 App 时,这些标准库也会被复制到 ipa bundle 中。
不过现在 Swift 的 ABI 已经稳定了,Xcode 10.2 及以后的版本打出来的包,在 iOS 12.2 及以后的系统的 app bundle 中不用再自带链接库了,节省了一定的体积。
了解 ipa 文件
.ipa(iOS App Store Package) 文件是最终被安装到 iPhone 上的应用格式,包含了运行 App 所必需的的签名、二进制包、资源等内容。
在 Organizer
中无论用什么方式 export
应用的安装包,最终生成的都是一个 ipa 文件。
如果要查看 ipa 中的内容,我们可以简单地把后缀名改为 .zip
然后解压,也可以用命令行进行解压:
zip -0 -y -r myAppName.ipa Payload/
观察解压以后的包,主要包含以下内容:
可执行文件
可执行文件是 ipa 的核心,占用体积也最大。
可执行文件我们可以用 lipo
命令来查看可执行文件支持的指令集:
签名文件
App 的签名信息会被放到 _CodeSignature
文件夹中。
info.plist
存储 App 主要信息的 plist 文件也会被一并打包到 ipa 中。
entitlements
entitlement 直译成中文是“权益”、“权限”的意思。
当你在 Capabilities
中开启一些特定的权限时,Xcode 会自动给你生成一个 .entitlements
文件,在这个文件中通过 xml 的格式将这些授权记录下来。
常见的权限包括:
- iCloud 存储
- Push notifications 推送通知
- Apple Pay 和 PassKit 苹果支付
- App Group
除了在 CodeSign 阶段被使用外,这个 entitlements 文件最终也会被打包到 ipa 中,在运行时供操作系统检测 App 的授权情况是否合法。
App Plugins
如果你的 App 实现了应用扩展(App Extension),扩展的包会以 .appex
的后缀存储在 PlugIns
文件夹中:
也就是说,App Extension 会跟随主 App 一起被安装到用户手机上,当然卸载的时候也是会被一起卸载。
链接库
App 运行所需要的各种链接库会被放入 Frameworks
文件夹。
资源文件
App 运行需要的各种资源文件也是 ipa 体积的大头,常见的有:
- 各种多媒体资源:图片、音视频
- xib 文件:
.nib
.storyboardc
- 各种打包的资源
.bundle
- 其它类型的资源:字体、数据库、证书等等
App 瘦身
要对 App 安装包体积进行压缩,我们首先要知道安装包占用的多少空间,这些空间由哪些部分组成,然后再进行针对性的优化。
查看最终用户安装包大小
实际上在 Xcode 本地 archive 出来的 app 包或者 export 出来的 ipa 包和最终用户下载的版本会有所不同(通常体积会大很多)。因为苹果可能会对 App 进行重新编译(如果上传了 BitCode),也会针对不同的设备型号、iOS 版本分发不同的资源(比如 2x、3x 的图片),最后还会对整个 .ipa 进行压缩,以减少从 App Store 下载时耗费的流量。
那么如何估算用户最终下载版本的包体积大小呢?其实在 iTunes Connect 页面我们可以直接查询到。
打开 iTunes Connect,选择 我的App
-> 活动
-> 所有构建版本
,然后选择一个要查看的版本:
找到 App Store 文件大小
按钮:
在弹出的列表中,可以看到在最新版本的 iOS 系统下,不同设备下载的包体积大小:
查询下载大小列表中的两列:
- 下载大小:表示通过无线下载的压缩 App 大小
- 安装大小:安装后此 App 将在用户设备上占用的磁盘空间大小
分析 App 包 Size
为了更直观地查看哪些资源占用了 App 安装包的体积,我们可以借助一些文件工具来分析解压后的 ipa 包,比如说 derlien
derlien可以很直观地看到各种不同类型文件所占的比例。
检查未使用资源
随着 App 的不断迭代,我们往往会无意间引入很多用不到的资源,或者一些资源的引用已经从代码中去除了,但是没有及时从 bundle 中删除,造成 App 包体积的浪费。
为了查找这些不再使用的资源,我们可以借助开源工具 LSUnusedResources 来检测整个工程。
[图片上传失败...(image-519b2e-1569495361068)]
针对一些特殊情况,比如代码中使用例如
[UIImage imageNamed:[NSString stringWithFormat:@"icon_tag_%d", index]]
的方式引用资源,LSUnusedResources 也支持使用正则表达式来模糊匹配。
压缩图片
图片文件是安装包中最常见的资源了,常常会占有相当一部分比例,未压缩的图片体积往往相当大,通过一些工具压缩图片资源,节省空间:
- 无损压缩:ImageOptim
- 有损压缩:tinypng
使用 Asset Catalogs 存储资源
相比于直接将图片拖入工程目录的方式,使用 Asset Catalogs
会更节省体积。Asset Catalogs 会用一个高度优化的特殊格式来存所有图片,对 png 图片也会进行最大化的压缩。
Xcode 工程模板会自动生成一个 Assets.xcassets
文件,我们也可以按需创建另外的 .xcassets
,最终在 ipa 包中,这些 xcassets 都会被压缩到 Assets.car 文件中,一定程度上也保证了安全性。
除了图片资源外,Asset Catalogs 也可以存储文本、Data 甚至 AR、apple TV 相关的资源,非常全能,所以比较好的实践就是:
能用 Asset Catalogs 管理的资源,尽量使用 Asset Catalogs 来管理
分析 LinkMap 文件
上面提到,App 包占用空间中很大一部分比例是最终编译生成的可执行文件(MACH-O),可执行文件的大小不仅和代码体积有关,也受编译器版本、编译选项、链接库、目标架构等影响。
我们可以通过分析编译时产生的 LinkMap
来了解 MACH-O 文件的组成部分。
要找到对应的 LinkMap,首先在 Xcode Target -> Build Settings -> Write Link Map File
设置为 YES,然后在 Target -> Build Settings -> Path to Link Map File
选项中设置好 LinkMap 的生成地址(一般用 build 文件夹中的默认地址就好了),archive 成功后,我们就可以在对应地址找到该次编译的 LinkMap 了:
LinkMap 记录了编译时的链接信息,用来描述可执行文件的构造成分,包括代码段 __TEXT
和数据段 __DATA
的分布情况:
网上有很多脚本可以对 LinkMap 进行分析统计,比如:
获取到分析结果后,我们可以精确了解各个模块、链接库、方法在可执行文件中的位置和占用空间:
Link Map 分析结果对于一些占比特别大的模块,常见的优化思路有:
- 寻找可替代的,小体积的依赖库,或者自己实现
- 去掉静态库中不需要的指令集,比如 armv7s,x86等,只保留发布需要的 armv7,arm64
- 提高代码重用性
- 进一步分析代码中没有被使用的方法、模块,对代码库进行精简。
- 砍需求
使用 bitcode
bitcode 是在 LLVM 体系中介于前端语言(OC、Swift、C)和后端语言(X86、ARM的机器码)之间的中间语言。
bitcode一次完整的编译(从源码到.O目标文件)包含三个主要步骤:
- 前端(Frontend):负责把各种类型的源代码编译为 bitcode 中间表示。
- 优化(Optimizer):负责对 bitcode 进行各种类型的优化,将 bitcode 代码进行一些逻辑等价的转换,使得代码的执行效率更高,体积更小。
- 后端(Backend):也叫 CodeGenerator,负责把优化后的 bitcode 编译为指定目标架构的机器码,比如 x86、arm64 等等。
我们可以在 Xcode Target -> Build Settings -> Enable Bitcode
中打开 bitcode 选项,这样在 archive 时,会将中间生成的 bitcode 嵌入到链接后的二进制文件(.o)中,用于提交到 App Store。
上面提到,bitcode 作为 LLVM 的中间语言,是可以从它直接编译出最终程序的,Apple 拿到我们上传的 bitcode 后,会使用最新的技术、编译器针对不同的终端设备重新编译 App,而这些重新编译的版本往往比我们本地 Xcode 编译的版本体积更小、效率更高。
如果后续需要支持新的平台或者有新的编译技术革新,苹果就不用依赖开发者重新上传了,直接使用现成的 bitcode 编译出船新的版本.
值得注意的是:在打包时,如果一些三方的依赖库没有开启 bitcode,或者开启了但是没有在最终引用的链接库中带有 bitcode,那么整个工程就无法用 bitcode 来编译了。
按需加载资源(On-Demand Resources)
iOS9 以后,苹果提供了 On-Demand Resources
功能来减少安装包的体积。我们可以将一些资源标记为 “按需加载”,在需要使用的时候请求操作系统从 App Store 中下载。这个功能非常适合一些大型游戏、带有付费内容或者大量不常使用的多媒体资源的 App。
[图片上传失败...(image-8b63f0-1569495361068)]
当然,按需加载只是针对 App 使用的资源文件,不包括二进制可执行文件或者源码。
On-Demand Resources 的配置可以很轻松地在 Xcode 中完成。
首先在 Target -> Resource Tags
中创建资源 tag,一个 tag 表示一组可以被独立下载的资源,后面我们就会使用这个 tag 在程序中请求操作系统下载对应的资源包到本地。
不同的 tag 包含的资源是可以重复的,App Store 会自己 differ,不会重复下载。
然后找到想要按需加载的资源文件,为它们分配一个或多个之前创建的 tag。
为资源分配 tag最后在代码中,我们可以使用 NSBundleResourceRequest:
- 请求下载 on-demand 资源
- 将资源标记为已使用状态(这样下载的资源会被清理掉,节省本地空间)
- 管理资源下载过程,配置优先级、追踪下载进度等等
- 检测磁盘容量警告
下面的代码是一个简单的资源下载请求:
// 配置要下载的 tags
NSSet *tags = [NSSet setWithObjects: @"birds", @"bridge", @"city"];
// 创建 NSBundleResourceRequest 对象
resourceRequest = [[NSBundleResourceRequest alloc] initWithTags:tags];
// 请求资源,处理回调
[resourceRequest beginAccessingResourcesWithCompletionHandler: ^(NSError * __nullable error) {
if (error) {
// 处理错误
self.resourcesLoaded = NO;
return;
}
// 下载成功,可以直接使用这些资源了
self.resourcesAvailable = YES;
}
];
下图总结了一个 on-demand 资源的生命周期:
On-demand resource life cycle总结
最近苹果取消了移动网络下载 150M 的限制,说明随着手机容量的增加和移动网络的普及,大家对 App 安装包体积不再那么敏感了,只要我们遵循一些最佳实践,一般不会在这一块有太大的问题。