使用Harmony检测Unity.Object的隐式转换
简介
Unity是一个非常优秀的引擎,但其中有些设计在提供方便的同时也埋下了隐患,比如BroadcastMessage以及UnityEngine.Object
中的隐式转换。
本文通过使用Harmony注入C#代码,达到检测隐式转换的效果,提供了替换Unity引擎C#代码的思路。
Harmony
Harmony - a library for patching, replacing and decorating .NET methods during runtime.
Harmony库的作用就是在运行时替换或者装饰.NET函数(Hook钩子或者AOP切面编程),也就是说我们需要和M$IL语言打交道。它官网上的文章介绍了原理以及入门教程。
这里我们选择使用手动补丁的方式注入,一方面是提供了更多灵活性,另一方面是如果存在多个隐式转换,那它们的函数签名会是一样的,我们需要通过反射获取返回值的类型来区分。
// add null checks to the following lines, they are omitted for clarity
// when possible, don't use string and instead use nameof(...)
var original = typeof(TheClass).GetMethod("TheMethod");
var prefix = typeof(MyPatchClass1).GetMethod("SomeMethod");
var postfix = typeof(MyPatchClass2).GetMethod("SomeMethod");
harmony.Patch(original, new HarmonyMethod(prefix), new HarmonyMethod(postfix));
DotPeek查看函数名
既然是需要用反射获取函数信息,那我们需要知道函数名称,那隐式转换的函数名称是什么呢? 难道函数名就叫bool?
由于反射需要加载程序集,而程序集是C#编译后的产物,也就是说,反射操作的其实是IL代码。此时我们需要看下编译后 隐式转换函数变成了什么。
这里我们知道了函数名以及函数签名,我们只需要捏造一个相同签名的函数(IL代码),并使用InitializeOnLoad
在Unity加载程序集的时候将其替换掉即可。
private static void PatchImplicitConversion()
{
var method = GetImplicitMethod(typeof(UnityEngine.Object), typeof(bool));
var patched = typeof(UnityPatcher).GetMethod(nameof(PatchBoolConversion));
var harmony = new Harmony("com.company.project.product");
var assembly = typeof(UnityEngine.Object).Assembly;
harmony.Patch(method, patched);
Debug.Log("PatchImplicitConversion patch succed");
}
public static bool PatchBoolConversion(UnityEngine.Object exists)
{
Debug.LogWarning($"WARNING: invoking implicit conversion to bool! {invokingAssembly}");
return exists != null;
}
区分调用来源
但是一旦注入成功,我们就会发现Unity引擎到处都在使用隐式转换,我们只需要关注自己的代码。那该怎么办呢?
A StackFrame is created and pushed on the call stack for every function call made during the execution of a thread. The stack frame always includes MethodBase information, and optionally includes file name, line number, and column number information.
Diagnostics命名空间为我们提供了StackTrace,然后我们可以通过GetFrame
获得对应层级的StackFrame栈帧。如果调用方所在的程序集为Unity引擎,则不显示日志,否则显示日志。
System.Diagnostics.StackTrace stackTrace = new System.Diagnostics.StackTrace();
System.Diagnostics.StackFrame stackFrame = stackTrace.GetFrame(2);
var invokingAssembly = stackFrame.GetMethod().DeclaringType.Assembly.FullName;
bool isUnity = invokingAssembly.Contains("Unity");
if (!isUnity)
Debug.LogWarning($"WARNING: invoking implicit conversion to bool! {invokingAssembly}");
性能优化
这样我们能够发现自己编写的代码是否调用了 隐式转换,但是实践检验这样性能不太行,编辑器使用时有迟滞的感觉。
这是因为Unity有很多地方都用了这个隐式转换,并且StackTrace的开销很大,两者结合导致了现在的情况。要么不使用StackTrace,要么减少调用次数。
这里我们采用了每隔几次隐式转换,检查一次StackTrace的方法。
private static int i = 0;
public static bool PatchBoolConversion(UnityEngine.Object exists)
{
i++;
if(i % 20 == 0)
{
System.Diagnostics.StackTrace stackTrace = new System.Diagnostics.StackTrace();
System.Diagnostics.StackFrame stackFrame = stackTrace.GetFrame(2);
var invokingAssembly = stackFrame.GetMethod().DeclaringType.Assembly.FullName;
bool isUnity = invokingAssembly.Contains("Unity");
if (!isUnity)
Debug.LogWarning($"WARNING: invoking implicit conversion to bool! {invokingAssembly}");
}
return exists != null;
}
最终代码
using HarmonyLib;
using System;
using System.Reflection;
using UnityEditor;
using UnityEngine;
[InitializeOnLoad]
public class UnityPatcher
{
static UnityPatcher()
{
DoPatch();
}
private static void DoPatch()
{
PatchImplicitConversion();
}
private static void PatchImplicitConversion()
{
var method = GetImplicitMethod(typeof(UnityEngine.Object), typeof(bool));
var patched = typeof(UnityPatcher).GetMethod(nameof(PatchBoolConversion));
var harmony = new Harmony("com.company.project.product");
var assembly = typeof(UnityEngine.Object).Assembly;
harmony.Patch(method, new HarmonyMethod(patched));
Debug.Log("PatchImplicitConversion patch succed");
}
private static MethodInfo GetImplicitMethod(Type type, Type returnType)
{
var methodList = type.GetMethods();
foreach (var method in methodList)
{
if (method.Name == "op_Implicit" && method.ReturnType == returnType)
{
return method;
}
}
return null;
}
private static int i = 0;
public static bool PatchBoolConversion(UnityEngine.Object exists)
{
i++;
if(i % 20 == 0)
{
System.Diagnostics.StackTrace stackTrace = new System.Diagnostics.StackTrace();
System.Diagnostics.StackFrame stackFrame = stackTrace.GetFrame(2);
var invokingAssembly = stackFrame.GetMethod().DeclaringType.Assembly.FullName;
bool isUnity = invokingAssembly.Contains("Unity");
if (!isUnity)
Debug.LogWarning($"WARNING: invoking implicit conversion to bool! {invokingAssembly}");
}
return exists != null;
}
}