使用Harmony检测Unity.Object的隐式转换

发布时间 2023-12-08 23:05:43作者: dewxin

简介

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;
    }
}