Unity iOS 出包 libGameAssembly 超 ARM64 分支寻址上限(±128MB)排查与修复

119 阅读8分钟

本文记录一次 Unity IL2CPP iOS 出包时,链接器报 ARM64 branch out of range 的完整排查与修复过程。核心结论:把 iOS 的 IL2CPP Code Generation 从 Faster runtime 改为 Faster (smaller) builds(开启全泛型共享),即可把 libGameAssembly.a 压回 ±128MB 分支寻址范围内,代价是泛型性能(主要是内联损失),风险在性能而非正确性。

现象

某次升级期间,iOS Xcode 链接阶段失败:

ld: b(l) ARM64 branch out of range (-136940452 max is +/-128MB):
  from _UploadHandler_Finalize_m8769706C9DA361C89C42AEFE2F3F11549AEFE4E4A (0x082A373C)
  to ___clang_call_terminate (0x0000AC28)
  in '_UploadHandler_Finalize_m8769706C9DA361C89C42AEFE2F3F11549AEFE4E4A'
  from .../ReleaseForRunning-iphoneos/libGameAssembly.a(flhsgtsc5rxn.o) for architecture arm64
clang++: error: linker command failed with exit code 1

影响范围:iOS 完全无法出包,编译期就断,拿不到 ipa。C# 代码本身没错,是链接期布局问题。

超限幅度很小:136940452 - 134217728 = 2722724,只超约 2.6 MB。任何中等瘦身手段都够,不需要大动。

环境

  • 引擎版本:Unity 2022.3.62f2
  • 平台 / 设备:iOS,IL2CPP,ReleaseForRunning-iphoneos(已经是 Release,不是 Debug)
  • 复现概率:必现

规模参考:Assets/Scripts 下 8098 个 .cs;il2cppOutput 1089 个 cpp / 1.6 GB;XLua/Gen/ 319 个 wrapper / 7.8 MB。

根因

ARM64 的 B/BL 指令用 26 位有符号字偏移编码跳转目标,直接分支只能覆盖 ±128 MB。libGameAssembly.a 的 __text 涨过这个上限后,链接器放不下可用的 branch island(跳板),直接报错。

跳转目标是 ___clang_call_terminate —— clang 为 C++ 异常边界生成的辅助函数,被链接器合并成全局唯一一份放在段首(0x0000AC28,约 43 KB 处)。所有 object 的异常路径都往它跳,所以它天然是"离所有人最远"的那个符号,超限时第一个暴露。它是症状,不是原因,不要去查异常处理。

与网上主流解释的差异(重要)

搜到的资料(Unity 官方论坛、Stack Overflow)大多把此错误归因为 IL2CPP 把生成代码分到 __TEXT,__text 与 __TEXT,__il2cpp 两个 section,bl 跨段跳导致超限。这个前提在本工程不成立:

  • Il2CppOutputProject/IL2CPP/libil2cpp/il2cpp-config.h:27-28 里 IL2CPP_METHOD_ATTR 是空定义,生成方法没有 section 属性,全部落在同一个 __text
  • Unity-iPhone.xcodeproj/project.pbxproj 里无 ORDER_FILE、无 Large Exe 相关设置

报错地址也印证:从 0x082A373C(约 130 MB)跳到 0x0000AC28(段首),是单段内的纯距离问题。所以一切依赖 section 分离的方案(order file、section 属性重排)在这里没有着力点。

排查过程

  1. 算超限幅度,确认只差 2.6 MB —— 决定了不用考虑拆 framework 之类的大改。
  2. git log --grep="libGameAssembly" 发现此问题此前已修过又被回滚:某提交修,后续某提交撤。
  3. 检查 IL2CPP_METHOD_ATTR 与 ORDER_FILE,推翻网上的跨 section 解释。
  4. 用时间线证伪 link.xml 方案(见下)。
  5. 从 Xcode 构建日志抓实际编译参数,确认优化级别无余量。

关键手法 —— 从 .xcactivitylog 提取真实编译 flag,比读 project.pbxproj 可靠(很多设置是 unset 走默认值,pbxproj 里看不见):

cd ~/Library/Developer/Xcode/DerivedData/<项目>/Logs/Build
LOG=$(ls -t *.xcactivitylog | head -1)
gunzip -c "$LOG" | tr '\r' '\n' | grep -oE '\-O[0-3szg]?\b' | sort | uniq -c

解析 project.pbxproj 里各 target/configuration 的实际取值:

xcodebuild -showBuildSettings -project Unity-iPhone.xcodeproj \
  -target GameAssembly -configuration ReleaseForRunning \
  | grep -E "GCC_OPTIMIZATION_LEVEL|LLVM_LTO|DEAD_CODE"

检索关键字:branch out of range、max is +/-128MB、___clang_call_terminate、libGameAssembly.a

逐条排除的方案

