【unity框架开发6】Resources的使用,如何封装一个Resources资源管理器(2024/10/25补充)

一、Unity资源加载的几种方式

1、Inspector窗口拖拽

  • 在脚本中用public声明变量,然后在Inspector窗口把要加载的资源拖拽给该脚本的变量。
  • 不建议在大型项目使用。在公司的项目也不要用。如果你是独立游戏开发者,则可以用。
  • 不支持热更新。

2、Resources

  • 用Resources.Load方法、Resources.LoadAsync方法、Resources.LoadAll方法来加载资源。
  • 可以在商业项目使用,包括公司的项目。但是Resources文件夹中可以存放的资源有限,大概只能存储2G左右的资源,因此要谨慎使用。
  • 不支持热更新。

3、AssetBundle

  • 用AssetBundle.LoadFromXXX方法来加载资源。
  • 商业项目常用的资源加载方案,如果你在公司做项目,则推荐用这种方式来加载资源。
  • 效率比Resources加载高,占用内存小,正式发布游戏后,资源所占用的空间也小。
  • 支持热更新。

4、Addressables(可寻址资源系统)

  • 可以理解为高级的AssetBundle,资源管理由Unity内部自动完成。
  • 但是目前还在发展中,可能有bug。主流的商业游戏都是使用AssetBundle来做资源加载的。
  • 支持热更新。

5、AssetDatabase

  • 用AssetDatabase.LoadAssetAtPath方法来加载资源。
  • 仅限于编辑器模式,主要用于在编辑器模式下用代码更改本地文件。
  • 游戏运行时不会用这种方案加载资源。
  • 不支持热更新。

二、准备

使用前必须在项目中创建一个名叫Resources的文件夹,这个名字是固定的。
在这里插入图片描述

三、同步加载Resources资源

1、Resources.Load同步加载单个资源

1.1、基本加载

Resources.Load(string 要加载的资源的路径)

返回值是Object型。

如果有多个相同路径的资源,则只会返回找到的第一个资源。

调用案例

GameObject go = Resources.Load("Prefabs/Cube") as GameObject;

1.2、加载指定类型的资源

Resources.Load(string 要加载的资源的路径, System.Type 要加载的资源的类型的Type对象)

返回值是Object型。

如果有多个相同类型且相同路径的资源,则只会返回找到的第一个资源。

调用案例

GameObject go = Resources.Load("Prefabs/Cube", typeof(GameObject)) as GameObject;

1.3、使用泛型加载指定类型的资源

Resources.Load<要加载的资源的类型>(string 要加载的资源的路径)

返回值是T型。

返回值是要加载的资源的类型。如果有多个相同类型,且相同路径的资源,则只会返回找到的第一个资源。

调用案例

GameObject go = Resources.Load<GameObject>("Prefabs/Cube");

2、Resources.LoadAll同步加载多个资源

2.1、基本加载

Resources.LoadAll(string 要加载的资源的文件夹路径或文件路径)

返回值是Object[]型。

同步加载Resources文件夹中指定路径的文件夹中的所有资源,包括其中子孙文件夹中的所有资源,然后返回到一个Object[]型数组。

如果该路径是一个文件,则只会加载该文件,并返回到一个Object[]型数组。如果没有加载到任何资源,则返回一个没有任何元素的Object[]型数组。

调用案例

Object[] gos = Resources.LoadAll("Prefabs");
for (int i = 0; i < gos.Length; i++)
{
    Instantiate(gos[i] as GameObject);
}

2.2、加载指定类型的资源

Resources.LoadAll(string 要加载的资源的文件夹路径或文件路径,System.Type 要加载的资源的类型的Type对象)

返回值是Object[]型。

同步加载Resources文件夹中指定路径的文件夹中的指定类型的所有资源,包括其中子孙文件夹中的该类型的所有资源,然后返回到一个Object[]型数组。

如果该路径是一个该指定类型的文件,则只会加载该文件,并返回到一个Object[]型数组。如果没有加载到任何资源,则返回一个没有任何元素的Object[]型数组。

调用案例

Object[] gos = Resources.LoadAll("Prefabs", typeof(GameObject));
for (int i = 0; i < gos.Length; i++)
{
    Instantiate(gos[i] as GameObject);
}

2.3、使用泛型加载指定类型的资源

Resources.LoadAll<泛型T类型>(string 要加载的资源的文件夹路径或文件路径)

返回值是T[]型。

同步加载Resources文件夹中指定路径的文件夹中的指定类型的所有资源,包括其中子孙文件夹中的该类型的所有资源,然后返回到一个T[]型数组。

如果该路径是一个该指定类型的文件,则只会加载该文件,并返回到一个T[]型数组。如果没有加载到任何资源,则返回一个没有任何元素的T[]型数组。

调用案例

GameObject[] gos = Resources.LoadAll<GameObject>("Prefabs");
for (int i = 0; i < gos.Length; i++)
{
    Instantiate(gos[i]);
}

