Add Zhihu note: Unity无GC读取图片和网格

This commit is contained in:
Zane
2026-06-09 18:12:03 +08:00
parent 664e69bfc6
commit 12ff980a7c

View File

@@ -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\<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**。
### 加载流程
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<byte>()` 返回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<byte>()`,还有两个进阶优化:
1.`AssetBundle.LoadFromFileAsync()` 加载AB包不要用 `LoadFromMemory`后者会把整个AB包拷贝到托管堆
2. 对于超大的地形纹理和网格数据,用 `AssetBundle.LoadFromStreamAsync()` 直接流式读取,不需要把整个文件加载进内存
3. **绝对不要用 `Resources.Load`**它的GC开销比AB包大10倍以上
---
## 总结
Unity里无GC加载资源的本质就是**托管壳子复用,原生数据替换**。之前的问题就是把壳子和数据一起销毁重建了现在把壳子留下只换里面的数据GC自然就没了。