【unity】反射机制

前言

很久之前就听说过反射,但不甚理解,今天看到底层终于领悟,遂记录一下相关内容。

C#反射

什么是反射

借用光学中的反射(Reflection)之名,C#中的反射是从对象外部去获取对象内部的各种信息。

这像极了利用波来探测某样物体内部结构,比如金属容器探伤、B超检测体内状况等。

反射是从对象外部获取对象内部的各种信息,具体做法和用途请看如下例子。

以Unity引擎为例,我们来看Unity引擎是如何利用反射机制的。

Unity引擎的反射机制

为Unity编辑器中的游戏对象挂载脚本并编辑好数据,如下(图片仅作为解释说明的例子,不用跟着操作)。
image

public class ReflectionTest : MonoBehaviour
{
    public int hp = 100;
    public int atk = 10;

    public int GetHit(int dmg)
    {
        this.hp -= dmg;
        Debug.Log("GetHit");
        return 10086;
    }

    public void Healing(int healNum)
    {
        this.hp += healNum;
        Debug.Log("Healing");
    }
}

对象上挂载的脚本及其相关数据会保存至场景文件中,如组件名、组件成员变量、成员函数等。

运行时,引擎会加载场景文件,读取场景文件中的数据,实例化节点和组件。

对于客户端开发者来说,新增脚本几乎不可避免;

而对于Unity引擎底层开发来说,加载场景文件时,需要根据组件名来获取类的类型,并对其实例化,挂载到对应游戏对象上。

如果不使用反射,那么每当客户端程序员新增一个脚本时,引擎底层开发人员必须改动相关代码,大抵如下:

string name = "当前组件名";

if(name == "Reflection")
    gameObject.AddComponent<Reflection>();
else if(name == "xxx")
...

于是引入反射来解决这个问题。

反射是怎么做的?

用一种方式来描述任意的类型。即对于每一个类,都建立起一个统一的规则化的描述,使得任意类及其实例均能用该描述来处理。

如何描述一个类?

  1. 类的实例占有一部分内存,其大小就是该类中所有数据成员的大小。那么描述中可以包含类实例的内存大小。

  2. 类的数据成员,其描述大抵如下。

    {"hp" , type int , 偏移0个字节}

  3. 类的成员函数,其描述大抵如下。

    {"GetHit" , type 成员函数(静态函数) , 在代码段的位置...}

如何描述一个类的实例?

​ C#为每个类的实例都创建了描述实例,它为Type类型,在System命名空间下。其结构大抵如下。

class FieldInfo
{
	string filedName;
	int type;
	int filedSize;//该字段内存大小
	int offset;//内存偏移
}
class MethodInfo
{
	string methName;
	int type;//静态/普通
	int offset;//函数代码指令的地址
	
}
class Type
{
	int memSize;//当前类的实例的内存大小
	List<FieldInfo> datas;
	List<MethodInfo> funcs;
}

若要描述上文中的类ReflectionTest,其步骤大抵如下。

Type t = new Type();
t.addFiled("hp" , 100);
t.addFiled("atk" , 10);
t.addMethod("GetHit" , 成员方法 , 地址);
t.addMethod("Healing" , 成员方法 , 地址);

编译完成后,我们可以根据编译后的信息,为每个类生成一个描述,写入.exe中。

这样一来,引擎底层就可以根据类的描述来构建实例,访问成员,调用方法了。

string name = "当前组件名";
Type t = System.Type.GetType(name);//它会返回name对应类的描述,这也是为什么脚本不能重名
gameObject.AddComponent(t);

调用底层OS的API来分配一个xxx大小的内存,作为对象实例的内存。

调用构造函数,将该内存块传递给构造函数,初始化对应数据。

总结

  1. 编译每个类时,会为每个类生成一个Type类型的全局数据,其中存放了描述。

    API System.Type.GetType("类型名") typeof(T) 根据类型名或类型来获取 描述对象实例。

  2. 系统已经定义Type类型:FieldsInfos;MethodInfos。

  3. 通过反射来实例化一个对象。API:Type t -> new relation obj

    Activator.CreateInstance (Type)

  4. 在Type中存放了每个数据成员的偏移和大小,因此可以从对象的内存中读取/设置数据的值。

  5. 在Type中存放了每个成员函数的地址。

    methodInfo = t.getMethod("函数名");

    Object returnObj = methodInfo.Invoke(instance , 参数列表);

反射演示

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;

public class ReflectionTest : MonoBehaviour
{
    public int hp = 100;
    public int atk = 10;

    public int GetHit(int dmg)
    {
        this.hp -= dmg;
        Debug.Log("GetHit");
        return 10086;
    }

    public void Healing(int healNum)
    {
        this.hp += healNum;
        Debug.Log("Healing");
    }

    private void Start()
    {
        //获取ReflectionTest的类型描述对象实例
        Type t = Type.GetType("ReflectionTest");

        //利用描述实例化一个对象
        var instance = Activator.CreateInstance(t);

        //利用存放的数据成员描述信息为其赋值
        //instance + 偏移 + 大小
        //FieldInfo[] fields = t.GetFields();

        FieldInfo hp = t.GetField("hp");
        hp.SetValue(instance, 50);

        Debug.Log((instance as ReflectionTest).hp);

        //调用成员函数
        MethodInfo m = t.GetMethod("GetHit");
        System.Object[] funcParas = new System.Object[1];
        funcParas[0] = 10;
        System.Object ret = m.Invoke(instance , funcParas);
        Debug.Log(ret);
    }
}

运行结果如下:

image

出现的问题

当我把数据成员或成员函数设置为私有时,上述代码将无法访问对应数据成员或成员函数。

查阅资料后发现需要使用BindingFlags类型枚举才能访问到私有成员,如下。

//通过反射来调私有的成员
Type type = typeof(ReflectionTest);
//BindingFlags类型枚举,BindingFlags.NonPublic | BindingFlags.Instance 组合才能获取到private私有方法
MethodInfo methodInfo = type.GetMethod("GetHit", BindingFlags.NonPublic | BindingFlags.Instance);

参考资料

C#反射机制 - 知乎 (zhihu.com)

C#中的反射到底用在哪,通俗解释(unity)、反射的概念及用法

一节课搞懂c#反射内部原理

c# 通过反射获取私有方法

posted @ 2022-09-30 22:57  AshScops  阅读(1895)  评论(0编辑  收藏  举报