前言
Unity中读取图片(纹理Texture)、网格(Mesh)产生GC的核心原因:频繁创建临时托管数组、自动装箱拆箱、资源读写备份、重复内存分配。无GC读取的核心思路是复用内存、禁用自动分配、规避临时对象、使用非托管/固定内存API ,以下分图片、网格两大模块,提供可直接落地的零GC配置+代码方案。
一、无GC读取图片(Texture2D/贴图)
对惹,这里有一 个游戏开发交流小组,希望大家可以点击进来一起交流一下开发经验呀!
1. 核心前置配置(杜绝隐性GC)
该配置从资源层规避内存拷贝和托管内存分配,是无GC读取的基础,必须优先设置:
- 关闭纹理Read/Write Enabled:导入图片后,在Inspector面板取消勾选该选项。开启后Unity会在CPU内存备份一份纹理数据,读写时频繁产生GC内存分配,关闭后数据仅驻留GPU显存,无CPU托管内存开销。
- 禁用异步加载冗余分配:非动态更新的静态贴图,避免放在Resources文件夹,优先使用AssetBundle/Addressables加载,适配Unity无损异步上传管线,减少临时内存创建。
- 固定纹理分辨率与格式:提前锁定图片尺寸、压缩格式(ASTC/ETC2),避免运行时缩放、格式转换产生临时像素数组GC。
2. 零GC代码实现(本地图片/资源贴图)
摒弃File.ReadAllBytes()(会新建临时byte数组)、new Texture2D()频繁创建实例的写法,采用内存复用+非托管读取+markNonReadable 参数彻底清零GC。
核心关键点:ImageConversion.LoadImage 传入 markNonReadable=true,加载后纹理直接脱离CPU读写内存,无托管数据残留;全局复用byte缓冲区,不重复分配内存。
using System;
using System.IO;
using UnityEngine;
using UnityEngine.ImageConversion;
public static class TextureNoGCUtil
{
// 全局复用缓冲区,程序生命周期仅分配1次,全程无GC
private static byte[] _reuseBuffer = new byte[1024 * 1024 * 5]; // 5MB 适配大部分贴图
/// <summary>
/// 无GC加载本地图片到复用纹理
/// </summary>
/// <param name="tex">复用的Texture2D实例(提前创建,不频繁new)</param>
/// <param name="filePath">本地图片绝对路径</param>
/// <returns>加载结果</returns>
public static bool LoadTextureNoGC(Texture2D tex, string filePath)
{
if (!File.Exists(filePath) || tex == null) return false;
// 1. 非托管文件读取,复用缓冲区,无新内存分配
using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
int fileLen = (int)fs.Length;
// 扩容缓冲区(仅首次不足时分配1次,后续复用)
if (_reuseBuffer.Length < fileLen)
{
_reuseBuffer = new byte[fileLen];
}
// 读取数据到固定缓冲区,无临时GC
fs.Read(_reuseBuffer, 0, fileLen);
}
// 2. 无GC加载核心:markNonReadable=true 关闭CPU读写备份,零托管开销
return LoadImage(tex, _reuseBuffer, 0, fileLen, true);
}
// 重载:支持直接加载资源字节流,全程复用内存
public static bool LoadTextureNoGC(Texture2D tex, byte[] sourceData, int offset, int length)
{
if (_reuseBuffer.Length < length)
_reuseBuffer = new byte[length];
Array.Copy(sourceData, offset, _reuseBuffer, 0, length);
return LoadImage(tex, _reuseBuffer, 0, length, true);
}
}
3. 运行时动态像素修改(无GC)
常规 SetPixels() 会创建临时Color数组产生GC,优化方案:全局复用像素数组 + 关闭读写备份:
private static Color[] _reusePixelBuffer;
public static void SetTexturePixelsNoGC(Texture2D tex, Color[] pixels)
{
if (_reusePixelBuffer == null || _reusePixelBuffer.Length != pixels.Length)
_reusePixelBuffer = new Color[pixels.Length];
Array.Copy(pixels, _reusePixelBuffer, pixels.Length);
tex.SetPixels(_reusePixelBuffer);
tex.Apply(false); // false=不重新计算mipmap,减少开销
}
二、无GC读取网格(Mesh模型)
1. 核心GC产生原因
Mesh读取/赋值产生GC,主要源于:mesh.vertices、mesh.triangles 等属性会每次返回新数组副本;频繁new Mesh实例、临时数组分配、读写备份内存。
2. 前置优化配置
- 模型导入设置:取消 Read/Write Enabled,关闭CPU网格数据备份,仅GPU渲染使用,杜绝隐性内存拷贝GC;
- 静态模型勾选Static,动态模型提前预创建Mesh实例,运行时复用,不重复new。
3. 无GC网格读写核心方案(复用数组+直接写入)
放弃属性赋值(mesh.vertices = 数组),使用Mesh.SetVertices、SetTriangles 系列API,支持传入复用数组,不会生成临时副本,全程零GC。
using UnityEngine;
public static class MeshNoGCUtil
{
// 全局复用网格数据数组,生命周期仅初始化1次
private static Vector3[] _vertBuffer;
private static int[] _triBuffer;
private static Vector2[] _uvBuffer;
/// <summary>
/// 无GC更新网格数据
/// </summary>
public static void UpdateMeshNoGC(Mesh targetMesh, Vector3[] vertices, int[] triangles, Vector2[] uvs = null)
{
// 1. 扩容复用缓冲区,无频繁new
if (_vertBuffer == null || _vertBuffer.Length != vertices.Length)
_vertBuffer = new Vector3[vertices.Length];
if (_triBuffer == null || _triBuffer.Length != triangles.Length)
_triBuffer = new int[triangles.Length];
// 2. 内存拷贝,复用固定数组,零临时GC
Array.Copy(vertices, _vertBuffer, vertices.Length);
Array.Copy(triangles, _triBuffer, triangles.Length);
// 3. 核心无GCAPI:直接写入GPU数据,不生成副本
targetMesh.SetVertices(_vertBuffer);
targetMesh.SetTriangles(_triBuffer, 0);
// UV数据无GC赋值
if (uvs != null)
{
if (_uvBuffer == null || _uvBuffer.Length != uvs.Length)
_uvBuffer = new Vector2[uvs.Length];
Array.Copy(uvs, _uvBuffer, uvs.Length);
targetMesh.SetUVs(0, _uvBuffer);
}
targetMesh.RecalculateNormals();
targetMesh.RecalculateBounds();
}
}
4. 无GC读取现有网格数据
禁止直接 var verts = mesh.vertices(产生GC),使用 Mesh.GetVertices 写入复用数组:
public static void ReadMeshNoGC(Mesh mesh)
{
// 复用已有缓冲区,无新内存分配
if (_vertBuffer == null || _vertBuffer.Length < mesh.vertexCount)
_vertBuffer = new Vector3[mesh.vertexCount];
// 直接读取到复用数组,零GC
mesh.GetVertices(_vertBuffer);
mesh.GetTriangles(_triBuffer, 0);
}
三、终极无GC通用规则 & 避坑要点
1. 绝对禁止的GC写法
- 图片:
File.ReadAllBytes()、频繁new Texture2D、LoadImage默认参数; - 网格:
mesh.vertices/uvs/triangles属性读取赋值、临时创建数组传参。
2. 零GC核心准则
- 所有缓冲区(byte、顶点、UV、三角面数组)全局单例复用,仅扩容不重复创建;
- 纹理优先 markNonReadable=true ,网格优先 SetXXX/GetXXX 系列API;
- 非必要全部关闭Read/Write Enabled,切断CPU托管内存备份;
- 避免using外临时变量、避免foreach遍历资源数据(部分版本存在隐性GC)。
3. 移动端适配补充
Android/iOS平台无需特殊适配,上述方案完全兼容;外部文件读取只需配置对应读写权限,代码逻辑无需修改,全程保持零GC开销。