Unity3d底层数据传递分析

本文分析了在Mono框架下,非托管堆、运行时、托管堆如何关联,以及通过哪些方式调用...

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做些分析,但文章内容多成这样,恐怕已然没什么人看,拆分一下吧,但愿不要太监了。

 

参考文献:

http://www.mono-project.com/docs/advanced/embedding/

https://en.wikipedia.org/wiki/Marshalling_(computer_science)

http://www.mono-project.com/docs/advanced/pinvoke/

http://docs.go-mono.com/index.aspx?link=T:System.Runtime.InteropServices.StructLayoutAttribute

https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/fixed-statement

https://msdn.microsoft.com/zh-cn/library/77e6taeh(v=vs.85).aspx

https://docs.microsoft.com/en-us/dotnet/framework/interop/interop-marshaling

http://www.uml.org.cn/c++/201508185.asp、

http://docs.go-mono.com/index.aspx?link=T:System.Runtime.InteropServices.HandleRef

http://docs.go-mono.com/index.aspx?link=F:System.Runtime.InteropServices.LayoutKind.Auto


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

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

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

 

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

 

最新文章
1WeTest携PC&主机游戏质量保障服务和性能测试平台PerfDog亮相Gamescom 2024 以全场景游戏质量保障服务及性能测试解决方案,助力全球游戏行业的创新与发展
2一张图带你了解小程序隐私合规检测 快速了解小程序隐私合规检测如何防范黑灰产风险,守护用户数据安全
3防范小程序隐私合规风险,筑牢用户信任防线 了解隐私合规检测如何帮助小程序规避数据安全风险
4WeTest 海外测试需求有奖问卷活动中奖名单公布 近日,WeTest 海外测试需求有奖问卷活动圆满结束,经过紧张的统计与筛选,以下朋友们中奖,成功获得了我们的门票礼品。
5海外本地化测试的全生命周期服务 第三期 支付测试 海外支付风控升级,非本地测试封号现象频发,真金测试推进困难?来看WeTest的本地化支付测试方案
购买
客服
反馈