iOS平台下闪退原因汇总(一):"Ran out of trampolines of type 0/1/2" 运行时间错误

在 Xcode 中崩溃时,会输出类似"Ran out of trampolines of type 0/1/2" 或者 " SIGABRT (ERROR:mini-trampolines.c:183:mono_convert_imt_slot_to_vtable_slot: code should not be reached) " 的日志

这个通常就是因为你的程序编译的时候给 trampoline 分配的空间太小,而你的程序中又大量使用了泛型、泛型方法调用和接口实现导致的。然后给出了具体的解决方法,那就是在 Unity3D 的编译选项 Player Setting 中有一个 AOT Compilation Options 条目,在这个选项条目中加上以下编译参数就好了

nrgctx-trampolines=8096,nimt-trampolines=8096,ntrampolines=4048

然后再重新一下,多多测试吧,

关于这三个参数的意思呢,分别如下:

nrgctx-trampolines=8096 这是留给递归泛型使用的空间,默认是 1024 
nimt-trampolines=8096 这是留给接口使用的空间,默认是 128 
ntrampolines=4048 这是留给泛型方法调用使用的空间,默认是 1024 


Mono Runtime AOT 机制剖析

虽然问题貌似已经得到解决了,而且我们貌似也搞清楚了具体原因就是因为默认 Mono Runtime 在 AOT 编译的时候给的 trampoline 配置太小,不适合我们这种设计优良,大量使用 interface,设计绝对遵照 OO 思想的稍大一些的项目呢。那么我们以后是不是在做 Unity3D 开发的时候就尽量少用接口呢?是不是我们就尽量少用泛型和泛型方法呢?

既然这么感兴趣,想问个究竟,那么我们就来好好看看这个 AOT 到底是个神马东西吧,尼玛为什么就这么复杂,这么隐蔽,这么折腾人,《铁血战神》在 App Store 上线都 5 个月了有木有,尼玛这个问题碰到也不是一次两次了有木有,作为程序猿的我们被玩家吐槽了很多次,我们的客服 XDJM 们为我们背了多少黑锅啊,我勒个去啊。

首先,还是先搞定这个 trampoline 吧,毕竟问题的根源是在它身上的,那么我们就好好来看看这是个神马东西。我们找到 Mono Runtime 的官方文档中关于 trampoline 的描述来看看吧。

Trampolines are small, hand-written pieces of assembly code used to perform various tasks in the mono runtime. They are generated at runtime using the native code generation macros used by the JIT. They usually have a corresponding C function they can fall back to if they need to perform a more complicated task. They can be viewed as ways to pass control from JITted code back to the runtime.

翻译一下吧:

Trampoline 是一些手写的非常短小的用来在 mono 运行时中执行很多操作的组件代码。主要是通过 JIT 使用到的本地代码宏在运行时动态生成的。它们通常都有与之相对应的 C 方法,在某些较为复杂的场景中,当 trampoline 无法胜任时,mono 运行时就会将这些复杂的操作交回给这些对应的 C 方法来执行。这也可以看作是将 JIT 代码的执行权交回给 runtime 的一种方式。

好吧,貌似还没有太明白,那么这个 Trampoline 为什么会导致出现闪退的问题的,这看起来明显是为了提高 mono runtime 在执行 C#代码时候的效率啊。

那么我们再来看看官方文档关于 JIT Trampolines 和 AOT Trampolines 的介绍吧,杯具的 IMT Trampolines 介绍还在//TODO 状态中。

JIT Trampolines These trampolines are used to JIT compile a method the first time it is called. When the JIT compiles a call instruction, it doesn’t compile the called method right away. Instead, it creates a JIT trampoline, and emits a call instruction referencing the trampoline. When the trampoline is called, it calls mono_magic_trampoline () which compiles the target method, and returns the address of the compiled code to the trampoline which branches to it. This process is somewhat slow, so mono_magic_trampoline () tries to patch the calling JITted code so it calls the compiled code instead of the trampoline from now on. This is done by mono_arch_patch_callsite () in tramp-.c.

好吧,再翻译一下吧。

JIT Trampolines 这些 Trampoline 主要是 JIT 在首次调用某个方法的时候编译方法用的。当 JIT 在编译一个方法调用指令时,它并不会立刻就编译这个被调用到的方法。实际上,它会先创建一个 JIT Trampoline,同时创建一个指向这个 trampoline 的调用指令。当这个 JIT Trampoline 在调用到的时候,它会再调用 mono_magic_trampoline() 方法来编译这个 trampoline 实际指向的目标方法,然后将编译后的方法的指针地址返回给这个指向它的 trampoline。这个过程呢稍微有点慢,所以呢,mono_magic_trampoline() 方法会优化调用 JIT 代码的过程,它会先尝试调用已经通过 JIT 编译过的方法而不是立即通过 trampoline 直接进行调用。这些都是通过在 tramp-.c 文件中的 mono_patch_callsiete() 方法来完成的。

