【unity】反射机制
前言
很久之前就听说过反射,但不甚理解,今天看到底层终于领悟,遂记录一下相关内容。
C#反射
什么是反射
借用光学中的反射(Reflection)之名,C#中的反射是从对象外部去获取对象内部的各种信息。
这像极了利用波来探测某样物体内部结构,比如金属容器探伤、B超检测体内状况等。
反射是从对象外部获取对象内部的各种信息,具体做法和用途请看如下例子。
以Unity引擎为例,我们来看Unity引擎是如何利用反射机制的。
Unity引擎的反射机制
为Unity编辑器中的游戏对象挂载脚本并编辑好数据,如下(图片仅作为解释说明的例子,不用跟着操作)。
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")
...
于是引入反射来解决这个问题。
反射是怎么做的?
用一种方式来描述任意的类型。即对于每一个类,都建立起一个统一的规则化的描述,使得任意类及其实例均能用该描述来处理。
如何描述一个类?
-
类的实例占有一部分内存,其大小就是该类中所有数据成员的大小。那么描述中可以包含类实例的内存大小。
-
类的数据成员,其描述大抵如下。
{"hp" , type int , 偏移0个字节}
-
类的成员函数,其描述大抵如下。
{"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大小的内存,作为对象实例的内存。
调用构造函数,将该内存块传递给构造函数,初始化对应数据。
总结
-
编译每个类时,会为每个类生成一个Type类型的全局数据,其中存放了描述。
API System.Type.GetType("类型名") typeof(T) 根据类型名或类型来获取 描述对象实例。
-
系统已经定义Type类型:FieldsInfos;MethodInfos。
-
通过反射来实例化一个对象。API:Type t -> new relation obj
Activator.CreateInstance (Type)
-
在Type中存放了每个数据成员的偏移和大小,因此可以从对象的内存中读取/设置数据的值。
-
在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);
}
}
运行结果如下:
出现的问题
当我把数据成员或成员函数设置为私有时,上述代码将无法访问对应数据成员或成员函数。
查阅资料后发现需要使用BindingFlags类型枚举才能访问到私有成员,如下。
//通过反射来调私有的成员
Type type = typeof(ReflectionTest);
//BindingFlags类型枚举,BindingFlags.NonPublic | BindingFlags.Instance 组合才能获取到private私有方法
MethodInfo methodInfo = type.GetMethod("GetHit", BindingFlags.NonPublic | BindingFlags.Instance);