核心原则:Native Container 不是“更快的 List”。它们用于非托管内存、Jobs 和 Burst;你必须显式管理容量、依赖和释放。
为什么需要 Native Container常用容器Allocator 决定生命周期组件中持有 NativeList并行写入哈希表JobHandle 是所有权的一部分ReadOnly 与并行限制容量、重用和 GCNativeArray 与 NativeList 怎么选FixedList 与 Blob常见错误验证清单系列导航参考资料
为什么需要 Native Container
普通数组、List 和 Dictionary 由 GC 管理,能保存对象引用,但 Burst Job 不能直接安全访问。Unity Collections 提供 Native Container:
- 内存位于托管堆之外
- 可被 Job Safety System 跟踪
- 支持 Burst 编译
- 通常只保存 unmanaged 数据
代价是需要显式
Dispose,错误的生命周期可能造成泄漏、越界或竞态。常用容器
容器 | 用途 | 对应思路 |
NativeArray<T> | 固定长度连续数据 | 数组 |
NativeList<T> | 动态连续数据 | List |
NativeHashMap<K,V> | 单写者键值映射 | Dictionary |
NativeParallelHashMap<K,V> | 并行写入哈希表 | 并行 Dictionary |
NativeHashSet<T> | 唯一成员集合 | HashSet |
NativeQueue<T> | 先进先出数据流 | Queue |
NativeReference<T> | 单个共享值 | 可追踪引用 |
Allocator 决定生命周期
- Temp:极短生命周期,不应跨帧或长期 Job。
- TempJob:短期 Job 数据,需及时释放。
- Persistent:跨多帧长期保存,开销较高但生命周期自由。
分配器不是性能标签,而是生命周期契约。选择比需要更短的分配器会触发安全错误;长期忘记释放 Persistent 会泄漏原生内存。
组件中持有 NativeList
若组件可能在 Job 未完成时禁用,必须先完成或串联保存的 JobHandle,再释放容器。
并行写入哈希表
调度:
ParallelWriter 只暴露适合并行的操作。容量不足时并行写入可能失败或产生昂贵扩容策略,因此应预估上限。JobHandle 是所有权的一部分
容器的读写必须形成依赖图:
不要在 Producer 未完成时从主线程读取,也不要提前释放。临时容器可以使用
Dispose(dependency) 把释放动作安排到依赖之后。ReadOnly 与并行限制
[ReadOnly] 告诉安全系统 Job 只读该容器,多个 Job 因此可以并行读取。写入同一索引或同一容器需要:- 正确 JobHandle 依赖
- 支持并行写的容器和 Writer
- 或明确划分每个 Job 的写入区间
关闭安全检查的 Player Build 不会替你修复竞态。Editor 能运行不代表并发逻辑正确。
容量、重用和 GC
Native Container 本身避免托管数据分配,但外围代码仍可能产生 GC:
- Lambda 和闭包
- LINQ
- 每帧创建托管包装对象
- Native 数据复制回新托管数组
高频系统应长期复用 Persistent 容器,在每帧开始时
Clear,并监控峰值容量。不要因为“零 GC”让容器无限增长。NativeArray 与 NativeList 怎么选
- Job 输出数量固定:
NativeArray<T>
- 输出数量动态但有上限:
NativeList<T>
- 多 Job 并行追加:使用容器提供的并行 Writer,并确认写入顺序不重要
- 需要稳定顺序:预先计算索引或分段写入 Array
并行 Writer 通常不保证全局确定顺序。回放、联机确定性或测试依赖顺序时,需要显式排序或确定性索引。
FixedList 与 Blob
很小、长度上限固定、需要嵌入 struct 的数据可以使用
FixedList*。大量不可变共享数据在 Entities 工作流中可考虑 Blob Asset。不要为了使用高级容器而强行把普通 MonoBehaviour 配置迁移到原生内存;跨边界复制也有成本。
常见错误
- 忘记
Dispose。
- 释放时 Job 仍在使用容器。
- 使用
Allocator.Temp跨帧。
- 容量不足却在并行写入。
- 把托管对象或 string 放进要求 unmanaged 的容器。
- 依赖 ParallelWriter 的插入顺序。
- 每帧分配 Persistent 容器。
- 只看 GC,不看原生内存和同步等待成本。
验证清单
- 开启 Jobs Debugger / 安全检查进行开发测试。
- Profiler 检查主线程等待 Job 的位置。
- Memory Profiler 检查 Persistent 生命周期。
- Burst Inspector 确认关键 Job 实际被 Burst 编译。
- 用代表性数据量比较托管版本和 Native 版本。
- 在 IL2CPP 目标平台验证,而不只在 Editor 测试。