From 12ff980a7c7f8ee2e1b641e170e7e12937a2ea71 Mon Sep 17 00:00:00 2001 From: Zane Date: Tue, 9 Jun 2026 18:12:03 +0800 Subject: [PATCH] =?UTF-8?q?Add=20Zhihu=20note:=20Unity=E6=97=A0GC=E8=AF=BB?= =?UTF-8?q?=E5=8F=96=E5=9B=BE=E7=89=87=E5=92=8C=E7=BD=91=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- InBox/zhihu_Unity无GC读取图片和网格.md | 68 ++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 InBox/zhihu_Unity无GC读取图片和网格.md diff --git a/InBox/zhihu_Unity无GC读取图片和网格.md b/InBox/zhihu_Unity无GC读取图片和网格.md new file mode 100644 index 0000000..32820f3 --- /dev/null +++ b/InBox/zhihu_Unity无GC读取图片和网格.md @@ -0,0 +1,68 @@ +--- +title: "unity中怎么无gc读取图片和网格?" +source: "知乎 - 老久" +url: "https://www.zhihu.com/question/1967951834250459011/answer/2040581102813894136" +date: 2026-06-09 +tags: [zhihu, unity, performance, gc, mesh] +type: answer +--- + +# unity中怎么无gc读取图片和网格? + +> 你之前所有GC的根源根本不是读AB包,而是每次都new新的Mesh/Texture托管对象。Unity的Mesh和Texture本质只是托管层的空壳,真正的顶点、像素数据全在原生内存里。无GC的核心就是:托管壳子永远复用,只替换壳子里的原生数据,绝对不销毁重建托管对象。 + +--- + +## 一、抛弃 AssetBundle.LoadAsset\() + +这个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**。 + +### 加载流程 + +1. 从池子里拿一个空闲的Mesh,先调用 `Mesh.Clear(false)`(第二个false表示不清空原生缓冲区,只是重置状态,零开销) +2. 用 `Mesh.AllocateWritableMeshData(1)` 拿到可写的MeshData对象,这个对象直接指向Mesh的原生内存缓冲区,没有任何托管拷贝 +3. 从AB包加载对应的TextAsset,**绝对不要用TextAsset.bytes属性**(每次get都会new新的byte数组,GC爆炸),用 Unity 2021+ 新增的 `TextAsset.GetData()` 返回NativeArray,直接指向原生内存,零GC +4. 用 `UnsafeUtility.MemCpy()` 把TextAsset里的顶点数据、索引数据直结拷贝到MeshData对应的缓冲区里,连NativeArray的中间拷贝都省了 +5. 把预存的Bounds直接赋给 `Mesh.bounds`,不要调用 `RecalculateBounds` +6. 用 `Mesh.ApplyAndDisposeWritableMeshData(meshData, false)`(第二个false表示CPU不需要再读写,直接释放CPU端内存,只留GPU端) +7. 把这个复用的Mesh赋值给MeshRenderer,完成加载 + +### 卸载流程 + +1. 先把MeshRenderer的mesh置为null,确保没有任何地方引用这个Mesh +2. 调用 `Mesh.Clear(false)` 重置状态,直接扔回对象池,**绝对不要Destroy** + +整个流程除了对象池初始化时得那批Mesh,运行时没有任何托管堆分配,GC Alloc为0。 + +--- + +## 三、AB包加载本身的GC优化 + +除了上面说的用 `GetData()`,还有两个进阶优化: + +1. 用 `AssetBundle.LoadFromFileAsync()` 加载AB包,不要用 `LoadFromMemory`,后者会把整个AB包拷贝到托管堆 +2. 对于超大的地形纹理和网格数据,用 `AssetBundle.LoadFromStreamAsync()` 直接流式读取,不需要把整个文件加载进内存 +3. **绝对不要用 `Resources.Load`**,它的GC开销比AB包大10倍以上 + +--- + +## 总结 + +Unity里无GC加载资源的本质就是:**托管壳子复用,原生数据替换**。之前的问题就是把壳子和数据一起销毁重建了,现在把壳子留下,只换里面的数据,GC自然就没了。