使用 Mono.Cecil 輔助 Unity3D 手遊進行效能測試(續)

weixin_34146805發表於2018-01-31

之前的方法及其侷限

問題背景和最初的嘗試見這裡。最開始的想法比較簡單,只想著利用 PostprocessBuild 這個事件,來對已經準備好的本地工程檔案(iOS 或 Android)中的 .NET 程式集進行注入。但是,這樣做限制很多。

首先,無法對 IL2CPP 作為 Scripting Backend 的情況進行注入。因為觸發這個事件時,本地工程檔案中沒有 .NET 程式集,只有 C++ 程式碼,無法用 Cecil 進行注入。

第二,Android 平臺,用 Mono2x 作為 Scripting Backend 的情況下,也需要打包為 Android Studio Project 才能使用。對於直接打包成 apk 的情況,無法簡單的進行注入(除非使用解包、注入、重新簽名打包的方法,比較麻煩)。

第三,iOS 平臺,即使用 Mono2x 作為 Scripting Backend,也無法成功。這是因為在 iOS 平臺打包需要先進行一個叫 AOT Cross Compiling 的步驟,對所有的程式集生成對應的 .dll.s 檔案。這些檔案包含的資訊會在執行時被校驗,如果我篡改了程式集,而沒有理會 .dll.s 檔案,在執行時會報錯。錯誤資訊類似

A script behaviour (probably XXX?) has a different seralization layout when loading. (Read ** bytes but expected ** bytes)

Did you #ifdef UNITY_EDITOR a section of your serialized properties in any of your scripts?

其中 XXX 是 .NET 指令碼名稱,兩組星號表示兩個不同值。這錯誤最終導致指令碼載入失敗,無法執行遊戲。與錯誤資訊描述不同,我並沒有在出問題的指令碼上寫任何條件編譯的程式碼。要想解決這個問題,估計需要篡改 .dll.s 檔案才可以,仍然是很不經濟的。

篡改編譯器的方法

接下來一個辦法,就是對 Unity 的 C# 編譯器 mcs.exe 進行篡改。我沒有深入實驗,因為幾個簡單的實驗就耗費了一天多的時間。我主要嘗試了兩種方法,當然,都沒成功。

方法一,將原 mcs.exe 重新命名(如 mcs1.exe),而後自己寫一個 .NET 控制檯應用程式,佔據原來 mcs.exe 的位置,在其中用 System.Diagnostic.Process 類來啟動 mcs1.exe。這個過程中,我對 Process 物件的一些配置,如環境變數(EnvironmentVariables 屬性)、輸入輸出重定向(RedirectStandardXXX 屬性)進行了多種排列組合,仍無法正確呼叫 mcs1.exe,就更不要說呼叫之後的事情了。

方法二,直接在 mcs.exe 中注入程式碼。因為 mcs.exe 也是一個 .NET 應用程式,並且看上去未經混淆,所以直接注入是可行的。即,「把向遊戲程式集中注入程式碼的程式碼,注入到編譯器中。」這樣做主要的問題,是 mcs.exe 的輸出目錄是臨時資料夾,無法保證其中有我們依賴的(如注入後寫入程式集時,需要用 Mono.Cecil 的 DefaultAssemblyResolver 進行解析的)程式集。

通過 OnPostprocessScene 回撥事件來進行注入

Unity 雖然沒有在執行 mcs.exe 和後續步驟(IL2CPP、Android 打包 apk、iOS 上的 AOT 交叉編譯等)之間提供回撥,但是回撥事件 OnPostprocessScene 目前是確保在它們之間至少觸發一次的。多虧 https://github.com/rayosu/UnityDllInjector 提醒了我。在這個事件回撥中處理 DLL,理論上在任何平臺、任何 Scripting Backend 上都可以有效注入。實現過程中有幾個要點需要注意:

  • 事件 OnPostprocessScene 對應 Build Settings 中指定打包的場景個數,所以它可能執行多次,故而需要防止重複。除了上述 UnityDllInjector 中提供的方法,還可以直接把注入標記寫入你的目標程式集。但值得注意的是,新增一個用於標記的空類在 iOS + Mono2x 下又是不好用的,猜測還和 AOT 交叉編譯有關。保險的做法之一,是在遊戲程式碼中保留幾個 bool 常量,值為 false,注入前檢查相應的值,如果為 true 則跳過,否則注入。注入完成後,將相應的 bool 常量篡改為 true 即可。

  • 遊戲指令碼對應的程式集,在注入時一定處於和 Assets 同級的 Library 下的 ScriptAssemblies 資料夾下,但要注意你依賴的 Unity 程式集。我使用 UnityDllInjector 提供的方法,依然不能保證獲取到需要的程式集。最終我採用的方法是,使用 EditorApplication.applicationContentsPath 獲取 Unity 安裝目錄,在其中 Data/Managed 目錄裡尋找必要的程式集。

目前我測試了 Android + Mono/IL2CPP 和 iOS + IL2CPP,都沒有問題。iOS + Mono2x 可能由於我們專案本身的一些問題,在 Xcode 連結階段有一些問題。


舊文搬運,2017-06-15 首發於部落格園。

相關文章