崩潰的情況
進入遊戲一會兒,神馬都不要做,雙手離開手機,盯着螢幕看吧,遊戲會定時從伺服器那兒讀取一些資料,時間一長,閃退了。尼瑪問題是神馬呢?完全沒有頭緒,不過大體猜測是因為網絡請求導緻的,那麼好,先排查伺服器傳回結果是否有問題,最終确認每次用戶端崩潰的時候,伺服器都成功的傳回了格式正确的資料,沒有任何異常。那麼可以确定問題是出在用戶端部分了。 先檢查代碼,确認邏輯上沒有任何問題之後,也倍感無力啊,問題依然在重制。腫麼辦呢?
确定具體原因
那麼好吧,打一個測試版本再來看,然後再等着崩潰,檢視崩潰日志吧,最終看到的崩潰日志中,崩潰線程輸出資訊如下:
Thread 27 Crashed:
0 libsystem_kernel.dylib 0x38e671fc __pthread_kill + 8
1 libsystem_pthread.dylib 0x38ecea4e pthread_kill + 54
2 libsystem_c.dylib 0x38e18028 abort + 72
3 gowonline 0x0178a0c0 mono_handle_native_sigsegv + 312
4 gowonline 0x01779a30 mono_sigsegv_signal_handler + 256
5 libsystem_platform.dylib 0x38ec9720 _sigtramp + 40
6 gowonline 0x00114f48 m_RestSharp_Http_ExecuteCallback_RestSharp_HttpResponse_System_Action_1_RestSharp_HttpResponse + 52
7 gowonline 0x001142b4 m_RestSharp_Http_RequestStreamCallback_System_IAsyncResult_System_Action_1_RestSharp_HttpResponse + 900
8 gowonline 0x00329c60 m_2be7 + 48
9 gowonline 0x00a39d08 m_System_Net_WebAsyncResult_DoCallback + 76
10 gowonline 0x00a29628 m_System_Net_HttpWebRequest_SetWriteStream_System_Net_WebConnectionStream + 536
11 gowonline 0x00a46f84 m_System_Net_WebConnection_InitConnection_object + 708
12 gowonline 0x0101ffac m_wrapper_runtime_invoke_object_runtime_invoke_dynamic_intptr_intptr_intptr_intptr + 200
13 gowonline 0x017792d4 mono_jit_runtime_invoke + 2152
14 gowonline 0x0181b324 mono_runtime_invoke + 132
15 gowonline 0x01820118 mono_runtime_invoke_array + 1448
16 gowonline 0x01820510 mono_message_invoke + 444
17 gowonline 0x018444a8 mono_async_invoke + 124
18 gowonline 0x01844174 async_invoke_thread + 312
19 gowonline 0x0184c580 start_wrapper + 496
20 gowonline 0x018695b4 thread_start_routine + 284
21 gowonline 0x01885750 GC_start_routine + 92
22 libsystem_pthread.dylib 0x38ecdc5a _pthread_body + 138
23 libsystem_pthread.dylib 0x38ecdbca _pthread_start + 98
好的,那麼已經确定是在我們使用的一個第三方類庫 RestSharp 中出現的問題,問題是出現在一個 Action 回調的地方。那麼這種問題為什麼會出現呢,那我們就得好好得來找找原因了。
那麼這個時候我們可以通過将裝置連接配接到 Mac 上,直接通過 Xcode 将程式編譯并運作,多嘗試着玩一段時間,當程式再次出現崩潰的時候,我們就能看到更清楚的函數調用關系了,同時也能看到更多的日志提示。
最終能确定每次崩潰的函數就是這個 mono_convert_imt_slot_to_vtable_slot,這個看上去就是 Mono Runtime 在将接口聲明的方法指針指向實際實作這個接口的對方的方法,我們可以找到 mono_convert_imt_slot_to_vtable_slot 這個方法所在的檔案檢視一下,這個方法就在 Mono 項目的目錄 mono/mini/mini-trampolines.c 中可以找到。
在 Xcode 中崩潰時,會輸出類似” SIGABRT (ERROR:mini-trampolines.c:183:mono_convert_imt_slot_to_vtable_slot: code should not be reached) “ 的日志,看着很像是原本是要執行某個方法,但是不知道因為什麼原因這個方法就無法通路到了,好奇葩啊。
解決方案
nrgctx-trampolines=8192,nimt-trampolines=8192,ntrampolines=4096
然後再重新一下,多多測試吧,騷年。關于這三個參數的意思呢,大神也給出了解釋,分别如下:
nrgctx-trampolines=8192 這是留給遞歸泛型使用的空間,預設是 1024
nimt-trampolines=8192 這是留給接口使用的空間,預設是 128
ntrampolines=4096 這是留給泛型方法調用使用的空間,預設是 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_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 的配置耗盡了,是以捏,我們就把這個編譯選項開大就好了,解決方案就是上面咱們提到的咯。
http://7dot9.com/