方案结论依据
收窄 Assets/link.xml 的 <assembly fullname="Scripts" preserve="all" />无效(已证伪)超限修复某提交比 link.xml 的任何改动都早 9 小时以上 —— 加那句之前 iOS 就已经报超限
调低 C++ 优化级别换体积无余量构建日志里实际已是 -Os(体积优先);GCC_OPTIMIZATION_LEVEL 在 pbxproj 里 unset 只是走默认
order file / section 属性重排无着力点IL2CPP_METHOD_ATTR 空定义,不存在 section 分离(见根因)
改用 Release 构建(网上常见建议)不适用报错本身就来自 ReleaseForRunning
调高 managedStrippingLevel 开启更激进裁剪高风险,未采用正撞在已知的裁剪崩溃区(IL2CPP 裁剪导致启动崩溃);症状是启动崩溃、编译期无感
DEAD_CODE_STRIPPING已是 YES无余量

关于 link.xml 的补充:preserve 只能阻止裁剪,不能启用裁剪。ProjectSettings.asset 的 managedStrippingLevel 只有 Android: 1、无 iPhone 条目,所以那句 Scripts preserve="all" 对 iOS 体积的影响很有限 —— 它是为修 IL2CPP 裁剪崩溃加的,不是体积变量。

修复

ProjectSettings/ProjectSettings.asset,把 iOS 的 IL2CPP Code Generation 从默认的 Faster runtime 改为 Faster (smaller) builds:

il2cppCompilerConfiguration: {}
il2cppCodeGeneration:
  iPhone: 1        # 1 = OptimizeSize / 全泛型共享
managedStrippingLevel:
  Android: 1       # 保持不动

Editor 路径:Player Settings → iOS → Other Settings → IL2CPP Code Generation → Faster (smaller) builds。

这个开关到底改了什么

它开启全泛型共享(full generic sharing)。默认 OptimizeSpeed 下,IL2CPP 为每个值类型泛型实参生成一份独立特化的 C++ —— List<int>、List<float>、Dictionary<int,Vector3> 各一份完整实现(引用类型实参本来就共享,指针尺寸相同无需特化)。开启后值类型实例化也塌缩成一份,类型信息(尺寸、字段偏移、是否装箱)通过隐藏参数 RGCTX 在运行时传入。生成的 C++ 少一大截。

代价:

  • 失去内联 —— 通常是实测差异的主因。List<int>.Add 这类小方法,特化版本 clang 能内联,共享版本带隐藏参数 + 动态查表基本内联不了
  • 涉及 T 尺寸/布局的操作要经 RGCTX 运行时查,不再是编译期常量折叠
  • 泛型虚方法、泛型接口分派最吃亏(本就走额外分派表,叠加后放大)
  • 每个实例化的 RGCTX 首次解析要建表 → 偶发首帧卡顿,之后缓存
  • Profiler 归因变差:多个泛型实例化塌缩成同一个 C++ 符号,热点定位变难
  • 平台行为分叉:此后 Android 走默认、iOS 走共享,"只有 iOS 慢"类问题多一个排查变量

不会有的影响(常见误解):不降低 C++ 优化等级(-O 不受影响);不改变语义;不引入反射/序列化崩溃(与裁剪是完全独立的两个轴);非泛型代码与引用类型泛型完全不受影响。

反向的好处:XLua 运行时会反射构造泛型 AOT 下反射构造值类型泛型实例化若无预生成特化会抛 ExecutionEngineException。全泛型共享正好兜住这类调用 —— 对 Lua 侧动态调泛型的健壮性是加分项。

验证

  • Xcode 链接通过,ipa 正常导出
  • 初步运行未发现明显影响
  • 性能未实测 —— 见下

暴露面不小:当前只是"初步看没影响",没有数据。

要把"担心性能"变成"有依据",需要:两种配置各出一包 → Xcode link map(LD_GENERATE_MAP_FILE = YES)对比 __text 大小 → 真机跑一段有代表性的战斗,Profiler 对比帧时间。若实测掉帧不可接受,说明体积增长已超出单个开关能兜住的范围,得考虑拆 framework 或砍依赖。

教训

  • preserve ≠ 开启裁剪。link.xml 只能阻止裁剪。裁剪级别没开时,往里加 preserve 条目对体积不产生影响 —— 别把它当体积旋钮。
  • 先查这个错以前修过没有。git log --grep 一条命令的事,比从零推演快得多。
  • 网上方案要先验证前提在本工程成立。此错误的主流解释依赖 IL2CPP 的 section 分离,而本工程 IL2CPP_METHOD_ATTR 是空的,那套方案整体不适用。
  • pbxproj 里 unset ≠ 没有值。要看真实生效的编译参数,查 .xcactivitylog 或 xcodebuild -showBuildSettings,别读 pbxproj 猜。
  • ___clang_call_terminate 作为跳转目标是超限的通用症状(全局唯一、位于段首),不是异常处理有问题。
  • 体积类修复有两个方向,代价相反,选之前先想清哪个能承受:裁剪零运行时代价、风险在正确性;泛型共享正确性无虞、风险在性能。

参考