unity反射底层原理

  1. 1. C#对象的内存布局
    1. 1.0.1. 类的成员函数会到哪里去
    2. 1.0.2. this实例的概念
    3. 1.0.3. 类的实例
    4. 1.0.4. t表示一块内存,如何获取每个成员?
  • 2. 什么是反射,反射有什么作用
    1. 2.0.1. 类型描述 对象实例(Type, System)是什么
    2. 2.0.2. 代码

  •  

    C#对象的内存布局

    • 类 : 是一种类型描述,描述了这个类型有哪些数据组成,同时描述一些成员函数
    • 类的实例 : new 类(); 是具体的内存对象,就是一块内存,包含这个类里的数据,这块内存是所有数据成员的集合

    类的成员函数会到哪里去

    • 类的成员函数属于代码指令,编译完成后,会变成代码指令,全局只有一份,所有的类的实例公用一份这个代码指令
    • 存入到代码段 : 编译器把代码编译成.exe的可执行文件,在运行这个文件时,会把里面的所有代码加载到内存的代码段,再跳到main函数里一行一行执行

    this实例的概念

    • 成员函数里面,如果我们使用this,指的就是当前的对象实例
    • 例 :
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      class 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
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26

    /// 业务开发者
    挂上Test组件
    挂上Test2组件
    /// end

    ----------------------------------------------------------

    /// Unity引擎底层开发
    step1 : 加载场景文件,读取数据;
    step2 : 判断组件名是哪种类型,就new出这种类型实例;

    if (name == "Test")
    {
    gameObject.AddComponent<Test>();
    }
    else if (name == "Test2")
    {
    gameObject.AddComponent<Test2>();
    }
    ...

    // 只要业务逻辑每开发一次(如增加一个类),引擎代码就要改一次
    // 这时就需要反射来做
    /// end

    • 上面我们描述一个类,每一个类都有一种类型,都有自己独立的描述
    • 所以会导致我们每新加一个类,就会有多的一种方式来描述
    • 上述问题的本质矛盾是 : 我们没有统一的方式来处理不同的类或类的实例
    • 需要用一种方式来描述任意的类型 :
      • 类的实例是一个内存块,内存块的大小就是这个类的所有数据成员的大小(通过编译器知道内存块大小)
      • 类有哪些数据成员,我可以把这些数据成员的名字,通过数组等其他方式保存起来
        如 :
        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
      21
      class 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
        6
        Type t = new Type();
        t.AddField("age", 0); // 编译器会知道的
        t.AddField("name", "");

        t.AddMethod("test", 成员方法, 地址);
        t.AddMethod("test2", 成员方法, 地址);
      • 编译完成后,就可以根据编译信息,生成一个类型描述对象的数据,存起来,一起写入到.exe
      • 底层就可以使用Type的方式来获得一个类的描述,可以根据类的描述来构建实例,调用方法和成员了
      • 调用底层OS的API来分配一个xxxx大小的内存出来,作为对象实例的内存
      • 调用构造函数,将这个内存块传递给构造函数,构造函数就会帮我们初始化对应的数据
    • 编译每个类时,会为每个类生成一个全局数据,这个全局数据是Type类型,里面存放一个类的描述数据
      • 只需要调用API : System.Type.GetType(“类型名字”) / typeof(T) 根据类型或类型名字获取类型描述对象实例
    • Type类型系统已经给我们定义好了,包含 :
      • FieldInfo : 数据成员信息
      • MethodInfo : 方法信息
    • 通过反射来实例化一个对象 :
      • Type t实例化一个对象出来,根据t这个Type实例
      • API : Activator.CreateInstance
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28

    /// 业务开发者
    挂上Test组件
    挂上Test2组件
    /// end

    ----------------------------------------------------------

    /// Unity引擎底层开发
    step1 : 加载场景文件,读取数据;
    step2 : 判断组件名是哪种类型,就new出这种类型实例;

    // if (name == "Test")
    // {
    // gameObject.AddComponent<Test>();
    // }
    // else if (name == "Test2")
    // {
    // gameObject.AddComponent<Test2>();
    // }
    // ...

    string name = "xxx"; // 从文件读取当前组件的名字
    Type t = System.Type.GetType(name);
    gameObject.AddComponent(t); // 内部有反射实现,通过type添加组件

    /// end

    • Type里存放了每个数据成员的偏移和大小,用这两个数据,就能从对象的内存里面读取/设置成员的数据
      • 从t里获取类型描述FieldInfo,结合t实例把大小、偏移等数据取出来就是数据的值了
      • API : SetValue/GetValue
    • 每个Type里面都存放了成员函数地址
      • MethodInfo = t.getMethod(“名字”);
        Object returnObject = MethodInfo.Invoke(instance, 参数列表);

    代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    using System.Reflection;

    class Test
    {
    public int age;
    public string name;
    public void test(int age, string name)
    {
    this.age = age;
    this.name = name;
    }
    public void test2()
    {
    Debug.Log("test2");
    }
    public int test3(int a, int b, string c)
    {
    return -1;
    }
    }

    public class MyTest : MonoBehaviour
    {
    void Start()
    {
    // step1 : 获取Test的类型描述对象实例
    Type t = System.Type.GetType("Test");

    // step2 : 利用描述对象实例,构建一个对象出来
    var instance = Activator.CreateInstance(t);

    // step3 : 利用存放的数据成员的信息给它们设值
    // instance + (偏移 + 大小)
    FieldInfo[] firlds = t.GetFields();
    // 获取单个
    FieldInfo ageInfo = t.GetFields("age");

    // 对象实例,ageInfo,偏移+大小
    ageInfo.SetValue(instance, 4);

    // test
    Debug.Log((instance as Test).age;


    // 调用成员函数
    // 获取MethodInfo
    MethodInfo m = t.GetMethod("test3");
    // 设置参数
    System.Object[] funcParams = new System.Object[3];
    funcParams[0] = 37;
    funcParams[1] = 2;
    funcParams[2] = "shiki";

    int ret = (int)m.Invoke(instance, funcParams);
    // test
    Debug.Log(ret);
    }
    }