我们知道在Unity中通过Resources.Load可以进行加载
// 加载一个音频资源
AudioClip go = Resources.Load<AudioClip>("Example/1");
通过Resources.UnloadAsset进行卸载
// 卸载上面的音频资源
Resources.UnloadAsset(go);
那我们如何查看资源已经卸载呢?
可以通过Profile工具查看Audio Clip Count: 0,当我们加载一个音频资源时Audio Clip Count: 1,卸载后Audio Clip Count: 0
在一个脚本中,如果要对一个资源进行加载的话,那么在脚本销毁的时候就一定要对其卸载,不然该资源将一直存在于内存中,直到退出游戏为止。也就意味着如果一直不退出游戏的话,那么资源占用的内存会越来越多,游戏将会越来越卡。这对于一个游戏开发者而言显然是不允许的。所以我们需要在OnDestroy方法中对资源进行卸载。
AudioClip ac;
private void Start(){
ac = Resources.Load<AudioClip>("aa");
}
private void OnDestroy(){
Resources.UnLoadAsset(ac);
ac = null;
}
这只是对一个资源的加载和卸载,但是如果资源很多的情况下,就需要写很多个UnLoadAsset,太麻烦了。对于一个懒人作者来说。有没有更好的方法去卸载资源呢?
答案是使用List,在加载的时候,我们将加载中的资源存放到List中,卸载的时候,直接通过遍历列表。代码如下:
AudioClip ac;
List<Object> list = new();
private void Start(){
ac = Resources.Load<AudioClip>("aa");
list.Add(ac);
}
private void OnDestroy(){
foreach(var item in list){
Resources.UnLoadAsset(item);
}
list.Clear();
}
如果重复加载一个资源或者重复卸载一个资源的话,就会导致内存的浪费甚至还可能出现一些问题。怎么避免重复加载呢?
答案是在每次加载的时候,在List中去查找这个资源,如果有,就返回这个资源。如果没有,在去通过Resources.Load进行加载。
所以我们定义一个LoadAsset方法,这个方法的核心目的就是:先找list,如果有则返回,没有才会加载
为什么需要使用泛型?而且泛型必须继承Object呢?
答:Object是Unity引擎中所有资源的父类,可以通过Object接收Unity中的所有资源,如音频、图片等。
而使用泛型是想要加载什么类型的资源就返回什么类型的资源。
List<Object> list = new();
private T LoadAsset<T>(string resname) where T : Object{
var resAsset = list.Find((e)=>e.name == resname) as T;
if(resAsset != null){
return resAsset;
}
T t = Resources.Load<T>(resname);
list.Add(t);
return t;
}
private void Start(){
LoadAsset<AudioClip>("aa");
}
private void OnDestroy(){
foreach(var item in list){
Resources.UnLoadAsset(item);
item = null;
}
list.Clear();
}
如果每个脚本都写这样的逻辑,那么也会显得很麻烦。作为一个懒人作者,我们需要封装上面的代码,不用每个脚本都这么写,那么将非常的方便。那么怎么去实现呢?
首先我们准备一个资源加载类ResLoader,用于对资源进行加载和卸载。
《代码大全2》第6章:可以工作的类,提到设计一个类需要有一个中心目的。
而ResLoader的中心目的是对资源进行加载和卸载。
using System.Collections.Generic;
using UnityEngine;
namespace DYFramework.ResKit
{
public class ResLoader
{
private List<Object> _loadedAssets = new();
public T LoadAsset<T>(string assetName) where T : Object
{
var retAsset = _loadedAssets.Find((e) => e.name == assetName) as T;
if (retAsset)
{
return retAsset;
}
retAsset = Resources.Load<T>(assetName);
_loadedAssets.Add(retAsset);
return retAsset;
}
public void UnloadAllAssets()
{
foreach (var item in _loadedAssets)
{
Resources.UnloadAsset(item);
}
_loadedAssets.Clear();
}
}
}
经过这样的设计,我们原本复杂的代码,变成了简单的3步。如下面的代码:
private ResLoader _resLoader = new ResLoader();
private void Start()
{
_resLoader.LoadAsset<AudioClip>("Example/1");
}
private void OnDestroy()
{
_resLoader.UnloadAllAssets();
_resLoader = null;
}
再次打开Profile分析器,找到Audio下面的Audio Clip Count,当加载资源时Audio Clip Count: 1,删除测试脚本,会调用OnDestroy函数,卸载资源,Audio Clip Count变为了0。
说明我们的逻辑没有问题。
现在我们可以对一个脚本解决重复加载和卸载问题。但是多个脚本访问一个资源又该怎么办呢?
比如脚本A访问资源1,脚本B也访问资源1,打开Profile分析器,发现资源1还是重复加载了。
所有我们需要一个全局的容器,用来存放所有脚本所加载过的资源。
核心思路就是:先从本地的容器中找,本地没有找到,再去找全局。全局找到了就返回,并把它放入本地缓存。全局每有找到,那就通过Resources.Load加载资源,然后放入本地和全局。
using System.Collections.Generic;
using UnityEngine;
namespace DYFramework.ResKit
{
public class ResLoader
{
private static List<Object> _sharedLoadedAssets = new();
private List<Object> _loadedAssets = new();
public T LoadAsset<T>(string assetName) where T : Object
{
// 先从本地找
var retAsset = _loadedAssets.Find((e) => e.name == assetName) as T;
if (retAsset)
{
return retAsset;
}
// 再从全局找
retAsset = _sharedLoadedAssets.Find((e) => e.name == assetName) as T;
if (retAsset)
{
_loadedAssets.Add(retAsset);
return retAsset;
}
// 全局和本地都没有找到
retAsset = Resources.Load<T>(assetName);
_loadedAssets.Add(retAsset);
_sharedLoadedAssets.Add(retAsset);
return retAsset;
}
public void UnloadAllAssets()
{
foreach (var item in _loadedAssets)
{
Resources.UnloadAsset(item);
}
_loadedAssets.Clear();
}
}
}
简单引用计数器
资源能够很好的加载,但是卸载就有点问题了。为什么这么说?
假设脚本A引用一个音频资源,脚本B也引用这个音频资源。当销毁脚本A的时候,会卸载这个音频资源吗?答案是不会的,因为如果卸载了音频资源,那么脚本B就无法使用这个音频资源了。
那么怎么去判断这个音频资源有没有被其他脚本所引用呢?
其实也比较简单,就是在加载资源的时候做一个计数判断,当一个脚本加载这个资源的时候,这个资源的引用就+1。当一个脚本卸载这个资源的时候,这个资源的引用就-1。如果资源的引用=0的时候,就表示这个资源没有被任何脚本引用,所以我们需要卸载该资源。
为此,我们可以制作一个引用计数接口有:资源引用的数量、当资源加载时引用数量如何改变、当资源卸载时引用数量如何改变。
public interface IRefCounter
{
int RefCount { get; }
void Retain(object refOwner = null);
void Release(object refOwner = null);
}
一个简单的引用计数器SimpleRc类就可以实现这个接口。只要继承这个SimpleRc类,那么继承SimpleRc类的这个类就拥有计数功能。
SimpleRc类的核心逻辑比较简单:加载资源时,需要调用Retain。卸载资源时,需要调用Release。当RefCount=0时,需要实现真正的卸载。
所以需要重写OnZeroRef方法。
public class SimpleRc : IRefCounter
{
public int RefCount { get; private set; }
public void Retain(object refOwner = null)
{
RefCount++;
}
public void Release(object refOwner = null)
{
RefCount--;
if (RefCount <= 0)
{
OnZeroRef();
}
}
protected virtual void OnZeroRef()
{
}
}
接着我们将资源进行一次封装。
public class Res : SimpleRc {
// 资源
public Object Asset { get; protected set; }
// 资源名
public string Name { get; protected set; }
protected override void OnZeroRef()
{
base.OnZeroRef();
}
}
那么我们实际的加载资源和卸载资源的逻辑应该放在Res类中,而不是ResLoader类。为此我们可以修改之前的代码。
所以我们需要重新定义类的功能
Res类:管理真正的资源,处理加载和卸载的逻辑。
ResLoader:加载资源,但不管加载的具体实现。
public class Res : SimpleRc {
// 资源
public Object Asset;
// 资源名
public string Name => Asset.name;
// 资源路径
public string AssetPath;
public Res(string assetName)
{
AssetPath = assetName;
}
public T LoadSync<T>() where T : Object
{
Asset = Resources.Load(AssetPath);
// 资源加载完毕后,资源引用+1
Retain();
return Asset as T;
}
protected override void OnZeroRef()
{
base.OnZeroRef();
// 资源正在的卸载
Resources.UnloadAsset(Asset);
Asset = null;
}
}
public class ResLoader
{
private static List<Res> _sharedLoadedAssets = new();
private List<Res> _loadedAssets = new();
public T LoadAsset<T>(string assetName) where T : Object
{
// 先从本地找
var res = _loadedAssets.Find((e) => e.AssetPath == assetName);
if (res != null)
{
return res.Asset as T;
}
// 再从全局找
res = _sharedLoadedAssets.Find((e) => e.AssetPath == assetName);
if (res != null)
{
_loadedAssets.Add(res);
return res.Asset as T;
}
// 全局和本地都没有找到
res = new Res(assetName);
var asset = res.LoadSync<T>();
_loadedAssets.Add(res);
_sharedLoadedAssets.Add(res);
return asset;
}
public void UnloadAllAssets()
{
foreach (var item in _loadedAssets)
{
item.Release();
}
_loadedAssets.Clear();
}
}
资源UI显示
为了方便测试,我们需要制作一个资源显示,其核心方法就是显示全局变量里面的资源,为了方便管理,我们需要将ResLoader中代码
private static List<Res> _sharedLoadedAssets = new();
移动到ResManager当中,当我们要查看资源时,只需要按下F2,就可以显示当前的所有资源
namespace DYFramework.ResKit
{
public class ResManager : MonoSingletonBase<ResManager>
{
public static List<Res> SharedLoadedReses = new();
#if UNITY_EDITOR
private void OnGUI()
{
if (Input.GetKey(KeyCode.F2))
{
GUILayout.BeginVertical("box");
SharedLoadedReses.ForEach((e) =>
{
GUILayout.Label($"Name: {e.Name}, refCount: {e.RefCount}, State: {e.State}");
});
GUILayout.EndVertical();
}
}
#endif
}
}
评论区