以前用UWA和WeTest的工具来查看代码效率,他们都能提供我们代码每个函数的统计效率,然而并不需要我们做什么,所以我就在想如何实现一个类似他们的功能?
Unity提供了Profiler.BeginSample();Profiler.EndSample();方法来统计代码效率,但是有两个问题
1.只能统计单帧的效率
2.需要手动给每个方法加上这两个方法
然而我想要的是统计每个函数一段时间的所有执行效率的统计,比如玩上游戏大概15分钟,将15分钟内每个方法调用的耗时效率做一个总和出一份报表,(这也是我看UWA和WeTest都有的功能)通过代码注入自动给每个函数的首行和尾行添加两个方法就可以了。
首先在 https://github.com/jbevain/cecil 将mono.cecil取出来,我使用的版本是0.9.6版本,因为我觉得旧版本会稳定一点。另外,我使用的是源代码,没有直接用mono.cecil的DLL。原因是我们项目别的地方使用到了cecil修改的一个版本,为了避免冲突所以使用源代码可以修改命名空间保证两个mono.cecil不会被相互影响。
如下图所示,将mono.cecil导入unity工程即可。
接着我们就需要写入注入代码了,注入代码测试也可以分为两种。
1.编辑模式下测试,也就是不需要打包。
2.打包测试,这样需要在打包的时候自动将代码注入进去。
先来看看注入的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
using Mono.Cecil; using Mono.Cecil.Cil; using System; using System.IO; using System.Collections.Generic; using UnityEditor; using UnityEngine; using UnityEditor.Callbacks; using System.Linq; 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)); //解决方法中直接 return 后无法统计的bug //https://lostechies.com/gabrielschenker/2009/11/26/writing-a-profiler-for-silverlight-applications-part-1/ Instruction last = methodDefinition.Body.Instructions[methodDefinition.Body.Instructions.Count - 1]; Instruction lastInstruction = Instruction.Create(OpCodes.Ldstr, typeDefinition.FullName + "." + methodDefinition.Name); ilProcessor.InsertBefore(last, lastInstruction); ilProcessor.InsertBefore(last, Instruction.Create(OpCodes.Call, logMethodReference1)); var jumpInstructions = methodDefinition.Body.Instructions.Cast<Instruction>().Where(i => i.Operand == lastInstruction); foreach (var jump in jumpInstructions) { jump.Operand = lastInstruction; } wasProcessed = true; } } } return wasProcessed; } } |
注意这个标签,[PostProcessScene] 如果是正式打包会自动进入该标签下的方法,这样每次打包代码就可以自动注入进去了。
前面我们也提到了注入代码就是在每个方法的首部和尾部自动注入两个函数。如下图所示,注入代码后反编译DLL能看到首行和尾行的代码已经注入进去了。
代码注入成功以后就可以统计效率了。Begin()的时候取当前的Time.realtimeSinceStartup时间和Profiler.GetTotalAllocatedMemoryLong()内存,然后在End()的时候在取当前的Time.realtimeSinceStartup时间和Profiler.GetTotalAllocatedMemoryLong()内存减去Begin()中之前就记录的值就能统计到每个函数的执行效率和内存分配了,最后将数据生成报表就可以方便查看了。
也可以配合 Profiler.BeginSample() 和Profiler.EndSample() 在Profiler中查看每个函数的消耗。。
这个方法不仅编辑器下可以用,同样可以支持真机IL2CPP和mono我已经测试通过,并且已经用此法优化项目效率啦。最后欢迎大家一起讨论,如有过意见或者建议欢迎在下面给我留言。
参考:http://www.codersblock.org/blog//2014/06/integrating-monocecil-with-unity.html
- 本文固定链接: https://www.xuanyusong.com/archives/4525
- 转载请注明: 雨松MOMO 于 雨松MOMO程序研究院 发表
PDB文件的怎么处理,在Write的时候出现 System.IO.IOException: Sharing violation on path /Users/fzcs/Downloads2/1-U3d/UnityDemo-master/Unity_Demo1/Library/ScriptAssemblies/Assembly-CSharp.dll
for (int i = 0; i < assemblies.Length; i++)
{
string assemblyPathStr = assemblyPath[i];
var writerParameters = new WriterParameters();
writerParameters.WriteSymbols = true;
var pdbName = Path.ChangeExtension(assemblyPathStr, "pdb");
if (File.Exists(pdbName))
{
var symbolProvider = new PdbWriterProvider();
writerParameters.SymbolWriterProvider = symbolProvider;
}
AssemblyDefinition assembly = assemblies[i];
assemblies[i].Write(assemblyPathStr, writerParameters);
}
我是这样写的
请问你上述问题解决了吗,我也遇到了同样的问题。。。
readerParameters.ReadWrite = true;注意ReaderParameters中ReadWrite设为true,同时 assemblyDefinition.Write(assemblyPath, writerParameters);改为 assemblyDefinition.Write(writerParameters);不然会重新打开该文件。
Unity2021,试了下,依然提示
System.IO.IOException: Sharing violation on path
写入失败
请问有人解决了吗?
松哥,我想问下IL2CPP方式不是不会生成dll文件吗?这个方法怎么生效的呢
它直接转成c++了,然后在编译成so或者.a 在安卓和IOS平台下运行。
System.IO.FileNotFoundException: Could not find file “\Library\ScriptAssemblies\Assembly-CSharp.dll.mdb”
使用”F:\Program Files\Unity2019.2.16f1\Editor\Data\MonoBleedingEdge\bin\mono.exe” “F:\Program Files\Unity2019.2.16f1\Editor\Data\MonoBleedingEdge\lib\mono\4.5\pdb2mdb.exe” Assembly-CSharp.dll之后报
Error: A portable PDB can’t be converted to mdb.
请问下有时候崩溃是啥问题,找了半天无果
我也一样 你后来有解决吗
松哥,可以分享下HookUtils文件吗
松哥,能分享下HookUtils文件吗
替换 Return 跳转的地方写错了,应该用 last,如下。
var jumpInstructions = methodDefinition.Body.Instructions.Cast().Where(i => i.Operand == last);
多谢老哥提醒,如果是
if(xxx)
{
}
不用last,就会放在括号里面
如何过滤掉调用c++的函数呢?
执行assemblyDefinition.Write保存时报错:System.IO.IOException: Sharing violation on path C:\workspace\sandbox\trunk\code\client\goe\client\11111\xxxx.dll
程序集浏览器软件 是什么?
mac的话自带的Visual Studio就可以
HookUtils可以分享下吗
松哥 这个HookUtils能一起放出来吗
HookUtils的begin和end注意判断一下当前是否在主线程里。
static Thread mainThread= Thread.CurrentThread;
void Begin(){
if(Thread.CurrentThread == mainThread){
}
}
不加线程判断,会有什么问题么?
unity的一些方法不能再主线程调用。如果使用了就会报错
哪些方法不能再主线程用呢
比如实例化unity的一些操作gameobject的api