四、异步加载Resources文件夹中的资源

Resources.LoadAsync异步加载单个资源方法

使用泛型加载指定类型的资源

Resources.LoadAsync<泛型T类型>(string 要加载的资源的路径)

返回值是ResourceRequest类型。

一般配合协程来使用。在协程中可以使用yield return来等待资源加载。

如果有多个相同路径的资源,则只会加载找到的第一个资源。

调用案例

void Start()
{
   StartCoroutine(LoadAsyncCoroutine());
}

IEnumerator LoadAsyncCoroutine(){
   //开始异步加载资源
   ResourceRequest rr = Resources.LoadAsync<GameObject>("Prefabs/Cube");

   //等待资源加载完毕
   yield return rr;

   //加载完成执行逻辑
   Instantiate(rr.asset);
}

五、卸载Resources资源

卸载单个独立的资源

AudioClip audioClip = Resources.Load<AudioClip>("Sound");
Resources.UnloadAsset(audioClip);

卸载prefab资源

使用前面的UnloadAsset进行卸载prefab资源

GameObject cube = Resources.Load<AudioClip>("Prefabs/Cube1");
Resources.UnloadAsset(cube );

运行你会发现报错了
在这里插入图片描述
意思是说,UnloadAsset这个API只支持独立的资源,不能用GameObject、Component、.AssetBundle资源的卸载上。
这样的话,Prefab就没法使用Resources.UnloadAsset这个API进行卸载了。

我们还有一个比较靠谱的方法,就是使用Resources.UnloadUnusedAssets这个API卸载。

GameObject cube = Resources.Load<AudioClip>("Prefabs/Cube1");
cube = null;
Resources.UnloadUnusedAssets();

你会发现Resources.UnloadUnusedAssets没有可以指定的参数,因为其实它会卸载所有资源的,如果你想卸载前面的其他资源,记得将资源的引用置空即可全部一起卸载

AudioClip audioClip = Resources.Load<AudioClip>("Sound");
GameObject cube = Resources.Load<AudioClip>("Prefabs/Cube1");
cube = null;
audioClip = null;
Resources.UnloadUnusedAssets();

所以目前,我们要加载的资源分为两种,一种是可以使用Resources.UnloadAsset进行卸载的,另一种的则是只能使用Resources.UnloadUnusedAssets进行卸载

想说两个大家要注意的事情。

第一个要注意的是Resources.UnloadUnusedAssets只能卸载没有引用的资源,比如在示例的代码中,必须要将cube = null(顺序无所谓),加载的Prefab才可以被Resources.UnloadUnusedAssets卸载。

第二个要注意的是Resources.UnloadUnusedAssets会带来很多的风险。从资源管理的角度来讲,它无法进行精确地对某个资原进行载。从工作机制来说,它会触发GC.Collect的调用,会造成游戏的卡顿。除了这两点,当项目复杂了之后,会造成很多莫名奇妙的bug,比如贴图丢失变黑等等,这些罪魁祸首很有可能就是使用了这个API。

六、资源查看——Profiler (分析器)

Profiler (分析器)可以查看资源加载卸载情况

打开
在这里插入图片描述
可以看到默认资源5489
在这里插入图片描述
比如我们加载音频之后变成了5490,卸载又变成5489,说明资源加载卸载成功
在这里插入图片描述

七、封装一个Resources资源管理器

1、封装异步加载资源

注意这个脚本依赖Mono管理器:【unity进阶知识2】Mono管理器

public class ResourcesManager : Singleton<ResourcesManager>
{
    /// <summary>
    /// 异步加载Resources文件夹中指定类型的资源
    /// </summary>
    public void LoadAsync<T>(string path, UnityAction<T> callback = null) where T : Object
    {
        MonoManager.Instance.StartCoroutine(LoadAsyncCoroutine(path, callback));
    }
    IEnumerator LoadAsyncCoroutine<T>(string path, UnityAction<T> callback = null) where T : Object
    {
        ResourceRequest resourceRequest = Resources.LoadAsync<T>(path);
        yield return resourceRequest;
        callback?.Invoke(resourceRequest.asset as T);
    }
}

调用,效果和前面一样

ResourcesManager.Instance.LoadAsync<GameObject>("Prefabs/Cube", Callback);

void Callback(GameObject obj){
    Instantiate(obj);
}

或者

ResourcesManager.Instance.LoadAsync<GameObject>("Prefabs/Cube", (obj)=>{
    Instantiate(obj);
});

2、封装同步加载资源

同步加载封装虽然是一样的,但是好处是自己封装可以统一管理和进行自定义注释

