无unsafe代码实现C#与C++数组互传及性能优化技术咨询
Unity中无unsafe调用C++ DLL处理纹理数组的最优实现
我完全懂你的处境——在Unity里处理动态生成的纹理(没法从磁盘加载),要转成int/byte/float数组,还想用C++ DLL提速,又因为兼容性要求不能碰unsafe代码,还要尽量复用数组避免内存泄漏。下面给你一套完全贴合需求的可运行方案,连内存管理的坑都帮你踩平了:
核心思路
咱们优先用传入两个数组的方案:C#侧提前创建好输入数组(从纹理转换而来)和输出数组(可复用),直接把两个数组的内存映射给C++,让C++直接修改输出数组的内容——全程不需要Marshal.Copy或者unsafe代码,性能拉满还不会有内存泄漏风险。
C++ DLL实现
首先写C的导出函数,注意要避免C的名字修饰,同时直接操作数组指针:
#include <cstring> // 用extern "C"避免C++名字修饰,确保C#能找到函数 extern "C" { // 导出函数:处理输入数组,直接修改输出数组内容 __declspec(dllexport) void ProcessTextureArray( const float* inputArray, // 输入数组(只读,避免误修改) float* outputArray, // 输出数组(直接修改) int arrayLength // 数组长度,必须和C#侧一致 ) { // 这里替换成你的目标检测逻辑,比如遍历每个元素处理 for (int i = 0; i < arrayLength; ++i) { // 示例:把输入值乘2后写入输出数组 outputArray[i] = inputArray[i] * 2.0f; } } // 如果你的逻辑需要临时分配非托管内存(比如中间缓存),才需要这个释放函数 // 但咱们当前方案用的是C#托管数组,所以这个函数可以不用! // __declspec(dllexport) void FreeTempMemory() { // // 释放你在C++里new的内存 // } }
代码解释
extern "C":禁用C++的函数名 mangling,保证C#能通过函数名正确找到导出方法__declspec(dllexport):标记函数为可导出,让DLL对外暴露这个接口const float* inputArray:把输入数组设为只读,避免误修改原数据float* outputArray:直接操作C#传入的托管数组内存,不需要拷贝,性能极高
Unity C#实现
接下来是C#侧的代码,包括纹理转数组、调用DLL、复用输出数组的逻辑:
using UnityEngine; using System.Runtime.InteropServices; public class TextureProcessor : MonoBehaviour { // 导入C++ DLL的函数,注意CallingConvention要和C++一致(默认C++是cdecl) [DllImport("TextureProcessingDLL", CallingConvention = CallingConvention.Cdecl)] private static extern void ProcessTextureArray( [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] float[] inputArray, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] float[] outputArray, int arrayLength ); // 复用的输出数组,避免每次创建新数组触发GC private float[] _reusableOutputArray; // 纹理尺寸,假设你的动态纹理大小固定 private const int TextureWidth = 2000; private const int TextureHeight = 2000; private int _arrayLength => TextureWidth * TextureHeight; void Start() { // 提前初始化复用的输出数组,只创建一次 _reusableOutputArray = new float[_arrayLength]; } void Update() { // 模拟动态生成的纹理(比如从RenderTexture获取) RenderTexture dynamicRT = GetYourDynamicRenderTexture(); float[] inputArray = ConvertTextureToArray(dynamicRT); // 调用C++ DLL处理数组,直接修改_reusableOutputArray ProcessTextureArray(inputArray, _reusableOutputArray, _arrayLength); // 这里可以用处理后的_reusableOutputArray做后续逻辑,比如转成纹理显示 UpdateTextureFromArray(_reusableOutputArray); } // 把动态纹理转成float数组的方法 private float[] ConvertTextureToArray(RenderTexture rt) { Texture2D tempTex = new Texture2D(rt.width, rt.height, TextureFormat.RGBAFloat, false); RenderTexture.active = rt; tempTex.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0); RenderTexture.active = null; // 获取float类型的像素数组(每个通道0-1范围) float[] pixels = tempTex.GetPixels(); Destroy(tempTex); // 销毁临时Texture2D,避免内存泄漏 return pixels; } // 把处理后的数组转回纹理的方法(可选) private void UpdateTextureFromArray(float[] processedArray) { Texture2D resultTex = new Texture2D(TextureWidth, TextureHeight, TextureFormat.RGBAFloat, false); resultTex.SetPixels(processedArray); resultTex.Apply(); // 比如把纹理赋值给材质 GetComponent<Renderer>().material.mainTexture = resultTex; } // 模拟获取动态生成的RenderTexture private RenderTexture GetYourDynamicRenderTexture() { // 这里替换成你实际的动态纹理生成逻辑 return RenderTexture.active; } }
关键细节解释
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)]:告诉Marshal把C#的托管数组映射成非托管指针,SizeParamIndex = 2指定第三个参数(arrayLength)是数组的长度,保证Marshal能正确处理数组边界_reusableOutputArray:提前创建好的输出数组,每次调用DLL都复用它,避免频繁new数组导致的GC卡顿ConvertTextureToArray:处理动态纹理转数组的逻辑,这里用RenderTexture转Texture2D再取像素,适合动态生成的纹理场景- 无unsafe代码:全程用Marshal的安全映射,完全符合你的兼容性要求
性能对比(参考你的测试数据)
对于400万元素的数组(2000x2000灰度纹理):
- C++ DLL方法:约5ms
- 纯C#循环方法:约23ms
差距主要来自C++的原生编译优化和无托管代码的额外开销,大数据量下优势非常明显。
内存管理注意事项
- 无需额外释放函数:因为我们操作的是C#的托管数组,内存由.NET GC管理,C++没有分配额外的非托管内存,所以不需要
SomeC++MethodToDeAllocateMemory这样的函数,从根源避免了内存泄漏 - 数组长度要严格匹配:C++里不要越界访问数组,一定要保证
arrayLength参数和C#侧数组的实际长度一致 - 纹理格式对应数组类型:
- 如果用
TextureFormat.RGBA32,对应byte[]数组(每个像素4个byte) - 如果用
TextureFormat.RGBAFloat,对应float[]数组(每个像素4个float) - 如果是灰度纹理,可以用
TextureFormat.RFloat对应单通道float[]
- 如果用
内容的提问来源于stack exchange,提问作者M.G.
相关产品推荐
相关产品推荐

