【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# 通过反射获取私有方法


__EOF__

本文作者OtusScops
本文链接https://www.cnblogs.com/OtusScops/p/16746498.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   AshScops  阅读(1988)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示