/// <summary>
/// <para>同步加载Resources文件夹中指定类型的资源。</para>
/// <para>如果有多个相同类型,且相同路径的资源,则只会返回找到的第一个资源。</para>
/// </summary>
/// <typeparam name="T">要加载的资源的类型</typeparam>
/// <param name="path">要加载的资源的路径。例如"Prefabs/Cube"表示Resources文件夹中的Prefabs文件夹中的名叫Cube的资源。</param>
public T Load<T>(string path) where T : Object
{
    return Resources.Load<T>(path);
}

调用

GameObject[] gos = ResourcesManager.Instance.LoadAll<GameObject>("Prefabs");
for (int i = 0; i < gos.Length; i++)
{
    Instantiate(gos[i]);
}

3、封装卸载资源方法

通过Resources加载的资源,就算销毁了,但是它还是占在内存里面,没有释放,所以我们需要卸载资源,比如切换场景的时候

/// <summary>
/// <para>异步卸载所有用Resources方式加载到内存中且当前没有被任何地方使用的资源。</para>
/// <para>例如要卸载某一个用Resources方式加载的预制体,则必须确保场景中所有这个预制体创建的物体都被销毁了,且这个预制体资源没有赋值给任何脚本中的任何变量。</para>
/// <para>如果有,可以把该变量也赋值为null,这样本方法才能成功释放它。</para>
/// </summary>
/// <param name="callback">资源卸载完毕后执行的逻辑</param>
public void UnloadUnusedAssets(UnityAction callback = null)
{
    MonoManager.Instance.StartCoroutine(UnLoadUnusedAssetsCoroutine(callback));
}
IEnumerator UnLoadUnusedAssetsCoroutine(UnityAction callback = null)
{
    //异步操作对象,记录了异步操作的数据。
    AsyncOperation asyncOperation = Resources.UnloadUnusedAssets();

    //等待资源卸载
    while (asyncOperation.progress < 1)
        yield return null;

    //资源卸载完毕后执行的逻辑
    callback?.Invoke();
}

调用

ResourcesManager.Instance.UnloadUnusedAssets(() =>
{
    Debug.Log("异步卸载所有资源成功");
});

4、最终代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using XYFrame;

/// <summary>
/// Resources资源加载管理器
/// 这个脚本依赖Mono管理器。
/// 要加载的资源必须放到项目中名叫Resources的文件夹中。项目中可以有多个名叫Resources的文件夹,但如此一来,必须避免资源的路径相同。
/// </summary>
public class ResourcesManager : Singleton<ResourcesManager>
{
    #region 同步加载单个资源
    /// <summary>
    /// <para>同步加载Resources文件夹中的资源。</para>
    /// <para>如果有多个相同路径的资源,则只会返回找到的第一个资源。</para>
    /// </summary>
    /// <param name="path">要加载的资源的路径。例如"Prefabs/Cube"表示Resources文件夹中的Prefabs文件夹中的名叫Cube的资源。</param>
    public Object Load(string path)
    {
        return Resources.Load(path);
    }

    /// <summary>
    /// <para>同步加载Resources文件夹中指定类型的资源。</para>
    /// <para>如果有多个相同类型,且相同路径的资源,则只会返回找到的第一个资源。</para>
    /// </summary>
    /// <param name="path">要加载的资源的路径。例如"Prefabs/Cube"表示Resources文件夹中的Prefabs文件夹中的名叫Cube的资源。</param>
    /// <param name="systemTypeInstance">要加载的资源的类型的Type对象。例如typeof(GameObject)表示要加载的资源的类型是GameObject型。</param>
    /// <returns></returns>
    public Object Load(string path, System.Type systemTypeInstance)
    {
        return Resources.Load(path, systemTypeInstance);
    }

    /// <summary>
    /// <para>同步加载Resources文件夹中指定类型的资源。</para>
    /// <para>如果有多个相同类型,且相同路径的资源,则只会返回找到的第一个资源。</para>
    /// </summary>
    /// <typeparam name="T">要加载的资源的类型</typeparam>
    /// <param name="path">要加载的资源的路径。例如"Prefabs/Cube"表示Resources文件夹中的Prefabs文件夹中的名叫Cube的资源。</param>
    public T Load<T>(string path) where T : Object
    {
        return Resources.Load<T>(path);
    }
    #endregion

    #region 同步加载多个资源
    /// <summary>
    /// <para>同步加载Resources文件夹中指定路径的文件夹中的所有资源,包括其中子孙文件夹中的所有资源,然后返回到一个Object[]型数组。</para>
    /// <para>如果该路径是一个文件,则只会加载该文件,并返回到一个Object[]型数组。</para>
    /// <para>如果没有加载到任何资源,则返回一个没有任何元素的Object[]型数组。</para>
    /// </summary>
    /// <param name="path">要加载的文件夹或文件的路径。例如"Prefabs"表示Resources文件夹中的Prefabs文件夹。例如"Prefabs/Cube"表示Resources文件夹中的Prefabs文件夹中的名叫Cube的资源。</param>
    public Object[] LoadAll(string path)
    {
        return Resources.LoadAll(path);
    }

