一次看似普通的功能合并让归档产物突然增加十几兆,但提交记录里既没有大图,也没有明确的新依赖。若只在发布前手工查看 IPA,问题通常已经混进多个分支。更稳妥的做法,是在固定的云端 Mac 构建环境中保存体积基线,每次归档后自动拆分 App、主可执行文件和动态框架,超过阈值就停止合并。
先定义可比较的测量对象
“包体积”至少有三种口径,不能混在一条曲线上。
| 指标 | 测量对象 | 主要用途 |
|---|---|---|
| App 展开体积 | .xcarchive 内的 .app 目录 |
发现资源、框架和本地化文件增长 |
| 主程序体积 | CFBundleExecutable 指向的 Mach-O 文件 |
发现代码、静态库和符号变化 |
| IPA 体积 | 导出后的压缩文件 | 观察用户实际下载包的趋势 |
门禁应以前两项为主。IPA 会受到压缩率和文件排列影响,同一批资源的小幅变化也可能放大或缩小压缩结果。调试符号位于归档的 dSYMs 目录,不应算入用户安装包,但应单独归档,方便后续定位问题。
只有构建条件一致,体积差异才有解释价值。Xcode 路径、配置、目标平台、导出方式和编译开关都必须固定。
用固定命令生成归档
先在干净工作目录中解析依赖,再为真实设备目标生成 Release 归档。工作区名和 Scheme 通过任务参数传入,避免把项目细节写死在脚本里。
set -euo pipefail
WORKSPACE="${WORKSPACE:?missing WORKSPACE}"
SCHEME="${SCHEME:?missing SCHEME}"
OUT="${OUT:-$PWD/build-size}"
ARCHIVE="$OUT/App.xcarchive"
rm -rf "$OUT"
mkdir -p "$OUT"
xcodebuild \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$ARCHIVE" \
clean archive \
CODE_SIGNING_ALLOWED=NO
如果工程的归档阶段必须执行签名相关脚本,就不要强行关闭签名,而应使用项目原本的受控配置。门禁关注的是“与正式构建一致”,不是追求一条适用于所有仓库的命令。首次接入时还要确认 Release 配置没有误用测试资源、诊断库或开发服务器地址。
拆分 App、可执行文件与框架
下面的脚本从归档中寻找唯一的 App,读取主程序名,并输出可机器解析的 TSV。du -sk 适合持续比较目录占用,stat 用来获得主程序的精确字节数。
set -euo pipefail
ARCHIVE="${1:?usage: measure.sh path/to/App.xcarchive}"
APP_ROOT="$ARCHIVE/Products/Applications"
APP_PATH="$(find "$APP_ROOT" -maxdepth 1 -type d -name '*.app' -print -quit)"
test -n "$APP_PATH"
EXECUTABLE="$(/usr/libexec/PlistBuddy \
-c 'Print :CFBundleExecutable' "$APP_PATH/Info.plist")"
printf "kind name bytes
"
APP_KB="$(du -sk "$APP_PATH" | awk '{print $1}')"
printf "app %s %s
" "$(basename "$APP_PATH")" "$((APP_KB * 1024))"
printf "executable %s %s
" "$EXECUTABLE" \
"$(stat -f '%z' "$APP_PATH/$EXECUTABLE")"
FRAMEWORKS="$APP_PATH/Frameworks"
if test -d "$FRAMEWORKS"; then
find "$FRAMEWORKS" -maxdepth 1 -type d -name '*.framework' -print0 |
while IFS= read -r -d '' item; do
kb="$(du -sk "$item" | awk '{print $1}')"
printf "framework %s %s
" "$(basename "$item")" "$((kb * 1024))"
done
fi
报告应作为本次构建产物保存,而不是只打印一个总数。发生回归时,审查者可以直接判断增长来自主程序、某个框架,还是整个 App 目录中的资源。
继续细分资源目录
若 App 总量增加而主程序和框架稳定,可对 Assets.car、本地化目录、离线数据文件和媒体资源分别执行 stat 或 du。不要直接删除看似重复的文件;先确认它们是否由不同 target、按需资源规则或本地化流程生成。
用双阈值减少误报
单独使用百分比会让小组件过度敏感,单独使用固定字节数又可能忽略大型 App 的持续膨胀。可以把允许增量定义为:
allowed = max(8 MiB, baseline_app_bytes × 3%)
failed = current_app_bytes - baseline_app_bytes > allowed
这里的 8 MiB 和 3% 只是接入初值,不是通用标准。先记录数次正常归档的波动,再按项目调整。主程序和单个框架应使用更小的独立阈值,否则一个新增依赖可能被 App 总量掩盖。
基线文件要和源码一起评审,但不能由失败任务自动覆盖。合理流程是:任务输出差异、开发者解释原因、审查者确认变化符合需求,最后在同一个变更中更新基线。这样才能区分“认可增长”和“为了让检查通过而重置数字”。
排查常见的异常增长
主程序增长时,先检查静态依赖、泛型实例化、重复链接和编译条件;框架增长时,核对依赖版本与嵌入方式;资源增长时,检查原图、字体、音视频文件和重复本地化内容。
还要避开四个常见误区:
- 不同 Xcode 环境生成的结果直接比较。
- 模拟器构建与真实设备归档混用。
- 只保存总大小,不保存组件明细和提交标识。
- 门禁失败后自动提高阈值或覆盖基线。
最终报告至少应包含提交标识、归档配置、App 总量、主程序大小、体积最大的若干框架、相对基线的增量和判定结果。门禁通过后仍保留报告,才能观察缓慢但连续的增长趋势;门禁失败时则保留归档和明细,在同一环境中复查,而不是依赖开发者本地重新构建。
常见问题
为什么不能只比较最终 IPA 文件大小?
IPA 使用压缩格式,压缩结果会受到资源内容、文件顺序和工具行为影响。门禁应以未压缩 App 目录和主可执行文件为主,IPA 大小只作为辅助指标。
包体积门禁的阈值应该设置为多少?
先用数次正常发布建立波动范围,再同时设置绝对增量和相对增幅。示例中的 8 MiB 与 3% 只是起点,最终阈值应按项目历史数据调整。
依赖升级导致体积增长时应该直接更新基线吗?
不应直接覆盖基线。先确认新增符号、资源或框架符合预期,在变更记录中说明原因并完成审核,然后随同该次变更提交新的基线。
把下一次 iOS 构建放到 MiniDebug M4 上。
固定提供 M4、16GB RAM 与 256GB SSD,可按天、周、月或季租用,并从新加坡、日本东京、韩国首尔、香港、美国东部五个节点中选择。实际可用状态以控制台实时返回为准。