Unity3D研究院自動注入程式碼統計每個函式的執行效率以及記憶體分配(九十八)
以前用UWA和WeTest的工具來檢視程式碼效率,他們都能提供我們程式碼每個函式的統計效率,然而並不需要我們做什麼,所以我就在想如何實現一個類似他們的功能?
Unity提供了Profiler.BeginSample();Profiler.EndSample();方法來統計程式碼效率,但是有兩個問題
1.只能統計單幀的效率
2.需要手動給每個方法加上這兩個方法
然而我想要的是統計每個函式一段時間的所有執行效率的統計,比如玩上游戲大概15分鐘,將15分鐘內每個方法呼叫的耗時效率做一個總和出一份報表,(這也是我看UWA和WeTest都有的功能)通過程式碼注入自動給每個函式的首行和尾行新增兩個方法就可以了。
首先在 ofollow,noindex" target="_blank">https://github.com/jbevain/cecil 將mono.cecil取出來,我使用的版本是0.9.6版本,因為我覺得舊版本會穩定一點。另外,我使用的是原始碼,沒有直接用mono.cecil的DLL。原因是我們專案別的地方使用到了cecil修改的一個版本,為了避免衝突所以使用原始碼可以修改名稱空間保證兩個mono.cecil不會被相互影響。
如下圖所示,將mono.cecil匯入unity工程即可。
接著我們就需要寫入注入程式碼了,注入程式碼測試也可以分為兩種。
1.編輯模式下測試,也就是不需要打包。
2.打包測試,這樣需要在打包的時候自動將程式碼注入進去。
先來看看注入的程式碼
using Mono.Cecil; using Mono.Cecil.Cil; using System; using System.IO; using System.Collections.Generic; using UnityEditor; using UnityEngine; using UnityEditor.Callbacks; public class HookEditor { static List<string> assemblyPathss = new List<string>() { Application.dataPath+"/../Library/ScriptAssemblies/Assembly-CSharp.dll", Application.dataPath+"/../Library/ScriptAssemblies/Assembly-CSharp-firstpass.dll", }; [MenuItem("Hook/主動注入程式碼")] static void ReCompile() { AssemblyPostProcessorRun(); } [MenuItem("Hook/輸出結果")] static void HookUtilsMessage() { HookUtils.ToMessage(); } [PostProcessScene]//打包的時候會自動呼叫下面方法注入程式碼 static void AssemblyPostProcessorRun() { try { Debug.Log("AssemblyPostProcessor running"); EditorApplication.LockReloadAssemblies(); DefaultAssemblyResolver assemblyResolver = new DefaultAssemblyResolver(); foreach (System.Reflection.Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) { assemblyResolver.AddSearchDirectory(Path.GetDirectoryName(assembly.Location)); } assemblyResolver.AddSearchDirectory(Path.GetDirectoryName(EditorApplication.applicationPath) + "/Data/Managed"); ReaderParameters readerParameters = new ReaderParameters(); readerParameters.AssemblyResolver = assemblyResolver; WriterParameters writerParameters = new WriterParameters(); foreach (String assemblyPath in assemblyPathss) { readerParameters.ReadSymbols = true; readerParameters.SymbolReaderProvider = new Mono.Cecil.Mdb.MdbReaderProvider(); writerParameters.WriteSymbols = true; writerParameters.SymbolWriterProvider = new Mono.Cecil.Mdb.MdbWriterProvider(); AssemblyDefinition assemblyDefinition = AssemblyDefinition.ReadAssembly(assemblyPath, readerParameters); Debug.Log("Processing " + Path.GetFileName(assemblyPath)); if (HookEditor.ProcessAssembly(assemblyDefinition)) { Debug.Log("Writing to " + assemblyPath); assemblyDefinition.Write(assemblyPath, writerParameters); Debug.Log("Done writing"); } else { Debug.Log(Path.GetFileName(assemblyPath) + " didn't need to be processed"); } } } catch (Exception e) { Debug.LogWarning(e); } EditorApplication.UnlockReloadAssemblies(); } private static bool ProcessAssembly(AssemblyDefinition assemblyDefinition) { bool wasProcessed = false; foreach (ModuleDefinition moduleDefinition in assemblyDefinition.Modules) { foreach (TypeDefinition typeDefinition in moduleDefinition.Types) { if (typeDefinition.Name == typeof(HookUtils).Name) continue; //過濾抽象類 if (typeDefinition.IsAbstract) continue; //過濾抽象方法 if (typeDefinition.IsInterface) continue; foreach (MethodDefinition methodDefinition in typeDefinition.Methods) { //過濾建構函式 if(methodDefinition.Name == ".ctor")continue; if (methodDefinition.Name == ".cctor") continue; //過濾抽象方法、虛擬函式、get set 方法 if (methodDefinition.IsAbstract) continue; if (methodDefinition.IsVirtual) continue; if (methodDefinition.IsGetter) continue; if (methodDefinition.IsSetter) continue; //如果注入程式碼失敗,可以開啟下面的輸出看看卡在了那個方法上。 //Debug.Log(methodDefinition.Name + "======= " + typeDefinition.Name + "======= " +typeDefinition.BaseType.GenericParameters +" ===== "+ moduleDefinition.Name); MethodReference logMethodReference = moduleDefinition.Import(typeof(HookUtils).GetMethod("Begin", new Type[] { typeof(string) })); MethodReference logMethodReference1 = moduleDefinition.Import(typeof(HookUtils).GetMethod("End", new Type[] { typeof(string) })); ILProcessor ilProcessor = methodDefinition.Body.GetILProcessor(); Instruction first = methodDefinition.Body.Instructions[0]; ilProcessor.InsertBefore(first, Instruction.Create(OpCodes.Ldstr, typeDefinition.FullName + "." + methodDefinition.Name)); ilProcessor.InsertBefore(first, Instruction.Create(OpCodes.Call, logMethodReference)); Instruction last = methodDefinition.Body.Instructions[methodDefinition.Body.Instructions.Count - 1]; ilProcessor.InsertBefore(last, Instruction.Create(OpCodes.Ldstr,typeDefinition.FullName + "." + methodDefinition.Name)); ilProcessor.InsertBefore(last, Instruction.Create(OpCodes.Call, logMethodReference1)); wasProcessed = true; } } } return wasProcessed; } }
注意這個標籤,[PostProcessScene] 如果是正式打包會自動進入該標籤下的方法,這樣每次打包程式碼就可以自動注入進去了。
前面我們也提到了注入程式碼就是在每個方法的首部和尾部自動注入兩個函式。如下圖所示,注入程式碼後反編譯DLL能看到首行和尾行的程式碼已經注入進去了。
程式碼注入成功以後就可以統計效率了。Begin()的時候取當前的Time.realtimeSinceStartup時間和Profiler.GetTotalAllocatedMemoryLong()記憶體,然後在End()的時候在取當前的Time.realtimeSinceStartup時間和Profiler.GetTotalAllocatedMemoryLong()記憶體減去Begin()中之前就記錄的值就能統計到每個函式的執行效率和記憶體分配了,最後將資料生成報表就可以方便查看了。
這個方法不僅編輯器下可以用,同樣可以支援真機IL2CPP和mono我已經測試通過,並且已經用此法優化專案效率啦。最後歡迎大家一起討論,如有過意見或者建議歡迎在下面給我留言。
參考:http://www.codersblock.org/blog//2014/06/integrating-monocecil-with-unity.html
- 本文固定連結: https://www.xuanyusong.com/archives/4525
- 轉載請註明:雨鬆MOMO 於雨鬆MOMO程式研究院 發表
雨鬆MOMO提醒您:親,如果您覺得本文不錯,快快將這篇文章分享出去吧 。另外請點選網站頂部彩色廣告或者捐贈支援本站發展,謝謝!
捐 贈 如果您願意花20塊錢請我喝一杯咖啡的話,請用手機掃描二維碼即可通過支付寶直接向我捐款哦。