Unity内存申请和释放

2015年08月03日 13:37 0 点赞 0 评论 更新于 2025-11-21 18:41

1. 资源类型

Unity中的资源类型丰富多样,主要包括GameObject、Transform、Mesh、Texture、Material、Shader以及各种其他Assets。

2. 资源创建方式

静态引用

在脚本中定义一个public GameObject变量,然后在Inspector面板中将一个prefab拖拽到该变量上。后续在需要引用该资源的地方,使用Instantiate方法进行实例化。

Resource.Load

使用Resource.Load方法加载资源时,资源需要放置在Assets/Resources目录下。

AssetBundle.Load

先使用AssetBundle.Load加载资源,加载完成后再使用Instantiate进行实例化。

3. 资源销毁方式

GameObject.Destroy(gameObject)

该方法用于销毁指定的游戏对象。

AssetBundle.Unload(false)

此方法会释放AssetBundle文件的内存镜像,但不会销毁通过Load方法创建的Assets对象。

AssetBundle.Unload(true)

该方法会释放AssetBundle文件的内存镜像,同时销毁所有已经加载的Assets内存镜像。

Resources.UnloadAsset(Object)

用于释放已加载的Asset对象。

Resources.UnloadUnusedAssets

释放所有没有被引用的Asset对象。

4. 生命周期

实验篇

在实验中,我们创建了一个简单的场景,其中包含一个Empty GameObject,并为其挂载了一个脚本。在脚本的Awake函数中,通过协程函数来创建资源,具体的协程函数如下所示。

实验使用的Prefab是一个坦克车,将其加入场景后,场景内存增加约3M。同时,我们还创建了一个AssetBundle资源供后续使用。

1. Resources.Load方式加载一个Prefab,然后Instantiate GameObject

代码如下:

IEnumerator LoadResources()
{
// 清除干净以免影响测试结果
Resources.UnloadUnusedAssets();
// 等待5秒以看到效果
yield return new WaitForSeconds(5.0f);
// 通过Resources.Load加载一个资源
GameObject tank = Resources.Load("Role/Tank") as GameObject;
yield return new WaitForSeconds(0.5f);
// Instantiate一个资源出来
GameObject tankInst = GameObject.Instantiate(tank, Vector3.zero, Quaternion.identity) as GameObject;
yield return new WaitForSeconds(0.5f);
// Destroy一个资源
GameObject.Destroy(tankInst);
yield return new WaitForSeconds(0.5f);
// 释放无用资源
tank = null;
Resources.UnloadUnusedAssets();
yield return new WaitForSeconds(0.5f);
}

统计结果及结论:

  • Resouces.Load一个Prefab相对于Instantiate一个资源来说,是相对轻量的操作。在上述过程中,Resources.Load加载一个Prefab几乎没有消耗内存,而Instantiate消耗了2.5M的资源空间。Resources.Load增加了Mesh和Total Object的数量,而Instantiate增加了GameObjects、Objects In Scene和Total Objects的数量。
  • Destroy一个GameObject之后,内存有所减少,但减少量较少,在本例中减少了0.6M。InstantiateDestroy前后Material和Texture没有还原,以便后续继续进行Instantiate操作。

若没有调用Resources.UnloadUnusedAssets,则多余的Mesh、Material和Object不会主动释放。

2. 以AssetBundle.Load的方式加载一个Prefab,然后Instantiate一个GameObject

代码如下:

IEnumerator LoadAssets(string path)
{
// 清除干净以免影响测试结果
Resources.UnloadUnusedAssets();
// 等待5秒以看到效果
yield return new WaitForSeconds(5.0f);
// 创建一个WWW类
WWW bundle = new WWW(path);
yield return bundle;
yield return new WaitForSeconds(0.5f);
// AssetBundle.Load一个资源
Object obj = bundle.assetBundle.Load("tank");
yield return new WaitForSeconds(0.5f);
// Instantiate一个资源出来
GameObject tankInst = Instantiate(obj) as GameObject;
yield return new WaitForSeconds(0.5f);
// Destroy一个资源
GameObject.Destroy(tankInst);
yield return new WaitForSeconds(0.5f);
// Unload Resources
bundle.assetBundle.Unload(false);
yield return new WaitForSeconds(0.5f);
// 释放无用资源
// obj = null;
// Resources.UnloadUnusedAssets();
yield return new WaitForSeconds(0.5f);
}