    /// <summary>
    /// <para>同步加载Resources文件夹中指定路径的文件夹中指定类型的所有资源,包括其中子孙文件夹中的该类型的所有资源,然后返回到一个Object[]型数组。</para>
    /// <para>如果该路径是一个该指定类型的文件,则只会加载该文件,并返回到一个Object[]型数组。</para>
    /// <para>如果没有加载到任何资源,则返回一个没有任何元素的Object[]型数组。</para>
    /// </summary>
    /// <param name="path">要加载的文件夹或文件的路径。例如"Prefabs"表示Resources文件夹中的Prefabs文件夹。例如"Prefabs/Cube"表示Resources文件夹中的Prefabs文件夹中的名叫Cube的资源。</param>
    /// <param name="systemTypeInstance">要加载的资源的类型的Type对象。例如typeof(GameObject)表示要加载的资源的类型是GameObject型。</param>
    public Object[] LoadAll(string path, System.Type systemTypeInstance)
    {
        return Resources.LoadAll(path, systemTypeInstance);
    }

    /// <summary>
    /// <para>同步加载Resources文件夹中指定路径的文件夹中指定类型的所有资源,包括其中子孙文件夹中的该类型的所有资源,然后返回到一个Object[]型数组。</para>
    /// <para>如果该路径是一个该指定类型的文件,则只会加载该文件,并返回到一个Object[]型数组。</para>
    /// <para>如果没有加载到任何资源,则返回一个没有任何元素的Object[]型数组。</para>
    /// </summary>
    /// <typeparam name="T">要加载的资源的类型</typeparam>
    /// <param name="path">要加载的文件夹或文件的路径。例如"Prefabs"表示Resources文件夹中的Prefabs文件夹。例如"Prefabs/Cube"表示Resources文件夹中的Prefabs文件夹中的名叫Cube的资源。</param>
    public T[] LoadAll<T>(string path) where T : Object
    {
        return Resources.LoadAll<T>(path);
    }

    /// <summary>
    /// <para>同步把Resources文件夹中指定路径的文件夹及其所有子孙文件夹中所有指定类型的资源添加到一个新建的字典中,并返回该字典。</para>
    /// <para>应保证Prefabs文件夹中以及它的子孙文件夹中没有重名的资源,如果有重名的,则只会添加找到的第一个资源进字典,其它重名的资源不会进到字典中。</para>
    /// </summary>
    /// <typeparam name="T">要加载的资源类型</typeparam>
    /// <param name="path">资源的路径。例如"Folder/Res"表示Resources文件夹中的Folder文件夹中的Res文件夹</param>
    public Dictionary<string, T> LoadAllIntoDictionary<T>(string path) where T : Object
    {
        Dictionary<string, T> dic = new Dictionary<string, T>();
        T[] temp = Resources.LoadAll<T>(path);
        for (int i = 0; i < temp.Length; i++)
        {
            if (!dic.ContainsKey(temp[i].name))//字典不存在该键,才添加进去。这样可以防止字典的键名重复而报错。
            {
                dic.Add(temp[i].name, temp[i]);
            }
            else//如果字典已经存在该键,则跳过这个资源,并输出警告,不将它加入到字典中
            {
                Debug.LogWarning(string.Format("Resources/{0}的子孙文件夹的资源{1}与已经添加到字典中的资源重名,因此无法将它添加到字典中,请确保加载的资源的名字是唯一的。", path, temp[i].name));
            }
        }
        return dic;
    }

    /// <summary>
    /// <para>同步把Resources文件夹中指定路径及其所有子孙文件夹中所有指定类型的资源添加到指定的字典中</para>
    /// </summary>
    /// <typeparam name="T">要加载的资源类型</typeparam>
    /// <param name="path">资源的路径。例如"Folder/Res"表示Resources文件夹中的Folder文件夹中的Res文件夹</param>
    /// <param name="dictionary">指定的字典</param>
    public void LoadAllIntoDictionary<T>(string path, Dictionary<string, T> dictionary) where T : Object
    {
        T[] temp = Resources.LoadAll<T>(path);
        for (int i = 0; i < temp.Length; i++)
        {
            if (!dictionary.ContainsKey(temp[i].name))//字典不存在该键,才添加进去。这样可以防止字典的键名重复而报错。
            {
                dictionary.Add(temp[i].name, temp[i]);
            }
            else//如果字典已经存在该键,则跳过这个资源,并输出警告,不将它加入到字典中
            {
                Debug.LogWarning(string.Format("Resources/{0}的子孙文件夹的资源{1}与已经添加到字典中的资源重名,因此无法将它添加到字典中,请确保加载的资源的名字是唯一的,并且传入参数的字典中不包含该名字的资源。", path, temp[i].name));
            }
        }
    }
    #endregion

