前言
本篇为学习总结性质的文章,若有任何问题或错误,欢迎在评论区指出。
如果本文对您有一定帮助,也欢迎点赞、收藏、关注。
目录
关于Resources
为什么要使用Resources进行资源的动态加载?
- 避免了在Inspector窗口拖拽更换资源的麻烦。
- 增加了可拓展性,便于维护。
准备工作
众所周知,在Unity的Project窗口、Assets文件下创建文件夹非常自由,但还是有一定讲究的,特别是当你要使用一些Unity的API对文件资源进行操作时。我们今天的主角——Resources,就会用到特殊文件夹之一,也就是"Resources"。
首先,我们在Assets下新建一个名为"Resources"的文件夹。注意检查,不要有拼写错误,否则Unity不会将其识别为Resources特殊文件夹。
然后,我们随意拖入一张图片,为稍后的测试做准备。
Resources资源的两种加载方法
同步加载
如果想要通过GUI将以上例子中的图片显示出来,不使用资源动态加载会怎么做呢?
大概是:新建一个脚本,其中有public Texture和相应GUI打印逻辑,再挂载脚本到场景中任意物体,最后把上面的示例图片拖到Inspector窗口的public Texture上。
如果使用Resources同步加载呢?新建脚本如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Test : MonoBehaviour
{
private Texture texture;
private Rect rect = new Rect(0, 0, 100, 100);
private void Start()
{
//1.普通Load方法,传入要加载的资源在Resources特殊文件夹中的路径,返回基类Object,需要as后才能使用
//texture = Resources.Load("Test") as Texture;
//2.泛型Load方法,传入要加载的资源在Resources特殊文件夹中的路径,直接返回对应类型,不需要再as
texture = Resources.Load<Texture>("Test");
}
private void OnGUI()
{
GUI.DrawTexture(rect, texture);
}
}
同样是挂载到场景任一物体上,运行,即可发现我们没有进行任何拖拽操作就成功进行了资源的读取。
那让我们聚焦于代码本身,第一种拖拽的方法和第二种Resources相关API的方法,原理分别是什么?
显然,拖拽实际上是为了去手动关联想要使用的资源。而Resources的Load方法,会去将指定路径的资源动态读取到内存中,便可以直接进行使用。
而两种Load方法,更常用的是泛型Load方法,直接返回对应类型省去了as操作,而且也可以避免同一路径下、不同类型的同名文件的Load错误。
比如这样:
如果还像上面一样直接使用texture = Resources.Load(“Test”) as Texture,那么就很有可能出现无法准确加载出想要的内容。此时我们使用泛型方法,就可以轻松解决问题。
当然,Unity也提供了其他API来使用:比如非泛型的Load有一个重载,第二个参数填的就是一个Type,告知Unity需要加载的资源类型。也有LoadAll方法加载所有的同名资源,返回值为一个Object数组。如下:
//1.
texture = Resources.Load("Test",typeof(Texture)) as Texture;
//2.
Object[] objs = Resources.LoadAll("Test");
//可以遍历,根据类型进行相关操作:
foreach (Object item in objs)
{
if (item is Texture)
{
}
else if(item is TextAsset)
{
}
}
以上便是使用同步加载加载图片并进行使用,加载音效(AudioClip)、文本(TextAsset)等同理。
预制体也可以放入Resources文件夹进行加载,和其他资源稍稍不同的是预制体通常是用于实例化,而不是像图片、文本等直接使用。
异步加载
上面说完了“同步加载”,我们接着说“异步加载”。
什么是“异步加载”呢?何种情况下我们要使用“异步加载”而不是“同步加载”呢?
首先我们需要简单了解下这两者工作原理的区别。
同步加载,如其名,当运行到例如“texture = Resources.Load(“Test”) as Texture;”这一行代码时,Unity就会立即去加载资源,直到加载完,再把加载好的资源内存地址赋给相应的变量(在上面的一行代码中是“texture”),然后就可以把相应变量作为已经加载好的资源来使用了。
这会出现什么问题呢?
就是当要加载的资源十分庞大时,Unity会花费大量时间来加载,这便会造成主线程的卡顿,破坏玩家体验。
而“异步加载”,生来就是解决该问题的。每次进行异步加载时,Unity不会像同步加载一样在主线程里“死磕”,而是不会在主线程中停留、直接开一个新线程去加载该资源。当加载好后,就会把该加载好的资源放入一个内存的“公共区域”,供主线程的代码使用。
要实现异步加载,我们会用到Resources.LoadAsync() 函数。
仔细看Resources.LoadAsync(),会发现其返回值是ResourceRequest,这意味着我们并不能像同步加载一样简单粗暴地用一个对应类型的变量来接收加载好的资源。
真正要使用异步加载后的资源有以下两种方法:
1. 通过异步加载中的完成事件监听,使用加载的资源
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Test : MonoBehaviour
{
private Texture tex;
void Start()
{
//1.接收异步加载返回的ResourceRequest
ResourceRequest rq = Resources.LoadAsync<Texture>("Test");
//2.点出事件监听函数completed,加入加载结束后要处理的相关逻辑
rq.completed += LoadOver;
}
//加载完毕后要处理的逻辑,需要有一个AsyncOperation参数(这是以上提到的ResourceRequest的父类)
private void LoadOver( AsyncOperation rq)
{
print("加载结束");
//asset即是加载完毕后的资源,将AsyncOperation父类as成ResourceRequest即可点出
tex = (rq as ResourceRequest).asset as Texture;
}
private void OnGUI()
{
//因为异步加载不是立即将资源加载好,所以需要判断不为空后再使用
if( tex != null)
GUI.DrawTexture(new Rect(0, 0, 100, 100), tex);
}
}
另外,AsyncOperation和其子类ResourceRequest还能点出诸如isDone(bool,是否加载完资源)、progress(float,加载该资源的进度)等参数,具体可以去看Unity官方文档。
2. 通过协程使用加载的资源
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Test : MonoBehaviour
{
private Texture tex;
void Start()
{
//开启加载协程
StartCoroutine(Load());
}
IEnumerator Load()
{
//开始异步加载
ResourceRequest rq = Resources.LoadAsync<Texture>("Test");
//关键:yield return接收的ResourceRequest。只有当该资源加载完毕后,Unity才会执行后面部分的代码
yield return rq;
print("加载结束");
tex = rq.asset as Texture;
}
private void OnGUI()
{
if( tex != null)
GUI.DrawTexture(new Rect(0, 0, 100, 100), tex);
}
}
两种方法比较
-
完成事件监听异步加载
好处:写法简单;
坏处:只能在资源加载结束后进行处理。 -
协程异步加载
好处:可以在协程中处理复杂逻辑,比如同时加载多个资源、进度条更新等;
坏处:写法稍麻烦。
Resources资源卸载
一个小结论
首先,我们需要明确的一个隐形知识点是:
无论是同步加载还是异步加载,每次加载后的资源会一直存在CPU的缓存区中,以方便下次又加载该资源时不用重头再来,而是在搜索到后取出赋给相应变量(但异步加载也要等至少一帧后才可使用)。
举个例子,我们将之前同步加载的例子写成这样:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Test : MonoBehaviour
{
private Rect rect = new Rect(0, 0, 100, 100);
private void OnGUI()
{
GUI.DrawTexture(rect, Resources.Load<Texture>("Test"));
}
}
这样写其实并不会浪费内存。
因为就像上面所说,虽然看起来我们每一帧都调用OnGUI、每一帧都在加载资源,但实际上只有第一次加载资源才是真正地去加载,后面的“加载”,实际上都只是从缓存中取出来使用而已。
当然,虽然不会浪费内存,但这样写还是不推荐的,因为每帧都去查找缓存中的资源也是会消耗性能的。所以Load方法最好都放在Start或Awake里初始化即可。
验证
对于这个结论,我们也可以通过Unity的Profiler调试窗口来进行进一步验证。
建立如下脚本:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Test : MonoBehaviour
{
private Texture tex;
void Update()
{
//按下数字键1,进行资源加载
if (Input.GetKeyDown(KeyCode.Alpha1))
{
print("加载资源");
tex = Resources.Load<Texture>("Test");
}
}
}
打开Profiler窗口并运行程序,点击数字键1,注意查看内存中Texture的变化:
初始:66.9MB
按下一次1(加载一次资源):67.0MB
按下多次1(多次加载同一资源):仍然是67.0MB
这足以验证对同一资源的多次加载实际上只进行了一次,不会浪费内存。
资源卸载
回到正题:Resources资源卸载。
虽然不停加载同一资源不会浪费内存,但这个特性同时也是一个缺点——因为资源一多且某些资源只被用到很少次甚至一次,那么最终就会有大量资源占用、堆积在缓存区,变相提高了GC频率,影响玩家游戏的流畅性。
所以我们需要方法来手动清除缓存区中的资源。
常用API有二:
1. 卸载指定资源:Resources.UnloadAsset()
比如上面验证结论的代码,我们可以在Update里添上这一段:
//按下数字键2,进行资源卸载
if (Input.GetKeyDown(KeyCode.Alpha2))
{
print("卸载资源");
Resources.UnloadAsset(tex);
tex = null;
}
这样按下数字键2会发现Texture占用会回到初始状态:66.9MB
说明我们成功地进行了资源卸载,释放了原本被占用了的内存。
但实际上我们很少会单独去用这个API。而且这个方法只能用于一些不需要实例化的内容(如图片、音效、文本等),不能用于释放GameObject对象(即使该GameObject还未被实例化),否则会报错:
UnloadAsset may only be used on individual assets and can not be used on GameObject's / Components / AssetBundles or GameManagers
2. 卸载未使用资源:Resources.UnloadUnusedAssets()
我们更常用的是该API,它会卸载掉所有未使用的资源。
通常会在过场景时与GC一起使用:
Resources.UnloadUnusedAssets();
GC.Collect();
总结
- 根据资源大小和我们需求的不同,我们可以使用Resources.Load() 进行同步加载,或使用Resources.LoadAsync() 进行异步加载。
- 两种加载的逻辑都是会先去缓存区查找是否已经加载好,如果没有才进行首次加载。
- 过场景时我们可以同GC一起,使用Resources.UnloadUnusedAssets() 卸载掉所有未使用的资源。