统计结果及结论: 通过WWW Load AssetBundle的方式加载一个资源时,会自动加载相应的Mesh,Texture和Material,而通过Resouces.Load方式进行加载只会加载Mesh信息。因此,通过AssetBundle方式加载后Instantiate一个资源的内存消耗较小,在本例中AssetBundle.Load增加了2.5M的内存,而Instantiate增加了1.1M的内存,相比较Resources.LoadInstantiate的内存增量要小很多。

3. 通过静态绑定的方法来Instantiate一个资源

代码如下:

IEnumerator InstResources()
{
Resources.UnloadUnusedAssets();
yield return new WaitForSeconds(5.0f);
GameObject inst = GameObject.Instantiate(tank, Vector3.zero, Quaternion.identity) as GameObject;
yield return new WaitForSeconds(1f);
GameObject.Destroy(inst);
yield return new WaitForSeconds(1f);
// 释放无用资源
tank = null;
Resources.UnloadUnusedAssets();
yield return new WaitForSeconds(1f);
}

统计结果及结论: 通过静态绑定的方式,各种资源的加载顺序和Resources.Load的方式是一样的。一个GameObject创建时,其Component中静态绑定的GameObject只会加载Mesh信息,只有当该GameObjectInstantiate出来之后才会加载Texture和Material信息。

理论篇

资源加载过程可分为两个阶段:

  • 第一阶段:使用Resources.Load或者AssetBundle.Load加载各种资源。
  • 第二阶段:使用GameObject.Instantiate克隆出一个新的GameObject。

Load的资源类型包括GameObject、Transform、Mesh、Texture、Material、Shader等各种资源,但Resources.LoadAssetBundle.Load存在区别:

  • 使用Resources.Load时,在第一次Instantiate之前,相应的Asset对象还未被创建,直到第一次Instantiate时才会真正去读取文件创建这些Assets,目的是实现按需使用,即到资源真正使用时才创建。
  • 使用AssetBundle.Load方法时,会直接将资源文件读取出来创建这些Assets,因此第一次Instantiate的代价相对较小。

上述区别可以解释为什么发射第一发子弹时会出现明显的卡顿现象。

Instantiate的过程是对Assets进行Clone(复制)和引用相结合的过程:

  • Clone过程需要申请内存存放自己的数据。
  • 引用过程只需一个简单的指针指向一个已经加载的资源。

例如,Transform是通过Clone出来的,Texture和TerrainData是通过引用复制的,而Mesh、Material、PhysicalMaterial等是Clone和引用同时存在的。以脚本为例,Script分为代码段和数据段,所有使用该Script的GameObject使用的代码相同,但数据不同,因此对数据段使用Clone方式,对代码段使用引用方式复制。

销毁资源的过程: 当Destroy一个GameObject或者其他实例时,只会释放实例中那些Clone出来的Assets,而不会释放引用的Assets,因为Destroy不知道是否有其他对象在引用这些Assets。当场景中没有任何物体引用这些Assets后,它们会成为UnusedAssets,此时可通过Resources.UnloadUnusedAssets进行释放。AssetBundle.Unload(false)只会释放文件的内存镜像,不会释放资源;AssetBunde.Unload(true)是暴力释放,可能导致程序错误,因为可能有其他对象在引用其中的Assets。

另外,系统在加载新场景时,所有的内存对象都会被自动销毁,包括Resources.Load加载的Assets、静态绑定的Assets、AssetBundle.Load加载的资源和Instantiate实例化的对象。但AssetBundle.Load本身的文件内存镜像(用于创建各种Asset)不会被自动销毁,必须使用AssetBundle.Unload(false)进行主动销毁。推荐在加载完资源后立即调用AssetBunble.Unload(false)销毁文件内存镜像。

总结篇

  • 为避免首次Instantiate时出现卡顿现象,推荐使用AssetBundle.Load的方式代替Resources.Load的方式来加载资源。
  • 加载完资源后,应立即调用AssetBunble.Unload(false)释放文件内存镜像。
  • Unity自身没有提供良好的内存申请和释放管理机制,Destroy一个GameObject会立即释放内存而不进行内部缓存,因此应用程序对频繁不用的对象(如NPC、FX等)进行对象池管理是必要的,可减少内存申请次数。
  • 何时进行Resources.UnloadUnusedAssets是一个需要深入讨论的问题。

作者信息

洞悉

洞悉

共发布了 3994 篇文章