五一节要劳逸结合,玩了一天晚上也要学习~毕设也忙完了,最近应该会开始更新博客了。
记录一下今晚公开课学到的class="superseo">unity中c#的底层原理
先看一看反射的概念
在程序运行时,动态获取 程序集, 类型(class,interface)和类型的成员信息(方法,字段,属性等)。
在程序运行时,动态创建 类型实例, 以及调用和方法 动态创建出来的 类型实例的成员。
首先我们需要明白内存机制,四大内存空间(堆、栈、全局变量、代码数据);借由类的生成来记录一下吧,类是程序员自己创建的所以自然内存是在堆内,而类当中的方法和非全局变量却不是在堆内的,而是存储在代码数据块内的。
Mycalss t = new Myclass();
t.equalnum(1,2,"3")
public void equalnum(int a,int b,string c)
{
this.a = a; this.b = b; this.c = c;
}
调用equalnum这个成员函数的时候,系统会自动把当前对象的实例作为this指针指向的对象。 t==》对象实例[即一段内存地址] ==》this,this指向该段地址。
那么接下来我们再思考一下,我们写好的类挂载到unity引擎上面,并给脚本初始化数据;然后就是保存到场景文件中。当我们运行程序的时候,我们就会根据场景文件里的内容,游戏引擎把这个节点和组件new出来。
class Myclass1;
class Myclass2;//假如有多个类挂载到程序中
//引擎底层运行时就需要判断类名是否一致然后才确定运行某个组件
if(ClassName == "Myclass1")
------执行class1的逻辑
else if(ClassName == "Myclass2")
-----执行class2的逻辑
else if......//等等
这样虽然能够运行正确且我们容易理解,但是对于引擎底层和执行来说如果是海量的组件和类这样会不会效率低了很多而且麻烦。那么就引起我们的思考有没有一种方法:我们采用一个统一的方式来处理所有不同的类或类的实例呢? 即所有任意的类,都可以转化为一种描述!
1.类的实例本质是一个内存块,这个内存块的大小即代表了这个类中所有的数据内存总和大小(即描述内存地址和大小)
2.类中有哪些数据成员,我们可以把数据成员的名字,通过数组列表等方法保存起来 (即描述数据成员的名字)
例如我们第一个类MyClass有三个成员变量一个成员方法,那么依照刚刚的方法
{
"a" : type:int ,在类中地址 内存为4个字节
"b" : type:int ,在类中地址 内存为4个字节
"c" : type:string,在类中地址 内存为8个字节
"equalnum" : type:成员函数 , 在代码数据块的位置
}
这样不管我们的类有多少方法和变量都可以统一的格式去描述了。
3.类型描述:
对象实例(Type) 既然我们类中的信息都描述了,那么往上一层我们也应该记录类名的信息和类型,也应该用同样的思想去描述:
每一个类,我们的编译器都知道,数据成员在代码数据块的内存地址和内存大小;运行的时候,c#系统会为我们每一个类描述实例(即记录内存信息);Type类型,Type实例属于System命名空间
class FiledData {
string filedName;
int type; //类型
int filedSize; //这个字段的内存大小;
int offset; //在内存对象中的内存偏移;
}
class MethodData {
string methName;
int type; //静态的还是,普通的;
int offset; //函数代码指令的地址;
}
class Type {
int memSize; //当前类的实例的内存大小;
List< FiledData> datas; //当前这个类的数据成员;
List < MethodData> funcs; //当前这个类的所有的成员函数;
}
到这里我们基本就从成员数据到类的描述都构思好了,那么接下来继续用第一个类Myclass来举例子看看底层的描述
Myclass t = new Myclass()
t.addFiled("a",1);
t.addFiled("b",2);
t.addFiled("c","3");
t.addMethod("equalnum",成员方法,地址);
底层编译完之后,我们就可以根据我们编译的信息,来为每一个类,生成对其描述的数据存储起来,写入到.exe中(即代码数据块内存的最终去向)
这样我们就可以直接通过Type来获得任何一个类的描述信息了。引擎底层也就不需要一直用if判断是否类名一致,直接就可以根据类的描述信息来构造实例,并调用其方法和成员了。
调用底层OS的API来分配一个一定大小的内存空间,作为对象实例的内存;然后调用构造函数,将这个内存传递给构造函数,构造函数就能够精准的将数据初始化了。
那么把上面的全部理解之后,就可以很轻松的消化反射了(好像都快忘了我是在学习反射了),我们现在加上反射的思想把逻辑梳理一遍
(1): 编译每个类的时候,我们会为每个类生成一个全局数据,这个全局数据Type类型,里面存放一个类的描述数据;
API System.Type.GetType("类型的名字”) typeof(T) 根据类型或类型名字来获取我们的类型描述对象实例;
(2):Type类型系统已经给我们定义好了;
FieldsInfos:数据成员信息;
MethodInfos;
(3):通过反射来实例化一个对象:
API:Type t -->实例化一个对象出来;
new Myclass();
Activator.Createlnstance
(4):我们Type里面存放了每个数据成员的内存地址,和内存大小,所以用这两个数据,就能从对象的内存里面读取/设置成员的数据
(1) t–>类型的描述FieldInfo,大小,偏移;
(2)结合这个实例,[内存地址, 内存大小] —》 取出来就是数据的值了; --> SetValue/GetValue;
(5):每个Type里面都存放了我们成员函数地址;
methodInfo = t.getMethod(“名字”);
Object returnObject = methodInfo.Invoke(instance,参数列表);
我们再返回头来看看反射的概念就很好理解了
在程序运行时,动态获取 程序集, 类型(class,interface)和类型的成员信息(方法,字段,属性等)。
在程序运行时,动态创建 类型实例, 以及调用和方法 动态创建出来的 类型实例的成员。
教程链接:https://www.bilibili.com/video/BV1x44y1J7ok?spm_id_from=333.337.top_right_bar_window_custom_collection.content.click
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)