这就是 JIT Trampolines 的机制,接下来我们看看 AOT Trampolines 又是怎么一回事呢。

AOT Trampolines

These are similar to the JIT trampolines but instead of receiving a MonoMethod to compile, they receive an image+token pair. If the method identified by this pair is also AOT compiled, the address of its compiled code can be obtained without loading the metadata for the method.

再翻译一下。

AOT Trampolines AOT Trampolines 和 JIT Trampolines 非常相似,但是 AOT Trampolines 接受的编译参数不是一个 Mono 方法而是一个 image+token 对。如果传入的用于编译的 image+token 对所指向的方法已经经过 AOT 编译过了,那么再次编译这个 image+token 对时,就会直接返回这个已编译方法的指针地址而不需要再次加载这个方法的元数据进行再次编译了。

好吧,看了这么多关于 Trampoline 相关的内容,貌似只是了解到了非常有限的内容,那就依然是 Trampolines 存在的价值就是为了减少 C#代码在 mono runtime 中运行时的性能损耗,提高 C#代码的执行效率。

还有那个没有出场的 IMT Trampolines 应该也就是用于优化接口调用效率的小『蹦床』吧。

那么我们在开发 Unity3D 游戏的时候通常都会发布到 iOS 设备和 Android 设备上,而 Unity3D 在 iOS 和 Android 设备上的发布都选择了使用 AOT 编译机制来实现。那么显然我们碰到的 Trampolines 问题都是跟 AOT Trampolines 有关,那么 AOT 又是神马呢?

AOT 就是区别于 JIT(Just In Time) 的另一个编译机制,全称是 Ahead Of Time,就是预先编译好,而不是在代码执行到了某个方法再进行编译,这样的话会有一些好处。

通过查看 Mono 官方 AOT 介绍文档 ,使用 AOT 编译的有点有以下优点: 1. 加快程序启动速度 2. 更强的内存共享机制 3. 潜在的性能提升

当然也会有一些限制,例如支持平台的有限,支持 AOT 的 Mono 版本有限等等,具体信息可以参考 Mono 官方 AOT 介绍文档 。

那么回到我们最开始的问题,为什么我们的游戏就会出现崩溃呢?好吧,现在一点点回顾吧。

我们出现的问题是偶尔会出现闪退,根据崩溃日志我们能定位到是 mono_convert_imt_slot_to_vtable_slot 这个方法导致的,然后我们再通过 Xcode 跟踪到了是 trampoline 无法被访问到的问题。

那么这么高端大气上档次的问题是肿么出现的呢?貌似 Mono 还算是个不错的产品啊,还是很活跃的啊,也有专门的公司 Xamarin 在支撑着,怎么就会出现这种问提呢?

好吧,程序都是人写的,有问题也是很正常的。上面的分析已经很清楚了,大体的原因就是因为 Mono 在 iOS/Android 等移动设备上使用了 AOT 这种机制,为什么选择这种机制?原因非常简单,那就是可以针对特定平台编译成在平台优化的字节码,在资源比较紧缺的移动平台上还是有着明显优势的。而使用 AOT 编译就需要为 Trampolines 这些小东西留足足够的空间,当然这个肯定是硬编码的某个常数啦,在整个程序加载成功运行之后,该常数就成为了 Trampolines 运行时的配置。AOT 默认编译时给 Trampolines 的参数有点低:

nrgctx-trampolines 默认为 1024

nimt-trampolines 默认为 128

ntrampolines 默认为 1024

这对于小一些的项目可能是够用的,因为整体项目的结构不会太复杂,使用到的接口、泛型、递归相对也不会太多,但是对于一个稍大一些的项目来说,特别是采用了某些设计良好的第三方库的项目来说,这就比较纠结了。

其实我们在项目中就使用了两个第三方的库,一个是 CodeTitan.JSon 库,一个是 RestSharp,分别用于 JSON 解析和 HTTP 请求处理,可是这两个库实在是设计得太好了,各种使用接口,各种抽象,没个两三天我都没法说完全理解了整个库的结构。

就是因为这些设计良好,完全遵循 OOP 原则,高度抽象的类库将 Mono 默认的 Trampolines 的配置耗尽了,所以捏,我们就把这个编译选项开大就好了,解决方案就是上面咱们提到的咯。

阅读更多
换一批

没有更多推荐了,返回首页