    #region 异步加载单个资源
    /// <summary>
    /// <para>异步加载Resources文件夹中的资源。</para>
    /// <para>如果有多个相同路径的资源,则只会加载找到的第一个资源。</para>
    /// </summary>
    /// <param name="path">要加载的资源的路径。例如"Prefabs/Cube"表示Resources文件夹中的Prefabs文件夹中的名叫Cube的资源。</param>
    /// <param name="callback">资源加载完毕后要执行的逻辑。参数表示加载的资源。</param>
    public void LoadAsync(string path, UnityAction<Object> callback = null)
    {
        MonoManager.Instance.StartCoroutine(LoadAsyncCoroutine(path, callback));
    }
    IEnumerator LoadAsyncCoroutine(string path, UnityAction<Object> callback = null)
    {
        ResourceRequest resourceRequest = Resources.LoadAsync(path);
        yield return resourceRequest;
        callback?.Invoke(resourceRequest.asset);
    }

    /// <summary>
    /// <para>异步加载Resources文件夹中指定类型的资源。</para>
    /// <para>如果有多个相同类型,且相同路径的资源,则只会加载找到的第一个资源。</para>
    /// </summary>
    /// <param name="path"></param>
    /// <param name="type">要加载的资源的类型的Type对象。例如typeof(GameObject)表示要加载的资源的类型是GameObject型。</param>
    /// <param name="callback">资源加载完毕后要执行的逻辑。参数表示加载的资源。</param>
    public void LoadAsync(string path, System.Type type, UnityAction<Object> callback = null)
    {
        MonoManager.Instance.StartCoroutine(LoadAsyncCoroutine(path, type, callback));
    }
    IEnumerator LoadAsyncCoroutine(string path, System.Type type, UnityAction<Object> callback = null)
    {
        ResourceRequest resourceRequest = Resources.LoadAsync(path, type);
        yield return resourceRequest;
        callback?.Invoke(resourceRequest.asset);
    }

    /// <summary>
    /// <para>异步加载Resources文件夹中指定类型的资源。</para>
    /// <para>如果有多个相同类型,且相同路径的资源,则只会加载找到的第一个资源。</para>
    /// </summary>
    /// <typeparam name="T">加载的资源的类型</typeparam>
    /// <param name="path">要加载的资源的路径。例如"Prefabs/Cube"表示Resources文件夹中的Prefabs文件夹中的名叫Cube的资源。</param>
    /// <param name="callback">资源加载完毕后要执行的逻辑</param>
    public void LoadAsync<T>(string path, UnityAction<T> callback = null) where T : Object
    {
        MonoManager.Instance.StartCoroutine(LoadAsyncCoroutine(path, callback));
    }
    IEnumerator LoadAsyncCoroutine<T>(string path, UnityAction<T> callback = null) where T : Object
    {
        ResourceRequest resourceRequest = Resources.LoadAsync<T>(path);
        yield return resourceRequest;
        callback?.Invoke(resourceRequest.asset as T);
    }
    #endregion

    #region 卸载资源
    /// <summary>
    /// <para>异步卸载所有用Resources方式加载到内存中且当前没有被任何地方使用的资源。</para>
    /// <para>例如要卸载某一个用Resources方式加载的预制体,则必须确保场景中所有这个预制体创建的物体都被销毁了,且这个预制体资源没有赋值给任何脚本中的任何变量。</para>
    /// <para>如果有,可以把该变量也赋值为null,这样本方法才能成功释放它。</para>
    /// </summary>
    /// <param name="callback">资源卸载完毕后执行的逻辑</param>
    public void UnloadUnusedAssets(UnityAction callback = null)
    {
        MonoManager.Instance.StartCoroutine(UnLoadUnusedAssetsCoroutine(callback));
    }
    IEnumerator UnLoadUnusedAssetsCoroutine(UnityAction callback = null)
    {
        //异步操作对象,记录了异步操作的数据。
        AsyncOperation asyncOperation = Resources.UnloadUnusedAssets();

        //等待资源卸载
        while (asyncOperation.progress < 1)
            yield return CoroutineTool.WaitForFrame();

        //资源卸载完毕后执行的逻辑
        callback?.Invoke();
    }

    /// <summary>
    /// <para>同步卸载指定的资源。</para>
    /// <para>只能卸载非GameObject类型和Component类型,例如Mesh、Texture、Material、Shader。如果卸载了不让卸载的资源,则会报错。</para>
    /// <para>如果随后加载的任何场景或资源引用了该资源,将导致从磁盘中加载该资源的新实例。此新实例将与先前卸载的对象相互独立。</para>
    /// </summary>
    /// <param name="assetToUnload">要卸载的资源</param>
    public void UnloadAsset(Object assetToUnload)
    {
        Resources.UnloadAsset(assetToUnload);
    }
    #endregion
}

八、封装更好的Resources资源管理器加载(2024/10/25补充)

1、 加载资源比较多时,使用list列表存储

