在Unity NativeArray与Span之间零拷贝互转的两种方法
解读
国内一线厂面试时,这道题不是考“会不会写”,而是考“敢不敢写、写得安不安全”。
NativeArray 与 Span 都是 unmanaged memory 的视图,二者互转必须满足:
- 绝对零拷贝(不 new、不 ToArray、不 Marshal);
- 线程与 Burst 安全;
- 在 Unity 2021 LTS 以后版本可编译通过;
- 对 IL2CPP 与 AOT 友好。
如果答“用 ToArray 再 AsSpan”直接挂;答“用 unsafe 指针但说不清生命周期”也挂。
必须给出 两种官方或半官方零拷贝路线,并指出各自的 生命周期管理细节 与 平台陷阱。
知识点
- NativeArray<T>.GetUnsafePtr() / GetUnsafeReadOnlyPtr()
返回 void*,生命周期与 NativeArray 绑定,Burst 兼容。 - Span<T> 的构造函数
public Span(void* pointer, int length) 仅在 unsafe 上下文生效,需要 Unity 2021.2+ 才带此 API。 - UnsafeUtility.AsRef<T> 与 UnsafeUtility.AsSpanRef<T>
Unity 2022.1 新增,内部直接 reinterpret_cast,不分配 GC。 - IL2CPP 陷阱
Span<T> 带指针构造必须加 [IL2CPP] 的 UnsafeUtility.SkipInit 或 Unity.Burst.CompilerServices.SkipLocalsInit,否则 AOT 会插零初始化,产生拷贝。 - Jobs/Burst 约束
在 IJobParallelForBurst 里只能用 readonly 或 writeonly 的 NativeArray,因此 Span 必须声明为 readonly ref 或 ref readonly,否则编译失败。 - Mono 与 Android ARM32 对齐
若 T 含 bool/decimal,需保证 sizeof(T)%4==0,否则 ARM32 下 Span 索引器会崩溃。
答案
方法一:unsafe 指针构造(Unity 2021.2+ 官方路线)
public static Span<T> AsSpan<T>(this NativeArray<T> na) where T : unmanaged
{
unsafe
{
return new Span<T>(na.GetUnsafePtr(), na.Length);
}
}
要点
- 调用端必须加 unsafe 关键字,Assembly 勾选 Allow Unsafe。
- 返回的 Span 生命周期 不能超过 NativeArray;一旦 NativeArray.Dispose,Span 立刻悬空。
- 在 IL2CPP Release 打包时,需在 Assets/link.xml 里保留 System.Span<T> 的完整泛型实例,防止 AOT 裁剪。
方法二:UnsafeUtility.AsSpanRef(Unity 2022.1+ 零拷贝封装)
public static Span<T> AsSpan<T>(ref this NativeArray<T> na) where T : unmanaged
{
unsafe
{
ref T head = ref UnsafeUtility.AsRef<T>(na.GetUnsafePtr());
return MemoryMarshal.CreateSpan(ref head, na.Length);
}
}
要点
- ref NativeArray 保证调用者不能传入临时副本,杜绝 Dispose 后悬针。
- MemoryMarshal.CreateSpan 是 .NET Standard 2.1 正式 API,IL2CPP 已完整支持,无需额外 link.xml。
- Burst 1.8+ 可内联此函数,性能与指针方案相同,但 无需 unsafe 块,仅需 Unity.Burst.CompilerServices.SkipLocalsInit 特性即可。
两种方法对比
- 方法一在 Unity 2021 LTS 即可上线,国内存量项目最多;
- 方法二在 Unity 2022.3 LTS 后成为 官方推荐,代码更短且对 Roslyn Analyzers 友好,无需 unsafe 关键字,通过 ref + MemoryMarshal 完成零拷贝。
拓展思考
- 双向转换:Span→NativeArray 只能走 NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray,但需手动 AtomicSafetyHandle 绑定,否则 Main-Thread 与 Job-System 并发检查会抛 InvalidOperationException。
- 只读优化:若 Span 仅做读取,可返回 ReadOnlySpan<T>,避免 WriteBack 带来的 Cache Miss;此时 NativeArray 需用 GetUnsafeReadOnlyPtr,并在 Job 中标记 [ReadOnly]。
- 大数组分页:当 Length > 2048*1024 时,Android 32-bit 进程 虚拟地址连续 可能失败,建议拆分为 NativeSlice<T> 再转 Span,防止 mmap ENOMEM。
- 版本升级坑:Unity 2023.2 起默认开启 STRICT_MEMORY_SAFETY,若 Span 生命周期超过 NativeArray.Dispose 会触发 SIGSEGV,国内很多项目升级后 闪退,需加 DisposeSentinel 做 Use-After-Free 检查。