zoukankan      html  css  js  c++  java
  • 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

     

     

     

     

     

  • 相关阅读:
    javaweb请求编码 url编码 响应编码 乱码问题 post编码 get请求编码 中文乱码问题 GET POST参数乱码问题 url乱码问题 get post请求乱码 字符编码
    windows查看端口占用 windows端口占用 查找端口占用程序 强制结束端口占用 查看某个端口被占用的解决方法 如何查看Windows下端口占用情况
    javaWeb项目中的路径格式 请求url地址 客户端路径 服务端路径 url-pattern 路径 获取资源路径 地址 url
    ServletRequest HttpServletRequest 请求方法 获取请求参数 请求转发 请求包含 请求转发与重定向区别 获取请求头字段
    HttpServletResponse ServletResponse 返回响应 设置响应头设置响应正文体 重定向 常用方法 如何重定向 响应编码 响应乱码
    Servlet主要相关类核心类 容器调用的过程浅析 servlet解读 怎么调用 Servlet是什么 工作机制
    linq查询语句转mongodb
    winddows rabbitmq安装与配置
    Redis For Windows安装及密码
    出现,视图必须派生自 WebViewPage 或 WebViewPage错误解决方法
  • 原文地址:https://www.cnblogs.com/wetest/p/8627351.html
Copyright © 2011-2022 走看看