如下,随着要加载的资源原来越多,我们都需要这样写,每次都要书写回收资源代码,很容易遗忘
在这里插入图片描述
我们可以改为如下,使用list列表存储
在这里插入图片描述
这样不管加载多少个资源,成员变量和卸载相关的代码就固定了。

2、资源的重复加载和卸载

对于Resources这个API来说,重复加载或者重复卸载,Unity都做了容错处理,但是AssetBundle就可能比较危险了,重复加载AssetBundle会导致闪退。所以这种逻辑比较危险了,是要避免的。

要解决重复加载或者卸载的问题其实也很简单,只要封装一个方法就好了,代码如下:
就是加载资源时先查看资源是否已经存在
在这里插入图片描述
为了方便使用,我们可以进行简单的封装为ResLoader
在这里插入图片描述
测试使用
在这里插入图片描述

3、全局重复卸载加载问题

以上的代码解决了重复加载和卸载的问题,但是解决的问题只是针对于这个界面,如果在其他界面同时加载Iogo这个贴图,结果还是会造成重复的加载或卸载的问题。

3.1 全局重复加载问题

要解决这个问题其实非常简单,只要加上一个静态的List用来记录已经加载过的资源就可以了。
在这里插入图片描述

3.2 全局重复卸载问题

比如UIXXXPanel和UIYYYPanel都用到了bgTexture资源,那当UIXXXPane|关闭的时候,我是否应该卸载bgTexture资原呢?答案当然不是的.

我们需要当UIXXXPanel和UIYYYPanel都关闭的时候,bgTexture才可以卸载

这种逻辑怎么实现呢?答案是使用引用计数

可能大家听过这个名词,它的原理非常简单,比如bgTexture这个资源,被获取一次,就进行加1操作,当不用的时候就进行减1操作。当计数的变为0的时候进行才进行真正的卸载操作。

封装Res类使用引用计数

直接看下代码,封装一个Res类
在这里插入图片描述
修改ResLoader,使用前面的Res类
在这里插入图片描述
在这里插入图片描述

封装简易的计数器实现

计数器的接口

public interface IRefCounter
{
    int RefCount { get; }
    void Retain(object refOwner = null);
    void Release(object refOwner = null);
}

Retain是增加l用计数(RefCount++),Release减去一个引用计数(RefCount–)。

在接下来的计数器具体实现中,当RefCount降为0时我们需要捕获一个事件,这个事件叫OnZeroRef
代码如下:

using UnityEngine;

public class SimpleRC : IRefCounter
{
    public int RefCount {get; private set; }

    public SimpleRC(){
        RefCount = 0;
    }
    
    public void Retain(object refOwner = null)
    {
        ++RefCount;
    }

    public void Release(object refOwner = null)
    {
        --RefCount;
        if(RefCount == 0){
            OnZeroRef()
        }
    }

    protected virtual void OnZeroRef(){ }
}
把刚刚封装的简易引用计数器应用到Res类中

在这里插入图片描述

4、显示资源信息

虽然我们可以通过Profiler (分析器)查看资源的加载和卸载情况,但是不是很方便,我们可以书写代码让资源信息在界面显示

封装显示资源信息ResMgr类

/// <summary>
/// 显示资源信息
/// </summary>
public class ResMgr : SingletonMono<ResMgr>
{
    public List<Res> SharedLoadedReses = new List<Res>();

#if UNITY_EDITOR
    private void OnGUI()
    {
        if (Input.GetKey(KeyCode.F2))
        {
            GUILayout.BeginVertical("box");
            SharedLoadedReses.ForEach(loadedRes =>
            {
                GUILayout.Label(string.Format("资源名称:{0} 引用计数 {1}", loadedRes.Name, loadedRes.RefCount));
            });
            GUILayout.EndVertical();
        }

    }
#endif
}

修改ResLoader

public class ResLoader
{
    private List<Res> mResRecord = new List<Res>();

    public T LoadSync<T>(string assetName) where T : UnityEngine.Object
    {
        var res = GetOrCreateRes(assetName);
        if (res != null)
        {
            return res.Asset as T;
        }
        //真正加载资源
        res = CreateRes(assetName);
        res.LoadSync();
        return res.Asset as T;
    }

    public void LoadAsync<T>(string assetName, Action<T> onLoaded) where T : UnityEngine.Object
    {
        //查询当前的资源记录
        var res = GetOrCreateRes(assetName);
        if (res != null)
        {
            onLoaded(res.Asset as T);
            return;
        }

        //真正加载资源
        res = CreateRes(assetName);
        res.LoadAsync(loadedRes =>
        {
            onLoaded(loadedRes.Asset as T);
        });
    }

    public void ReleaseAll()
    {
        mResRecord.ForEach(loadedAsset => loadedAsset.Release());
        mResRecord.Clear();
    }

