本文记录一次 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 属性,全部落在同一个__textUnity-iPhone.xcodeproj/project.pbxproj里无ORDER_FILE、无 Large Exe 相关设置
报错地址也印证:从 0x082A373C(约 130 MB)跳到 0x0000AC28(段首),是单段内的纯距离问题。所以一切依赖 section 分离的方案(order file、section 属性重排)在这里没有着力点。
排查过程
- 算超限幅度,确认只差 2.6 MB —— 决定了不用考虑拆 framework 之类的大改。
git log --grep="libGameAssembly"发现此问题此前已修过又被回滚:某提交修,后续某提交撤。- 检查
IL2CPP_METHOD_ATTR与ORDER_FILE,推翻网上的跨 section 解释。 - 用时间线证伪 link.xml 方案(见下)。
- 从 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作为跳转目标是超限的通用症状(全局唯一、位于段首),不是异常处理有问题。- 体积类修复有两个方向,代价相反,选之前先想清哪个能承受:裁剪零运行时代价、风险在正确性;泛型共享正确性无虞、风险在性能。
参考
- Unity 官方论坛同款报错:用户
distantsuns(2024-12)报 Unity 6 + Xcode 16,from _XRStats_TryGetStat_Internal_... (0x08029CC4) to ___clang_call_terminate (0x00009F2C),超限-134348960;Unity Staffaurimasc(2025-01)答复即"switch il2cpp code generation from faster runtime to smaller code in Player Settings" —— 与本次修复同款。 注意其场景是Debug-xrsimulator,只超 128 KB(134348960 - 134217728);本工程是真机 Release 超 2.6 MB,缺口约 20 倍,不是一个量级。 - Unity Discussions: ARM64 branch out of range
- Stack Overflow: How to measure ARM64 branch size
- Unity Issue Tracker: iOS ARM64 branch out of range