http://7dot9.com/?p=444ios
http://whydoidoit.com/2012/08/20/unity-serializer-mono-and-trampolines/app
肯定具體緣由async
那麼好吧,打一個測試版本再來看,而後再等着崩潰,查看崩潰日誌吧,最終看到的崩潰日誌中,崩潰線程輸出信息以下:ide
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 + 312this
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回調的地方。那麼這種問題爲何會出現呢,那咱們就得好好得來找找緣由了。
關於如何查看iOS崩潰日誌,讓崩潰日誌更加友好,咱們能夠參考這篇文章,iOS應用崩潰日誌揭祕,主要就是要確保你的設備上跑着的這個App的編譯和打包的二進制文件要在你用於查看日誌的Mac上,這樣的話,當咱們查看崩潰日誌的時候,Xcode會自動將那些沒法閱讀的函數調用的堆棧信息轉化成可讀性較強的日誌信息,幫助仍是很大的。
那麼這個時候咱們能夠經過將設備鏈接到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) 「的日誌,看着很像是本來是要執行某個方法,可是不知道由於什麼緣由這個方法就沒法訪問到了,好奇葩啊。
解決方案
如今雖然已經知道了問題出現的地方,可是貌似徹底看不明白的樣子,尼瑪trampoline都仍是第一次據說耶,那麼先請教一個我大Google吧,咱們老是相信本身不是那第一個吃螃蟹的人,因此咱們找到了一位大神的解決方案就在這裏,大神的文章寫得很是言簡意賅,大致意思就是若是你在作Unity3D開發時,特別是在針對iOS和Android平臺的時候,你頗有可能會碰到比較杯具的就是程序會莫名其妙地閃退哦,不過不要着急,這個一般就是由於你的程序編譯的時候給trampoline分配的空間過小,而你的程序中又大量使用了泛型、泛型方法調用和接口實現致使的。而後給出了具體的解決方法,那就是在Unity3D的編譯選項Player Setting中有一個AOT Compilation Options條目,在這個選項條目中加上如下編譯參數就行了
nrgctx-trampolines=8096,nimt-trampolines=8096,ntrampolines=4048
而後再從新一下,多多測試吧,騷年。關於這三個參數的意思呢,大神也給出瞭解釋,分別以下:
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的配置耗盡了,因此捏,咱們就把這個編譯選項開大就行了,解決方案就是上面我們提到的咯。