MiniDebug 工程笔记

在云端 Mac 上建立 iOS 包体积回归门禁

在云端 Mac 上建立 iOS 包体积回归门禁

一次看似普通的功能合并让归档产物突然增加十几兆,但提交记录里既没有大图,也没有明确的新依赖。若只在发布前手工查看 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、本地化目录、离线数据文件和媒体资源分别执行 statdu。不要直接删除看似重复的文件;先确认它们是否由不同 target、按需资源规则或本地化流程生成。

用双阈值减少误报

单独使用百分比会让小组件过度敏感,单独使用固定字节数又可能忽略大型 App 的持续膨胀。可以把允许增量定义为:

allowed = max(8 MiB, baseline_app_bytes × 3%)
failed  = current_app_bytes - baseline_app_bytes > allowed

这里的 8 MiB 和 3% 只是接入初值,不是通用标准。先记录数次正常归档的波动,再按项目调整。主程序和单个框架应使用更小的独立阈值,否则一个新增依赖可能被 App 总量掩盖。

基线文件要和源码一起评审,但不能由失败任务自动覆盖。合理流程是:任务输出差异、开发者解释原因、审查者确认变化符合需求,最后在同一个变更中更新基线。这样才能区分“认可增长”和“为了让检查通过而重置数字”。

排查常见的异常增长

主程序增长时,先检查静态依赖、泛型实例化、重复链接和编译条件;框架增长时,核对依赖版本与嵌入方式;资源增长时,检查原图、字体、音视频文件和重复本地化内容。

还要避开四个常见误区:

  1. 不同 Xcode 环境生成的结果直接比较。
  2. 模拟器构建与真实设备归档混用。
  3. 只保存总大小,不保存组件明细和提交标识。
  4. 门禁失败后自动提高阈值或覆盖基线。

最终报告至少应包含提交标识、归档配置、App 总量、主程序大小、体积最大的若干框架、相对基线的增量和判定结果。门禁通过后仍保留报告,才能观察缓慢但连续的增长趋势;门禁失败时则保留归档和明细,在同一环境中复查,而不是依赖开发者本地重新构建。

常见问题

为什么不能只比较最终 IPA 文件大小?

IPA 使用压缩格式,压缩结果会受到资源内容、文件顺序和工具行为影响。门禁应以未压缩 App 目录和主可执行文件为主,IPA 大小只作为辅助指标。

包体积门禁的阈值应该设置为多少?

先用数次正常发布建立波动范围,再同时设置绝对增量和相对增幅。示例中的 8 MiB 与 3% 只是起点,最终阈值应按项目历史数据调整。

依赖升级导致体积增长时应该直接更新基线吗?

不应直接覆盖基线。先确认新增符号、资源或框架符合预期,在变更记录中说明原因并完成审核,然后随同该次变更提交新的基线。

独享物理 Mac

把下一次 iOS 构建放到 MiniDebug M4 上。

固定提供 M4、16GB RAM 与 256GB SSD,可按天、周、月或季租用,并从新加坡、日本东京、韩国首尔、香港、美国东部五个节点中选择。实际可用状态以控制台实时返回为准。

选择节点并订购