C#对象的内存布局
- 类 : 是一种类型描述,描述了这个类型有哪些数据组成,同时描述一些成员函数
- 类的实例 : new 类(); 是具体的内存对象,就是一块内存,包含这个类里的数据,这块内存是所有数据成员的集合
类的成员函数会到哪里去
- 类的成员函数属于代码指令,编译完成后,会变成代码指令,全局只有一份,所有的类的实例公用一份这个代码指令
- 存入到代码段 : 编译器把代码编译成.exe的可执行文件,在运行这个文件时,会把里面的所有代码加载到内存的代码段,再跳到main函数里一行一行执行
this实例的概念
- 成员函数里面,如果我们使用this,指的就是当前的对象实例
- 例 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class Test
{
public int age;
public string name;
public void test(int age, string name)
{
this.age = age;
this.name = name;
}
public void test2()
{
...
}
}
Test t = new Test();
t.test(37, "shiki"); - 调用这个成员函数.test()的时候,我们会自动给成员函数,把当前的对象实例作为this传入进去
- this.age操作的就是当前的对象实例这块内存
- 底层就会把t这个实例(这块内存)传递给this(this指向这块内存),通过this来操作
类的实例
- 类的数据成员的所有数据(看得见的,age,4个字节,name,引用类型变量,在32位系统下4个字节)
- 看不见的数据 : 编译器的对齐等
t表示一块内存,如何获取每个成员?
- 编译器会根据一些编译的选项、数据的类型,在写好代码以后,定死一个数据成员相对这个类的内存的偏移,如age在对象实例里面,内存偏移多少,大小为age的类型(int 4字节)
- 当我们编写好一个类型后,编译器会知道 :
- 每个数据的相对于对象实例内存块的偏移
- 每个类的成员函数在代码段的偏移,因为最终是要把这些成员函数编译成机器指令,在运行时就可以让指令直接跳转到这里进行函数调用
什么是反射,反射有什么作用
以Unity引擎为例
- 编辑器上挂脚本,我们给脚本初始化数据
- 编辑完了以后,保存到场景文件里面
- 运行的时候,根据场景文件里面的内容,游戏引擎把这个节点和组件实例new出来
1 |
|
- 上面我们描述一个类,每一个类都有一种类型,都有自己独立的描述
- 所以会导致我们每新加一个类,就会有多的一种方式来描述
- 上述问题的本质矛盾是 : 我们没有统一的方式来处理不同的类或类的实例
- 需要用一种方式来描述任意的类型 :
- 类的实例是一个内存块,内存块的大小就是这个类的所有数据成员的大小(通过编译器知道内存块大小)
- 类有哪些数据成员,我可以把这些数据成员的名字,通过数组等其他方式保存起来
如 :1
2
3数据成员数组 :
{"age", type int, 在对象里面偏移为0个字节}
{"name", type string, 在对象里面偏移为4个字节} - 类有哪些成员函数
如 :1
2{"test", type 成员函数(静态函数), 在代码段的位置}
{"test2", type 成员函数, 在代码段的位置}
- 任意的类都可以转化成一种描述
- 定义了这样一种描述方式,我们就解决了上面的问题,我们用统一的方式来描述任意不同的类型
类型描述 对象实例(Type, System)是什么
- 每个类,编译器都知道,数据成员的偏移、函数代码段的位置
- 运行的时候,C#系统会为每个类生成描述实例(数据内存),称作Tpye实例;描述类的类型,称作Type类型,属于System名字空间
- Type : 一些类型的描述信息
伪代码 :1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class FiledData
{
string filedName; // 名字
int type; // 类型
int fieldSize; // 这个字段的内存大小
int offset; // 在内存对象中的内存偏移
}
class MethodData
{
string methodName; //方法名
int type; // 类型,静态的还是普通的
int offset; //函数代码指令的地址
}
class Type
{
int memSize; // 当前类的实例的内存大小
List<FiledData> datas; // 当前类的数据成员
List<MethodData> funcs; // 当前类的所有成员函数
}- 描述Test :
1
2
3
4
5
6Type t = new Type();
t.AddField("age", 0); // 编译器会知道的
t.AddField("name", "");
t.AddMethod("test", 成员方法, 地址);
t.AddMethod("test2", 成员方法, 地址); - 编译完成后,就可以根据编译信息,生成一个类型描述对象的数据,存起来,一起写入到.exe
- 底层就可以使用Type的方式来获得一个类的描述,可以根据类的描述来构建实例,调用方法和成员了
- 调用底层OS的API来分配一个xxxx大小的内存出来,作为对象实例的内存
- 调用构造函数,将这个内存块传递给构造函数,构造函数就会帮我们初始化对应的数据
- 描述Test :
- 编译每个类时,会为每个类生成一个全局数据,这个全局数据是Type类型,里面存放一个类的描述数据
- 只需要调用API : System.Type.GetType(“类型名字”) / typeof(T) 根据类型或类型名字获取类型描述对象实例
- Type类型系统已经给我们定义好了,包含 :
- FieldInfo : 数据成员信息
- MethodInfo : 方法信息
- 通过反射来实例化一个对象 :
- Type t实例化一个对象出来,根据t这个Type实例
- API : Activator.CreateInstance
1 |
|
- Type里存放了每个数据成员的偏移和大小,用这两个数据,就能从对象的内存里面读取/设置成员的数据
- 从t里获取类型描述FieldInfo,结合t实例把大小、偏移等数据取出来就是数据的值了
- API : SetValue/GetValue
- 每个Type里面都存放了成员函数地址
- MethodInfo = t.getMethod(“名字”);
Object returnObject = MethodInfo.Invoke(instance, 参数列表);
- MethodInfo = t.getMethod(“名字”);
代码
1 | using System.Reflection; |