摘要
包体优化表面上是在“减几 MB”,但真正做起来,背后牵扯的是资源治理、依赖收敛、分发策略、性能权衡和工程化机制。这里结合一套相对成体系的思路,梳理一下我对 iOS 包体优化的理解:先量化,再治理;先抓大头,再谈细节;最后把专项动作沉淀成持续能力。
一次 iOS 包体优化的小记
包体优化这个话题,很多时候容易被说成一些零散动作:压缩图片、删无用资源、开 -Osize、清理死代码。
这些动作当然都没错,但如果只停留在这一层,事情通常做不深。因为真实项目里的包体问题,往往不是某一个点造成的,而是长期迭代之后,资源、依赖、二进制、分发方式和工程机制一起堆出来的结果。
所以这几年我越来越倾向于把包体优化看成一个工程治理问题,而不是一个“技巧清单”。
如果让我概括这件事,我更习惯按下面四步来做:
- 先量化,建立基线
- 再按 ROI 排优先级,优先处理大头
- 中间讲清楚 trade-off,避免负优化
- 最后接入工程化机制,防止回涨
这篇随笔,主要就是顺着这条线,梳理一下我对 iOS 包体优化的一些实践理解。
先量化,而不是一上来就谈方案
我一般不会一开始就说“先压图”或者“先删资源”。
第一步一定是先回答两个问题:
- 当前包体到底由什么构成
- 这次优化的目标到底是什么
这两个问题看起来很基础,但其实很关键。
因为有时候业务真正关心的是 App Store 下载大小,比如要控制在某个下载门槛内,或者希望提升弱网场景下的首装转化;有时候更关心的是 安装大小,因为它会影响用户设备空间占用和长期体验。两者相关,但不完全相同,优化手段的优先级也会有差异。
在量化阶段,我通常会做几件事:
- 拆 IPA,看资源、可执行文件、动态库、配置文件分别占多少
- 用
LinkMap、xcrun size、otool -l看二进制内部段分布和目标文件占比 - 结合 App Store Connect 的下载大小和安装大小,明确优化目标
- 对资源按类型聚类,区分图片、音视频、字体、多语言、配置文件
这一步做完之后,事情通常会变得很清楚。
因为很多时候,真正的大头并不是“代码写多了”,而是下面这几类:
- 资源膨胀
- 重依赖或历史依赖包袱
- 编译与链接配置不够收敛
如果没有这一步量化,后面的优化就很容易变成拍脑袋。团队会忙很多动作,但收益未必集中。
资源治理,通常是 ROI 最高的一层
如果问我包体优化里最容易拿到明显收益的是什么,我通常会先看资源。
尤其是迭代时间比较久的业务项目,资源很容易在长期开发中自然膨胀。活动图、历史素材、重复资源、散落资源、不再使用的字体和视频,都会慢慢堆进主包。
图片:不是“全量压缩”,而是先抓大头
图片通常是第一个入口,但图片治理也最容易走偏。
真实项目里,图片优化不太适合“一刀切”地全量压缩一遍。更稳妥的方式通常是:
- 导出图片清单,按大小排序,看 Top N
- 看哪些图片没有进入
Asset Catalog - 看是否存在重复图片、历史活动残留图片、不同模块重复拷贝的素材
- 按类型分层处理:图标、透明 UI 图、照片类、插画、大图背景分别采用不同策略
我比较倾向的一条原则是:资源治理优先做 大头 + 低风险 + 易落地 的项。
所以很多时候,第一波动作不是研究多复杂的格式,而是:
- 把散落在 Bundle 里的图片收口到
Asset Catalog - 清理重复资源和无用资源
- 清理历史活动图片
- 对 Top 大图做抽样压缩和格式评估
图片进入 Asset Catalog 的意义,不只是资源管理更规范。更重要的是,它能更好地配合 App Slicing,让用户只下载当前设备需要的那部分资源,这对下载包大小会有比较直接的帮助。
至于图片格式本身,我一般不会只看“谁更小”,而会一起看这几个维度:
- 是否支持透明通道
- 画质是否能接受
- 解码性能是否合适
- 是否影响首屏和列表滚动
- 工具链和设计导出流程是否可持续
也就是说,图片优化真正要平衡的是 包体、画质、性能、兼容性,不是单点追求最小。
音视频:收益明显,但更要关注体验边界
音视频资源的体积通常比较大,所以收益往往也更直接。
但这类资源的优化,通常不能只看“能不能放到服务端”,而要先看两个问题:
- 它是不是首屏或主链路强依赖
- 它是不是低频访问
如果是首屏依赖资源,就不太适合简单外放,更多还是要从编码格式、码率、时长、分辨率这些方向优化。
如果是低频、非核心资源,比如教程视频、引导动画、某些活动插画,那就可以评估 On Demand Resources 或服务端下发。
这里比较关键的一点是:资源外放并不是没有代价。
它能减轻首包压力,但也可能带来这些问题:
- 首次进入页面需要等待下载
- 弱网环境下拉取失败
- 缓存和版本兼容变复杂
- 离线场景不可用
所以这类优化更适合先分层、再局部试点,而不是全量铺开。
字体:经常被忽略,但常常很“值”
字体是一个很容易被忽视的点,尤其是中文字体文件,体积往往不小。
如果业务确实需要自定义字体,我通常会先问几个问题:
- 这套字体是不是必须的
- 是否所有页面都要用
- 是否真的需要完整字集
- 能不能做子集化
- 能不能按语言或场景拆分
很多时候,一个品牌字体实际只用在少量标题或者局部视觉场景,这时候直接内置完整字库,其实性价比并不高。相对来说,做 subset 往往更符合收益预期。
无用资源清理:典型的高 ROI 动作
这类动作通常收益不小,而且改造风险相对可控。
常见做法包括:
- 借助
LSUnusedResources、FengNiao这类工具做初筛 - 由工具先给候选集,而不是直接删除
- 结合模块负责人确认动态引用、配置化引用、字符串拼接引用
- 分批删除、分批回归
这里一个很现实的经验是:工具适合“发现问题”,不适合直接“裁决问题”。
因为 iOS 工程里常常会有反射、KVC、桥接调用、动态资源名等情况,静态扫描结果并不总是可靠。真正落地时,还是要有人做业务确认。
依赖和二进制,往往比想象中更集中
除了资源,另一个高收益点通常是依赖治理。
很多项目迭代久了之后,都会出现一些典型问题:
- 为一个小功能引入整个重库
- 传递依赖过多
- 不同模块引入了重复能力
- 历史库没人敢删,但其实已经很少使用
这类问题平时开发感知不一定强,但一旦结合 LinkMap 做聚合,往往很容易找到几个包体大户。
我自己更习惯把 LinkMap 当成体积归因工具,而不只是“看大小”的工具。它更重要的价值是帮助回答下面这些问题:
- 哪个库最大
- 哪个模块最大
- 哪个目标文件异常大
- 是不是某些符号或链接参数把整库都带进来了
例如 -ObjC 就是一个很典型的点。它在某些场景下确实必要,但也可能把静态库里的 ObjC 类和分类全量拉进最终产物,造成明显膨胀。所以更稳妥的做法通常不是“默认保留”,而是确认它到底是不是必须,有没有更精确的替代方式,比如局部 force_load。
至于编译和链接优化本身,我一般会把它理解成 Release 构建的基础卫生项,包括:
-Osize:以减小代码体积为目标的优化选项。相比更激进地追求运行时性能,它会更倾向于收敛生成代码大小,适合对包体敏感的业务 App。Whole Module Optimization:把整个 Swift Module 放在一起做统一优化,而不是按单文件独立优化。它的价值在于让编译器获得更完整的上下文,做更有效的内联、冗余消除和代码收敛。LTO:Link Time Optimization,也就是在链接阶段再做一轮跨文件、跨目标文件优化。它可以进一步消除冗余代码、合并重复逻辑,让最终产物更紧凑。Dead Code Stripping:删除最终二进制里未被引用的符号和代码段,减少“编进来了但实际上没有被使用”的内容,是 Release 包体优化里很基础也很有效的一项。Strip Linked Product:对最终产物做符号剥离,去掉不需要随 App 一起发布的符号信息,直接减小可执行文件体积。Strip Debug Symbols During Copy:在拷贝阶段移除调试符号,避免无关的调试信息进入最终安装包,进一步压缩发布产物大小。
这些配置都应该开,而且长期来看是有收益的。
但如果从专项 ROI 来说,它们通常不是第一优先级,更像是资源和依赖治理之后的“收尾增强”。
它们的共同点,是都在做二进制层的“收敛”;但本质上解决的问题并不完全一样:有的是在编译阶段减少代码膨胀,有的是在链接阶段继续做全局优化,有的是把最终产物里不需要带上的符号和调试信息剥离掉。
不过这类优化也不是“开了就一定大幅下降”。尤其在 ObjC 工程里,动态派发、Category、反射以及 -ObjC 等链接参数,都会影响死代码剥离的实际收益。所以它们更适合作为基础收敛手段,而不是包体专项的唯一抓手。
换句话说,在大多数业务 App 里,真正收益最大的往往不是研究编译参数,而是先把资源和依赖的大头治理掉。
比“会列手段”更重要的,是讲清楚 trade-off
如果说包体优化最容易把人区分开,我觉得不是谁知道更多术语,而是谁更清楚每种方案的边界和代价。
动态库拆分,不等于包体优化
动态库拆分经常会被误认为是包体优化手段,但更准确一点说,它更偏向:
- 架构边界治理
- 按需加载
- 模块解耦
- 启动阶段负载拆分
它并不一定能减少总安装大小。相反,因为每个 Mach-O 都有额外的 header、load commands、符号表和 dyld 装载成本,拆得不合适时,体积和加载开销反而可能增加。
所以如果当前目标是“压安装包”,我通常不会把动态库拆分当成核心抓手。除非这个模块本身满足几个条件:
- 访问频率低
- 边界清晰
- 能延迟加载
- 不属于首屏强依赖
- 拆出后不会带来新的复杂耦合
也就是说,动态库拆分不是不能做,而是要明确它优化的到底是什么。
如果目标是包体,就要谨慎;如果目标是模块治理和加载时机管理,它的价值就会更大。
图片压缩,也不能只看文件大小
图片优化里最常见的误区,就是只看“磁盘上更小”。
但客户端资源不是存下来就结束了,它最终还要经历下载、解码、渲染、缓存、重复展示这些过程。所以我更习惯把图片优化分成两个视角来看:
存储侧:文件大小、下载包大小、安装包大小运行时:解码耗时、CPU、内存、首屏时间、滚动帧率
有些格式或者压缩策略,在磁盘上确实更小,但如果它刚好用于首屏或者高频列表里,可能会把解码压力放大。那从业务结果看,未必是真正的优化。
所以更稳妥的方式通常是:
- 对 Top 大图做样本实验
- 横向比较压缩率、画质、透明通道支持和解码表现
- 先在低风险页面或低频场景试点
- 观察页面渲染和弱网下载表现,再决定是否推广
很多时候,包体优化里的 trade-off,不是“我知道有风险”,而是“我知道该怎么用数据判断值不值得做”。
分发策略,优化的不只是包体数字
当我们把包体问题从“文件大小”扩展到“用户下载与安装体验”时,系统分发能力的重要性就会变得更明显。
App Thinning / App Slicing
这是一个很典型的系统能力。
如果资源管理方式不规范,很多切片收益其实是吃不到的。
所以,把图片和其他资源规范地收口到合适的资源体系里,本身就是一种“为分发优化做准备”的动作。它不一定像“删掉几十张图”那样立刻可见,但从下载包角度看,价值往往更长期。
On Demand Resources 和服务端下发
如果要简单区分两者适合什么场景,我一般会这样理解:
ODR 更适合:
- 静态
- 低频
- 和版本强绑定
- 可以接受按场景下载的资源
比如教程素材、关卡资源、AR 模型、某些低频功能页的大素材。
而服务端下发更适合:
- 高频变化
- 运营驱动
- 需要灰度控制
- 需要分地域或分人群策略的资源
比如活动 banner、节日皮肤、动态配置文案等。
两者都能降低首包压力,但本质不同:
ODR更像系统层的分发能力服务端下发更像业务层的灵活配置能力
它们都不是银弹。真正落地时,还是要一起看业务场景、弱网表现、离线要求和缓存策略。
工程化,才是防止包体回涨的关键
如果说前面的动作更偏专项治理,那工程化解决的是“后面怎么不再涨回来”。
我一直觉得,包体优化不应该只在“快超线了”时才被想起来,而应该变成一个持续被感知、被约束的工程指标。
比较常见的做法包括:
- 在 CI 中产出每次构建的包体报告
- 解析 LinkMap,做模块级、库级的体积 diff
- 对大资源新增、重依赖新增做阈值告警
- 在 MR 阶段把包体增量作为 review 维度之一
这样做的价值,不只是知道“又变大了”,而是能进一步知道:
- 是哪个模块涨了
- 是哪类资源涨了
- 是不是某个三方依赖带进来的
- 这次增长到底合不合理
只有把变化归因得足够清楚,团队才有可能形成相对稳定的治理习惯。
某种程度上说,前面的专项动作解决的是“这次怎么降”,而工程化解决的是“以后怎么稳”。
最后
回过头看,包体优化最难的地方,其实并不是缺少手段。
压图、删资源、裁依赖、开编译选项,这些都不难知道。
真正难的是三件事:
- 有没有先量化再动手
- 能不能分清优先级,先拿高 ROI
- 会不会在优化过程中把首屏、弱网、离线这些体验做坏
所以如果再让我总结一次,我会更愿意把它描述成这样:
- 包体优化的起点是数据,不是经验
- 包体优化的核心是归因,不是罗列技巧
- 包体优化的关键是 trade-off,不是单点极致
- 包体优化的终点是工程化,而不是一次性的专项冲刺
它表面上看是在“减几 MB”,但真正做起来,背后牵扯的是资源体系、依赖治理、分发策略、性能权衡和团队机制。
从这个角度看,包体优化其实不只是一个性能题。
它更像是在逼着我们重新审视工程本身。