别一上来就删图:聊聊 iOS 包体积瘦身这件事
有的人一提到包体积优化,脑子里第一反应就是删图片、清资源、换格式;也有人一上来就搬出一堆“祖传参数”,仿佛只要勾几个 Build Settings,包体积就会自己瘦下来。可问题在于,iOS 的包体积优化不是一个孤立动作,它跟资源管理、依赖治理、编译链接、交付方式,甚至和团队工程习惯都有关系。更要命的是,这个领域还有不少“历史经验”,放到今天已经半只脚踏进博物馆了。比如 Bitcode,从 Xcode 14 开始就已经被弃用,App Store 也不再接受 bitcode 提交;再比如 On-Demand Resources,它虽然还在,但苹果现在已经明确把它标成 legacy technology,并推荐迁移到 Background Assets。所以今天再写 iOS 包体积瘦身,不能只是把几年前的方案重新拼一遍,而是得先分清楚什么是还在生效的工程实践,什么只是时代眼泪。
前言:iOS 包体积瘦身还有必要吗?
我个人的看法是:有必要,而且非常有必要。
这个必要性,不是为了发版前在群里汇报一句“本周又减了 6MB”,也不是为了把优化做成一种玄学 KPI,而是因为包体积会实打实地影响几个核心问题:下载体验、安装成功率、更新成本、设备存储占用,以及团队后续维护的复杂度。苹果到现在仍然要求开发者关注构建版本的文件大小,并在 App Store Connect 中给出不同设备变体的安装大小和下载大小;如果某些变体超过 200 MB 的 over-the-air 下载警戒线,还会直接给出黄色警告。与此同时,当前 iOS / iPadOS App 的未压缩包体上限是 4 GB,可执行文件也有对应大小限制。换句话说,包体积这件事从来不是“有空再看”,而是一个和交付质量绑定的基础工程问题。
当然,包体积瘦身也不是越狠越好。为了抠出几 MB,结果把资源体系搞乱、把依赖关系搞得一团麻、把动态能力全打残,那就是典型的“为了减肥把骨头也拆了”,很勇,但不太聪明。所以在我看来,做包体积优化有三个前提:
第一,先测量,再动刀。
第二,先抓大头,再抓细节。
第三,安全优先,收益第二。
下面这篇文章,我会从这几个方向来展开:
- 如何正确测量包体积,而不是拿错对象做错误决策
- 资源层面到底该怎么瘦,哪些动作收益高,哪些动作容易翻车
- 二进制和依赖治理怎么做,哪些是真正的大头
- 今天还值得用的交付方案有哪些,哪些已经明显过时
- 最后再聊聊一个业务项目里更务实的瘦身落地流程
一、先别急着优化,先把“测量”这件事做对
很多团队做包体积优化的第一步就错了:直接拿本地 Debug 包、Archive 产物、甚至一个随手导出的 IPA 开始算大小,然后激情讨论一通,最后得出一个并不靠谱的结论。
苹果官方其实写得很明确:你本地归档出来的 app、xcarchive、上传到 App Store Connect 的 IPA,都不是用户最终拿到的那个包。因为 App Store 后续还会对构建做 App Thinning、变体分发、额外处理和重新压缩,所以如果你想拿到最准确的结果,App Store Connect 里的构建版本文件大小信息才是最终参考;而 Xcode 导出的 App Size Report 更适合做开发阶段的近似测量。TestFlight 包还会因为包含额外测试数据而略大于正式上架版本。
所以我更建议把测量分成两层:
第一层:开发态快速定位
用 Xcode 导出 App Size Report,看不同设备变体的大致下载大小、安装大小;同时打开 Link Map,用来定位二进制里到底是谁在长肉。
第二层:提测/发版态最终确认
以上传到 App Store Connect 后的构建元数据为准,看真实的变体大小、下载大小、安装大小以及警告信息。
如果只是想快速看二进制和依赖,也可以借助一些命令行工具先做基础体检:
# 查看可执行文件各段大小
xcrun size -m MyApp.app/MyApp
# 查看当前可执行文件依赖了哪些动态库
otool -L MyApp.app/MyApp
而 Link Map 依然是排查“二进制到底被谁撑胖了”的老伙计。苹果开发者论坛和文档里也一直把它当成定位链接产物构成的有效手段:打开 LD_GENERATE_MAP_FILE,去看 LD_MAP_FILE_PATH 指向的输出,就能把体积账单扒得很明白。
一句话总结这一节:不要拿错包做决策,更不要闭着眼优化。
二、资源瘦身:真正容易出效果的地方,往往不在代码里
很多项目的包体积大头,其实不是代码,而是资源。
这事挺合理。业务代码再怎么长,通常也就是几十 MB 级别;但图片、音频、视频、动画、字体、模型文件,是真的会一不留神就喂出一个“大胖小子”。
1. 资源先收口,不要到处散养
苹果官方一直推荐使用 Asset Catalog 来管理资源,而不是把图片、颜色、数据文件零散地丢在 bundle 里。原因不只是“好管理”,更重要的是,Asset Catalog 能帮助 Xcode 和 App Store 在构建与分发阶段做更高效的资源组织与优化,也更利于 app slicing。苹果还明确提到,把数据和素材从源码里挪出去、放进 asset files,本身就能降低二进制大小,并让 App Store Connect 对你的 App 做更高效的压缩。
所以,如果一个项目里还有大量 loose files,或者资源一半在 Images.xcassets,一半散落在各种目录里,第一件事通常不是“去删”,而是先收口。先把资源体系变得可管理,后面的优化才不是盲人摸象。
2. 无用资源清理,收益高,但别把自己清没了
无用资源清理几乎是包体积优化必做项,但它也是最容易翻车的一项。
原因很简单:静态扫描工具看到的是“代码里有没有显式引用”,可业务项目里有太多资源并不是直接写死的。比如字符串拼接、配置表下发、服务端动态返回名称、A/B 实验、主题换肤、Lottie/JSON 驱动、Storyboards/XIB 间接引用……这些都可能让一个“看起来没被用到”的资源,在运行时突然蹦出来。
这件事不只是资源层面会发生,类和方法扫描同样如此。戴铭在关于无用类检查的文章里就专门提到,很多静态层面有关联的类,运行时未必真的会用到;反过来,很多运行时会用到的类,静态扫描也不一定看得出来,因此需要递归分析,甚至结合线上灰度数据来提高准确性。类似地,掘金上一些实战文章也提到,遇到动态化、下发配置、xib 或 Swift 间接引用的场景时,误删风险会明显上升。
所以更稳的做法是:
- 先扫描出“疑似无用资源”
- 再结合白名单、运行时埋点、业务配置表做二次筛选
- 最后分批删除、灰度验证
别想着一键清库。那种操作方式,特别有“拿电锯修刘海”的气质。
3. 压缩和格式转换别一刀切
图片压缩当然值得做,但这里最忌讳“大一统思维”。
不是所有 PNG 都该转成别的格式,也不是所有资源都适合继续压缩。UI 图标、带透明通道的素材、拉伸图、照片类图片,它们的最优策略完全不是一回事。项目里真正应该做的,是按资源类型分治:
- UI 图标、控件切图:优先考虑矢量化、复用、合图策略是否合理
- 照片类资源:评估更高压缩比格式
- 大型音视频、模型、离线包:优先考虑不要跟主包强耦合
很多团队真正省下来的,并不是“把每张图抠 10KB”,而是把不该跟安装包绑死的内容拆出去。
4. 大资源不要硬塞主包
如果你的 App 里有大模型、离线素材、视频模板、游戏关卡资源、AI 资源包这类“大块头”,那它们就不该天然属于首包。
苹果今天对这类需求的现代答案已经比较明确:On-Demand Resources 还在,但属于 legacy technology,新的项目更应该评估 Background Assets。同时,苹果在 App Store Connect 的限制页里也把 Background Assets 作为托管大型素材的建议方案提了出来。换句话说,今天再把 ODR 当成“先进方案”硬吹,就有点年代感了;它不是不能用,而是你至少应该知道它已经不是苹果主推的路线。
三、二进制瘦身:真正难的不是“会不会配参数”,而是有没有做依赖治理
如果说资源瘦身更像“清库存”,那二进制瘦身就更像“查账”。
因为你会发现,二进制体积的增长经常不是因为某一段业务代码写多了,而是因为依赖越来越多、历史包袱越来越重、构建配置越来越随意,最后把整个可执行文件和嵌入的 framework 一层层垒起来。
1. Dead Code Stripping 这种基础项,该开的就别客气
苹果很多关于代码体积的资料里都反复强调两件事:一是发布前应剥离不需要的符号,二是可以在 Xcode 的链接设置中打开 Dead Code Stripping 来移除未使用代码。这个动作不算高深,但收益很稳定,尤其是在 C/ObjC 混编、静态链接较多、历史代码比较厚的项目里,往往是性价比很高的基础项。
但这里也要提醒一句:Dead Strip 不是魔法。凡是运行时反射、NSClassFromString、performSelector、Storyboards/XIB、动态注册、插件化入口、配置下发等路径涉及到的代码,都要非常小心。因为“看起来没被引用”不代表“运行时真的不会被用到”。
2. 第三方依赖不是“用了就完事”,而是要持续瘦身
一个业务项目里,最典型的二进制膨胀来源,往往是第三方库。
尤其是那种“为了一个小能力引进一个大组件”的情况:本来只是想要一个网络图片能力,最后拉进来一串图片编解码库;本来只是想用一个埋点 SDK,结果它顺手带来了一堆额外模块。你在业务代码里只写了一行,结果链接器在背后扛了一车东西。
所以依赖治理一定要做,而且要定期做。Link Map 的意义不只是看“谁大”,更重要的是看“谁其实不值得这么大”。
3. 动态库、静态库、静态 Framework,不要迷信单一答案
这块特别容易被写成口诀:
“动态库一定大。”
“改成静态库一定小。”
“全部静态化就赢了。”
现实没这么单纯。
苹果关于启动优化的文档明确提醒过:App 启动前,系统需要先由 dyld 加载可执行文件以及它依赖的 framework / dynamic library,因此外部依赖越多,启动链路越长。另一方面,苹果现在也支持把 framework target 配成 static framework;而从 Xcode 15 开始,客户端在链接并嵌入 static framework 时,Xcode 还会省掉那个已经静态链接进客户端的主二进制部分。
所以更准确的说法应该是:
- 只服务于主 App 的私有组件,可以认真评估静态化
- 需要被多个可执行目标共享,或者有明确分发诉求的模块,动态库可能依然合理
- 到底选哪种,不要靠口号,靠产物和 Link Map 说话
很多时候,真正让包瘦下来的不是“改成静态库”这四个字本身,而是你顺便把依赖关系、符号暴露、无用代码和资源重复一起治理了。
四、交付层面的瘦身:别忘了 App Thinning 和增量更新
很多同学一聊包体积,只盯着首包,却忽略了更新包。
这其实挺亏的。因为对于很多成熟 App 来说,用户并不是第一次下载,而是持续更新。你首包做得很漂亮,结果每个版本更新都又大又重,体验照样不会太优雅。
苹果官方在“Doing advanced optimization”里专门提到过一条很实用的建议:减少不必要的文件修改。如果某次版本迭代里,某些 bundle 文件内容没有变化,就尽量不要让它们因为构建流程、资源重新打包、时间戳抖动之类的原因被改动。你可以拿上一个版本和新版本的应用包做 diff,看看 executable、storyboard、nib、本地化资源、图片等文件里,到底是谁在“没必要地变化”。这类问题非常隐蔽,但对更新包大小的影响经常比想象中大。
另外,App Thinning 到今天依然是必须理解的基本概念。App Store 会根据设备和系统版本生成不同变体,所以一个“通用大包”并不等于用户真实下载到的版本。开发阶段可以通过 Xcode 导出 “All compatible device variants” 来看近似结果,但在今天的语境下,老文档里和 “Rebuild from Bitcode” 绑定的部分就只能当历史背景看了,不能再把 bitcode 当成主优化点来讲。
五、一个更务实的 iOS 包体积瘦身流程
说了这么多方法,最后我更想给大家一个偏工程化的落地思路。因为包体积优化这件事,最怕的不是“方法不够多”,而是“招很多,但没有流程”。
我比较推荐这样的做法:
第一步:建立基线
先记录当前版本在 App Store Connect 中的真实下载大小、安装大小,以及 Xcode App Size Report 的近似数据。这样后面每一次优化,才有可靠的对照组。
第二步:做拆账,而不是猜
把体积拆成几块来看:资源、主二进制、嵌入 framework、离线文件、其他附属内容。不要凭感觉判断“肯定是图片多”或者“肯定是 SDK 大”,先拿数据说话。Link Map、xcrun size、目录体积统计,都可以一起上。
第三步:优先打最大的点
通常排序会是这样:
大资源 > 重依赖 > 无用资源/类 > 编译链接细节 > 零碎参数
也就是说,先把 20MB、30MB 的问题处理掉,再去讨论某个参数能不能省 300KB。别把精力花在边角料上。
第四步:每次只动一类问题
删资源就专心删资源,调链接就专心调链接,改依赖结构就专心改依赖结构。这样收益可归因,回滚也更安全。否则最后包确实小了,但谁起的作用、谁埋的雷,全都说不清。
第五步:把包体积治理做进 CI
包体积不是一次性项目,而是一条会反复回弹的曲线。最好的方式不是隔半年组织一次“瘦身运动”,而是在 CI 或发版流程里做阈值报警,发现某次提交突然增长 5MB、10MB,就尽早拦下来。
六、几个今天特别容易写错的点
写这类文章的时候,我觉得有几个地方最好主动说清楚,不然特别容易把读者带沟里:
1. 不要再把 Bitcode 写成主优化方案
它今天最多只能算历史背景。Xcode 14 起 bitcode 已被弃用,App Store 也不再接受来自 Xcode 14 的 bitcode 提交。
2. 不要把 ODR 写成“最新最佳实践”
ODR 不是失效了,但苹果已经把它标成 legacy,并推荐迁移到 Background Assets。新项目至少应该优先评估后者。
3. 不要把“改动态为静态”写成银弹
它可能有效,但不是一定有效,更不是所有场景都该这么做。链接方式只是结果,依赖结构和真实产物才是关键。
4. 不要只看 IPA 大小
用户真正下载和安装的,是 App Store 处理过、按设备切分后的变体。最终判断应以 App Store Connect 元数据为准。
5. 不要无脑删除“未使用”资源和类
静态扫描只是起点,不是裁决书。运行时引用、动态下发、反射机制、XIB/Storyboard 等场景都可能导致误判。
小结
到这里,iOS 包体积瘦身这件事就差不多讲完了。
如果一定要让我用一句话概括,我会说:包体积优化不是一个技巧集合,而是一套工程治理能力。
它不是发版前的临时急救,也不是谁会几条命令就能完全拿下的事情。真正有效的瘦身,往往来自下面这些动作的组合:
- 用正确的方式测量
- 用 Link Map 和构建产物做拆账
- 先治理资源和依赖这些真正的大头
- 对二进制做基础但稳定的瘦身
- 对交付链路和更新包保持长期关注
- 用流程而不是热情维持成果
说到底,包体积从来都不是“越小越神”,而是“在保证业务能力和工程质量的前提下,尽可能小”。做到这一点,才是一个成熟项目应该追求的状态。
参考文章
下面这些资料是我整理这篇文章时重点参考,放在文末作为延伸阅读:
- 戴铭:GMTC 上分享滴滴出行 iOS 端瘦身实践的 Slides (ming1016.github.io)
- 戴铭:在快手做分享、无用类检查…… (ming1016.github.io)
- 掘金:iOS 包体积优化方案与实践(一) (掘金)
- 掘金:iOS包体积瘦身 (掘金)
- 苹果官方:Reducing your app’s size / 缩减 App 的大小 (Apple Developer)
- 苹果官方:App Store Connect 中查看构建版本及其元数据 (Apple Developer)
- 苹果官方:On-demand resources size limits (Apple Developer)
- 苹果官方:Build size limits / 构建版本的文件大小要求 (Apple Developer)
- 苹果官方:Reducing your app’s launch time / Creating a static framework 用(Apple Developer)