UPL5-6——本机-托管的桥接

本机-托管的桥接

本机-托管的桥接(Native-Managed Bridge)指的是 C# 的托管代码与 C/C++ 的本机代码之间的相互调用或通信机制
Unity 本身就是由 C++ 实现的引擎内核
而我们日常写的是 C# 脚本,它们之间必须有桥梁来通信

  • 托管(Managed):

    主要指的是 C# 代码,它的执行环境是 CLR、Mono、IL2CPP

  • 本机(Native):

    主要指的是 C/C++ 代码,它的执行环境为直接运行于 CPU(无虚拟机概念)

桥接的产生
Unity 在执行 C# 脚本时,需要让 C# 能调用到底层引擎的 C++ 函数(比如渲染、物理),这就产生了两个方向的桥接

  1. 托管 到 本机

    即 C# 调用 Unity 引擎内部的 C++ 函数
    比如访问 Unity 提供的 GameObject​、MonoBehaviour 类中的各种属性和方法
    这是通过 IL2CPP 或 Mono 的内部调用机制来桥接到 Unity 的 C++

  2. 本机 到 托管

    即 C++ 层主动调用 C# 方法,比如原生插件调用 Unity 脚本中注册的事件,Android / iOS 的原生代码调用 Unity 脚本方法等

总结:
Unity 中的本机-托管的桥接就是 C++ 与 C# 之间相互调用的通道机制
我们在对 Unity 的 GameObject​、Transform​、Component​ 等进行操作时
有很多操作都会触发桥接的调用,即便你在 C# 层写的是简单的属性访问,很可能发生了隐形的跨域调用

比如:

  1. 访问 transform​ 中的 position

    1
    2
    Vector3 p = this.transform.position;
    this.transform.position = p;
  2. 添加组件 gameObject.AddComponent<T>()

  3. 获取组件 GetComponent<T>()

  4. 获取子物体 transform.GetChild

  5. 激活/失活物体 gameObject.SetActive()

  6. 查找物体 GameObject.Find

等等

本机-托管的桥接带来的性能消耗

  1. 上下文切换

    C# 调 C++ 或反过来,都需要堆栈转换、线程状态保存

  2. 数据封送

    Vector3 等结构体需要从 C++ 拷贝到 C# 层,或反之

  3. GC 压力

    封送中生成临时对象可能在托管堆上分配新的内存,产生垃圾

  4. 调试困难

    Profiler 只能显示 C# 或 C++ 的一侧,桥接代码不易追踪

等等

优化建议

  1. 不要频繁操作 GameObject​、Transform​、Component 相关属性

    例如:同一帧对 transform.position​ 的多次修改应先缓存,再统一写回
    例如:Vector3 v = transform.position;
    先进行相关的位置计算,最终计算完毕后再赋值回去 transform.position = v;

  2. 不要在 Update​ 频繁调用 GetComponent<T>() 等组件查找方法

    推荐在初始化阶段缓存引用,避免在 Update 中重复获取

  3. 不要每帧遍历 transform.GetChild() 获取子物体

    子物体应在初始化阶段获取并缓存

  4. 逻辑和表现分离,弱化桥接路径

    避免游戏逻辑直接操作 GameObject,转而使用纯数据结构(位置、状态等),最后集中批量同步回 Unity

  5. 自定义托管对象池,降低桥接生命周期成本

    桥接调用也和生命周期有关,例如频繁调用 AddComponent​、Instantiate​ 会从本机层动态分配新对象,开销很大,使用对象池复用 GameObject 和组件

等等

总之,就是要想方设法的减少 本机-托管桥接 的成本
频繁访问建议 缓存引用,减少跨域访问,需要做大量位置、方向运算时,应优先在托管代码中处理后统一回写