    #region 内部方法
    private Res GetResFromRecord(string assetName)
    {
        return mResRecord.Find(loadedAsset => loadedAsset.Name == assetName);
    }
    private Res GetFromResMgr(string assetName)
    {
        return ResMgr.Instance.SharedLoadedReses.Find(loadedAsset => loadedAsset.Name == assetName);
    }
    private void AddRes2Record(Res resFromResMgr)
    {
        mResRecord.Add(resFromResMgr);
        resFromResMgr.Retain();
    }
    private Res CreateRes(string assetName)
    {
        var res = new Res(assetName);
        ResMgr.Instance.SharedLoadedReses.Add(res);
        AddRes2Record(res);
        return res;
    }
    private Res GetOrCreateRes(string assetName)
    {
        //查询当前的资源记录
        var res = GetResFromRecord(assetName);
        if (res != null)
        {
            return res;
        }
        //查询全局资源池
        res = GetFromResMgr(assetName);
        if (res != null)
        {
            AddRes2Record(res);
            return res;
        }
        return res;
    }
    #endregion
}

测试调用

public class UIXXX : MonoBehaviour
{
    ResLoader mResLoader = new ResLoader();
    private IEnumerator Start()
    {
        yield return new WaitForSeconds(2f);
        //加载资源
        mResLoader.LoadSync<AudioClip>("Sound");
        mResLoader.LoadAsync<GameObject>("Prefabs/Cube1", cube1 =>
        {
            Instantiate(cube1);
        });

        
        yield return new WaitForSeconds(5f);
        //卸载资源
        mResLoader.ReleaseAll();
    }
}

效果, 按住F2显示资源信息
在这里插入图片描述

5、封装LoadSync同步加载资源

修改Res,封装LoadSync同步加载资源
在这里插入图片描述

修改Reloader,调用
在这里插入图片描述
改名
在这里插入图片描述
测试调用,资源放在resources
在这里插入图片描述

6、卸载prefab

可以使用is来判断,它是否是prefab类型的资源,如果是,则使用UnloadUnuserdAssets进行卸载操作。
在这里插入图片描述

测试调用
在这里插入图片描述

7、封装LoadAsync异步加载资源

修改Res,封装LoadAsync异步加载资源方法

public void LoadAsync(Action<Res> onLoaded)
{
    var resRequest = Resources.LoadAsync(mAssetPath);
    resRequest.completed += operation =>
    {
        Asset = resRequest.asset;
        onLoaded(this);
    };
}

修改ResLoader调用

public class ResLoader
{
    private List<Res> mResRecord = new List<Res>();

    public T LoadSync<T>(string assetName) where T : UnityEngine.Object
    {
        var res = GetOrCreateRes(assetName);
        if (res != null)
        {
            return res.Asset as T;
        }
        //真正加载资源
        res = CreateRes(assetName);
        res.LoadSync();
        return res.Asset as T;
    }

    public void LoadAsync<T>(string assetName, Action<T> onLoaded) where T : UnityEngine.Object
    {
        //查询当前的资源记录
        var res = GetOrCreateRes(assetName);
        if (res != null)
        {
            onLoaded(res.Asset as T);
            return;
        }

        //真正加载资源
        res = CreateRes(assetName);
        res.LoadAsync(loadedRes =>
        {
            onLoaded(loadedRes.Asset as T);
        });
    }

    public void ReleaseAll()
    {
        mResRecord.ForEach(loadedAsset => loadedAsset.Release());
        mResRecord.Clear();
    }

    #region 内部方法
    private Res GetResFromRecord(string assetName)
    {
        return mResRecord.Find(loadedAsset => loadedAsset.Name == assetName);
    }
    private Res GetFromResMgr(string assetName)
    {
        return ResMgr.Instance.SharedLoadedReses.Find(loadedAsset => loadedAsset.Name == assetName);
    }
    private void AddRes2Record(Res resFromResMgr)
    {
        mResRecord.Add(resFromResMgr);
        resFromResMgr.Retain();
    }
    private Res CreateRes(string assetName)
    {
        var res = new Res(assetName);
        ResMgr.Instance.SharedLoadedReses.Add(res);
        AddRes2Record(res);
        return res;
    }
    private Res GetOrCreateRes(string assetName)
    {
        //查询当前的资源记录
        var res = GetResFromRecord(assetName);
        if (res != null)
        {
            return res;
        }
        //查询全局资源池
        res = GetFromResMgr(assetName);
        if (res != null)
        {
            AddRes2Record(res);
            return res;
        }
        return res;
    }
    #endregion
}

测试调用

public class UIXXX : MonoBehaviour
{
    ResLoader mResLoader = new ResLoader();
    private IEnumerator Start()
    {
        yield return new WaitForSeconds(2f);
        
        //同步加载资源
        mResLoader.LoadSync<AudioClip>("Sound");
        
        //异步加载资源
        mResLoader.LoadAsync<GameObject>("Prefabs/Cube1", cube1 =>
        {
            Instantiate(cube1);
        });

        
        yield return new WaitForSeconds(5f);
        
        //卸载资源
        mResLoader.ReleaseAll();
    }
}

