3.7 KiB
3.7 KiB
title, source, url, date, tags, type
| title | source | url | date | tags | type | |||||
|---|---|---|---|---|---|---|---|---|---|---|
| unity中怎么无gc读取图片和网格? | 知乎 - 老久 | https://www.zhihu.com/question/1967951834250459011/answer/2040581102813894136 | 2026-06-09 |
|
answer |
unity中怎么无gc读取图片和网格?
你之前所有GC的根源根本不是读AB包,而是每次都new新的Mesh/Texture托管对象。Unity的Mesh和Texture本质只是托管层的空壳,真正的顶点、像素数据全在原生内存里。无GC的核心就是:托管壳子永远复用,只替换壳子里的原生数据,绝对不销毁重建托管对象。
一、抛弃 AssetBundle.LoadAsset<Mesh>()
这个API只要调用就必然new新的Mesh托管对象,神仙都救不了GC。
打包阶段就换掉Mesh资源
不要直接把mesh拖进AB包,而是在编辑器里把Mesh的所有原始数据扒出来,序列化成你自己的二进制格式,打成TextAsset进AB包。具体要扒的数据:
- 顶点属性:位置、法线、UV1、UV2、切线(用
Mesh.GetVertexAttributes()获取布局,按布局连续存成二进制块) - 索引数据:每个子网格的索引数组
- 预计算的Bounds(这个一定要存,避免运行时RecalculateBounds遍历顶点)
- 子网格数量和每个子网格的拓扑结构
这样AB包里存的就不是Unity的Mesh对象,而是纯二进制数据,加载时不会触发任何托管对象创建。
二、运行时用对象池 + 原生内存拷贝
提前创建一个Mesh对象池,按"最大顶点数+最大索引数+子网格数量"分类。比如大世界每个地块最多10万顶点、20万索引、3个子网格,就提前创建20个这样的空Mesh放池子里,永远不Destroy。
加载流程
- 从池子里拿一个空闲的Mesh,先调用
Mesh.Clear(false)(第二个false表示不清空原生缓冲区,只是重置状态,零开销) - 用
Mesh.AllocateWritableMeshData(1)拿到可写的MeshData对象,这个对象直接指向Mesh的原生内存缓冲区,没有任何托管拷贝 - 从AB包加载对应的TextAsset,绝对不要用TextAsset.bytes属性(每次get都会new新的byte数组,GC爆炸),用 Unity 2021+ 新增的
TextAsset.GetData<byte>()返回NativeArray,直接指向原生内存,零GC - 用
UnsafeUtility.MemCpy()把TextAsset里的顶点数据、索引数据直结拷贝到MeshData对应的缓冲区里,连NativeArray的中间拷贝都省了 - 把预存的Bounds直接赋给
Mesh.bounds,不要调用RecalculateBounds - 用
Mesh.ApplyAndDisposeWritableMeshData(meshData, false)(第二个false表示CPU不需要再读写,直接释放CPU端内存,只留GPU端) - 把这个复用的Mesh赋值给MeshRenderer,完成加载
卸载流程
- 先把MeshRenderer的mesh置为null,确保没有任何地方引用这个Mesh
- 调用
Mesh.Clear(false)重置状态,直接扔回对象池,绝对不要Destroy
整个流程除了对象池初始化时得那批Mesh,运行时没有任何托管堆分配,GC Alloc为0。
三、AB包加载本身的GC优化
除了上面说的用 GetData<byte>(),还有两个进阶优化:
- 用
AssetBundle.LoadFromFileAsync()加载AB包,不要用LoadFromMemory,后者会把整个AB包拷贝到托管堆 - 对于超大的地形纹理和网格数据,用
AssetBundle.LoadFromStreamAsync()直接流式读取,不需要把整个文件加载进内存 - 绝对不要用
Resources.Load,它的GC开销比AB包大10倍以上
总结
Unity里无GC加载资源的本质就是:托管壳子复用,原生数据替换。之前的问题就是把壳子和数据一起销毁重建了,现在把壳子留下,只换里面的数据,GC自然就没了。