• Unity3d底层数据传递分析


    WeTest 导读

    这篇文章主要分析了在Mono框架下,非托管堆、运行时、托管堆如何关联,以及通过哪些方式调用。内存方面,介绍了什么是封送,以及类和结构体的关系和区别。

     


     

    一、托管交互(Interop)

    在Mono的官方文档(http://www.mono-project.com/docs/advanced/embedding/)中有关于嵌入原理的描述。我们知道Unity3d底层是C++完成的,而C#代码会被编译成CIL(Common Intermediate Language),连接两部分的技术就是MonoRuntime。通常C++部分被称为非托管代码(Unmanaged code),即下图左侧,CIL/.NET部分被称为托管代码(manage code),即下图右侧。

     

     

     

     

    二、封送

     

    在C#中的string,通过内部调用传给C++时,会使用MonoString* ,它是指向托管堆对象的字符串类型指针,这个转换就是封送(Marshalling)。

     

     

     

    具体说来,封送是将对象的内存表示,变换为适合存储或发送的数据格式的过程。

     

     

     

    对于简单的数据类型,例如整数和浮点数等基础类型,封送是隐式的按位拷贝(blitting)。另一种不必封送的情况是指针传递,例如通过引用传递结构体到非托管代码,只会拷贝结构的指针。当然,也可以通过MarshalAs来自定义封送策略。

     

     

     

    需要谨记的是,这两部分内存则完全独立。托管内存分配在GC堆上,非托管内存则完全由C++层的业务代码自己控制。因此堆上的内容被C++访问时,很有可能因为堆的机制被GC掉了。为了防止出现这种情况,可以使用C#的fixed关键字来单边锁定变量。

     

     

     

    在P/Invoke模式中没有使用fixed,而采用另一种常见的托管到非托管的封送方式:

     

    1. Runtime分配一块非托管内存。

     

    2. 托管类数据拷贝到刚申请的非托管内存中。

     

    3. 调用非托管方法时,使用上面的非托管内存数据,而不是原始托管内存数据。这样做是为了,当GC发生时,非托管内存是可用的。

     

    4. 将非托管内存拷回托管内存。

     

     

     

    因为不能确定托管堆中的内存会何时失效,在非托管代码中,我们不应该缓存任何托管代码传进来的数据。

     

     

     

    另一种情况是返回值,类在非托管代码中,不可以作为值返回,只可以返回指针。因为堆内容无法互通,当返回到托管代码时,会经历以下步骤:

     

    1. 托管代码调用非托管代码,返回了指向在非托管内存中的结构体的指针。

     

    2. 在托管代码中找到对应的托管类并实例化,将非托管内容封送到托管类中。

     

    3. 非托管代码中的内存被Marshal.FreeCoTaskMem()函数释放。

     

     

     

    想要避免这种内存分配,可以返回一个IntPtr,并且用Marshal类方法操作指针。关于类与结构体,在后面有更详细的论述。

     

     

     

     

     

    三、跨域调用

     

    托管代码能通过以下两种方式调用C++,即P/Invoke与内部调用(Embedding)。

     

     

     

    P/Invoke

     

    使用P/Invoke调用方式,需要将C++函数声明为public。例如:

     

    然后在C#层添加下面的声明即可:

     

     

    通过__Internal关键字可以令Mono在当前执行的非托管代码中查找函数,通过自扩展的Marshalling,可以适配大量的数据类型,是最简单的Interop方式。

     

     

     

    内部调用

     

    内部调用是在C++中注册调用,并直接访问托管对象,控制Marshall。例如,我们要返回字符串,就先要在C++中显示注册接口。

     

     

     

    然后在C#中声明下面的函数:

     

     

    最后实现在C++中实现这个函数:

     

     

    通过MonoString和mono_string_new,即完成了字符串的Marshalling过程。

     

    四、内存分配

     

    类与结构体

    对于托管代码与非托管代码,类与结构体有不一样的传递方法。

     

    1、类的传递

    类是在托管堆上分配的,因此不能以值类型传给非托管代码,而只能传引用。以代码举例来说:

     

    对于下面的非托管代码:

     

    一个可用的类包装(class wrapper),可以是:

     

    在托管代码中,我们需要指定类的数据格式,默认是LayoutKind.Auto。这种分配方式下,运行时会自动选择合适的内存布局来创建非托管内存,因此内存结构不能被外部所知。我们可以使用LayoutKind.Sequential或LayoutKind.Explicit来指定内存分配策略。例如托管代码的定义还可以这样写:

     

     

    另外,类方法有自己的封送方式。正如前面提到的,很多数据是借助Marshaling进行访问。如果需要制定拷贝规则,要指定关键字[In],[Out],[In,Out],传递方向如下图所示:

     

    当不指定这些属性时,就会根据数据类型(Value或Reference)来决定拷贝方式。

     

    例如,引用类型(类,数组,字符串,接口)作为值传递时,出于性能考虑会被标注为[In]。这也是默认标记,即不做从非托管拷贝回托管的操作。

     

    2、结构体的传递

    结构体与类有两点不同:

    1. 结构体分配在运行时的栈上(Runtime Stack)。

    2. 默认使用Sequential,非托管代码使用时不需要额外设置属性。

     

    在把结构体传递给非托管代码时,有些情况下不会产生内存拷贝:

    1. 作为值传递时,结构分配在栈上,并且是可比特化类型(blittable types)

    2. 作为引用传递

     

    在上述情况下,不需要指定[Out]作为关键字。反过来说,如果结构体中包含不可比特化的类型,例如:System.Boolean,System.String,或者array,就需要自己完成Marshalling了。

     

    依照上面的非托管代码定义,结构体包装可以是:

     

     

     

     

     

    结构体在非托管代码中,可以作为值返回,但不可以返回ref或out。所以要想返回指向结构的指针,就必须使用IntPtr,或在外部定义unsafe。如果使用IntPtr做返回值,可以用Marshal.PtrToStructure系列函数,将指针转换为托管结构体。

     

     

     

    成员变量

     

    对于类与结构体的成员变量,乖巧的做法是:不要将包含引用类型(比如说类)的类或结构体传给非托管代码。因为非托管代码不能安全的操作非托管引用,托管代码也不一定会深封送数据。因此,打包类中最好不包含数组对象,尤其是string。当然,如果无法绕开,就需要自定义封送。

     

     

     

    例如:

     

     

     

    或者:

     

    需要注意的是,如此使用必须保证托管代码中有内存分配,例如:

     

     

     

     

     

     

    五、GC安全

     

    由于Marshalling是通过数据拷贝实现的,仔细看来其实不太靠谱。如上面所说,通常会用IntPtr和unsafe特性来处理封送拷贝问题。但指针来说,需要注意避免在函数运行时被垃圾回收掉。例如下面的代码:

     

    执行完c.m()后,GC就会回收C的实例。很有可能非托管代码中的C.OperatOnHandle依然在使用_handle,因为已经跨界了,托管代码是不可能知道这件事的。解决办法是在这种情况下使用HandleRef来替代IntPtr。它可以保证直到非托管代码调用结束之后才GC托管对象。在.NET2.0中,我们也可以查阅文档(http://www.mono-project.com/docs/advanced/safehandles/)使用SafeFileHandle或者SafeWaitHandle。

     

     

    既然我们要持有,那就要肩负起从托管代码释放非托管代码的责任。简单的做法是,确保所有资源的包装类中都有释放函数,并在使用完成后调用。如果不希望等待统一的GC,可以使用

     

     

    来防止对象进入析构队列,直接回收资源。

     

    如果觉得手动调用析构不放心,可以用using块来包围,以确保在块结束时自动释放,代码大致如下:

     

     

    最后提醒一下,由于继承会提升GC权重(promote GC generation),包装类要尽量避免使用虚函数或作为非封存类(non-sealed calss)。如果释放的成员变量是包含其他对象的ArrayList,那么这个List、容器中的子对象、子对象中递归引用的对象,都会被提升GC权重。我们都知道,GC权重越大,被回收的速率越慢。所以优化的策略是:每个析构类都是叶子结点,主干是则是由这些互不引用的叶子组成的树。

     

    六、总结

    篇文章主要分析了在Mono框架下,非托管堆、运行时、托管堆如何关联,以及通过哪些方式调用。内存方面,介绍了什么是封送,以及类和结构体的关系和区别。本来准备结合Unity3D做些分析,但文章内容多成这样,恐怕已然没什么人看,拆分一下吧,但愿不要太监了。

     

     


    UPA—— 一款针对Unity游戏/产品的深度性能分析工具,由腾讯WeTest和unity官方共同研发打造,可以帮助游戏开发者快速定位性能问题。旨在为游戏开发者提供更完善的手游性能解决方案,同时与开发环节形成闭环,保障游戏品质。

    点击http://wetest.qq.com/cube/ 即可使用。

     

    对UPA感兴趣的开发者,欢迎加入QQ群:633065352

     

    如果对使用当中有任何疑问,欢迎联系腾讯WeTest企业QQ:800024531

     

     

     

     

     

  • 相关阅读:
    [sql查询] 重复数据只取一条
    SSIS,参数坑
    数据仓库之建立多维数据库
    数据仓库之SSIS开发
    开发规范
    页面以base64输出图片
    内嵌iframe
    T-Sql编程基础
    MVC3.0----整理之一
    原生JS 表单提交验证器
  • 原文地址:https://www.cnblogs.com/wetest/p/8627351.html
Copyright © 2020-2023  润新知