8、最终代码

IRefCounter

public interface IRefCounter
{
    int RefCount { get; }
    void Retain(object refOwner = null);
    void Release(object refOwner = null);
}

SimpleRC

public class SimpleRC : IRefCounter
{
    public int RefCount {get; private set; }

    public SimpleRC(){
        RefCount = 0;
    }
    
    public void Retain(object refOwner = null)
    {
        ++RefCount;
    }

    public void Release(object refOwner = null)
    {
        --RefCount;
        if(RefCount == 0){
            OnZeroRef();
        }
    }

    protected virtual void OnZeroRef(){

    }
}

SharedLoadedReses

/// <summary>
/// 显示资源信息
/// </summary>
public class ResMgr : SingletonMono<ResMgr>
{
    public List<Res> SharedLoadedReses = new List<Res>();

#if UNITY_EDITOR
    private void OnGUI()
    {
        if (Input.GetKey(KeyCode.F2))
        {
            GUILayout.BeginVertical("box");
            SharedLoadedReses.ForEach(loadedRes =>
            {
                GUILayout.Label(string.Format("资源名称:{0} 引用计数 {1}", loadedRes.Name, loadedRes.RefCount));
            });
            GUILayout.EndVertical();
        }

    }
#endif
}

Res

public class Res : SimpleRC
{
    public UnityEngine.Object Asset { get; private set; }

    public string Name { get; private set; }

    private string mAssetPath;

    public Res(string assetPath)
    {
        mAssetPath = assetPath;
        Name = assetPath;
    }
    
    public bool LoadSync()
    {
        return Asset = Resources.Load(mAssetPath);
    }

    public void LoadAsync(Action<Res> onLoaded)
    {
        var resRequest = Resources.LoadAsync(mAssetPath);
        resRequest.completed += operation =>
        {
            Asset = resRequest.asset;
            onLoaded(this);
        };
    }

    protected override void OnZeroRef()
    {
        if (Asset is GameObject)
        {
            Asset = null;
            Resources.UnloadUnusedAssets();
        }
        else
        {
            Resources.UnloadAsset(Asset);
        }

        ResMgr.Instance.SharedLoadedReses.Remove(this);
        Asset = null;
    }
}

ResLoader

public class ResLoader
{
    private List<Res> mResRecord = new List<Res>();

    public T LoadSync<T>(string assetName) where T : UnityEngine.Object
    {
        var res = GetOrCreateRes(assetName);
        if (res != null)
        {
            return res.Asset as T;
        }
        //真正加载资源
        res = CreateRes(assetName);
        res.LoadSync();
        return res.Asset as T;
    }

    public void LoadAsync<T>(string assetName, Action<T> onLoaded) where T : UnityEngine.Object
    {
        //查询当前的资源记录
        var res = GetOrCreateRes(assetName);
        if (res != null)
        {
            onLoaded(res.Asset as T);
            return;
        }

        //真正加载资源
        res = CreateRes(assetName);
        res.LoadAsync(loadedRes =>
        {
            onLoaded(loadedRes.Asset as T);
        });
    }

    public void ReleaseAll()
    {
        mResRecord.ForEach(loadedAsset => loadedAsset.Release());
        mResRecord.Clear();
    }

    #region 内部方法
    private Res GetResFromRecord(string assetName)
    {
        return mResRecord.Find(loadedAsset => loadedAsset.Name == assetName);
    }
    private Res GetFromResMgr(string assetName)
    {
        return ResMgr.Instance.SharedLoadedReses.Find(loadedAsset => loadedAsset.Name == assetName);
    }
    private void AddRes2Record(Res resFromResMgr)
    {
        mResRecord.Add(resFromResMgr);
        resFromResMgr.Retain();
    }
    private Res CreateRes(string assetName)
    {
        var res = new Res(assetName);
        ResMgr.Instance.SharedLoadedReses.Add(res);
        AddRes2Record(res);
        return res;
    }
    private Res GetOrCreateRes(string assetName)
    {
        //查询当前的资源记录
        var res = GetResFromRecord(assetName);
        if (res != null)
        {
            return res;
        }
        //查询全局资源池
        res = GetFromResMgr(assetName);
        if (res != null)
        {
            AddRes2Record(res);
            return res;
        }
        return res;
    }
    #endregion
}

完结

赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!

好了,我是向宇https://xiangyu.blog.csdn.net

一位在小公司默默奋斗的开发者,闲暇之余,边学习边记录分享,站在巨人的肩膀上,通过学习前辈们的经验总是会给我很多帮助和启发!如果你遇到任何问题,也欢迎你评论私信或者加群找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

向宇it

创作不易,感谢你的鼓励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值