6.资源管理模块

6.资源管理模块


6.1 知识点

资源管理模块的主要作用

在Unity开发中,资源管理是不可或缺的核心环节。不同类型的资源需要不同的加载策略,而原生的Unity资源加载方式往往存在各种问题。

核心问题:

  • Resources同步加载会卡顿:每次加载都会阻塞主线程
  • 频繁调用异步加载浪费性能:每次都会开启新的协程
  • 资源管理混乱:无法判断资源是否被使用,难以释放
  • 平台差异:不同平台资源加载方式不同
  • 代码冗余:资源加载逻辑分散在各个地方

解决方案:
资源管理模块通过统一管理缓存优化引用计数等机制,为Unity游戏开发提供高效、灵活的资源管理解决方案。

主要功能:

  1. Resources资源管理:封装同步和异步加载,提供缓存和引用计数
  2. Editor资源管理:编辑器模式下快速加载资源,方便开发测试
  3. AssetBundle管理:支持同步和异步加载,自动处理依赖包
  4. UnityWebRequest管理:支持网络资源和本地文件加载
  5. 统一接口:提供统一的资源管理接口,简化使用



资源管理模块的基本原理

设计理念:

  • 统一管理:通过管理器统一管理所有资源操作
  • 缓存机制:避免重复加载,提高性能
  • 引用计数:通过引用计数判断资源是否被使用
  • 异步优化:通过协程实现真正的异步加载
  • 平台适配:支持不同平台和不同资源加载方式

工作流程:

  1. 资源加载:调用管理器加载资源
  2. 缓存检查:检查资源是否已加载
  3. 加载策略:根据资源状态选择加载方式
  4. 引用管理:通过引用计数管理资源生命周期
  5. 资源释放:引用计数为0时释放资源

Resources资源管理模块基础实现

基础实现

重要提示:
Unity 官方明确建议:不要将大量资源放在 Resources 文件夹中。Resources 会让内存管理变难、启动/构建变慢。建议仅作少量通用或开发期兜底资源,主流程走 AB/Addressables/YooAsset 等方案。

Resources资源管理的四个步骤:

  1. 创建ResMgr继承单例模式基类:继承Singleton<T>,确保全局唯一
  2. 封装同步加载方法:封装Resources.Load<T>,简化调用
  3. 封装异步加载方法:封装Resources.LoadAsync<T>,配合协程实现异步加载
  4. 封装资源卸载方法:封装Resources.UnloadAssetUnloadUnusedAssets

基础源码实现:ResMgr.cs

using System.Collections;
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// Resources资源管理模块管理器(基础版本)
/// </summary>
public class ResMgr : Singleton<ResMgr>
{
    private ResMgr() { }

    /// <summary>
    /// 同步加载Resources下资源的方法
    /// </summary>
    /// <typeparam name="T">资源类型</typeparam>
    /// <param name="path">资源路径</param>
    /// <returns>加载的资源</returns>
    public T Load<T>(string path) where T : UnityEngine.Object
    {
        return Resources.Load<T>(path);
    }

    /// <summary>
    /// 异步加载资源的方法
    /// </summary>
    /// <typeparam name="T">资源类型</typeparam>
    /// <param name="path">资源路径(Resources下的)</param>
    /// <param name="callBack">加载结束后的回调函数</param>
    public void LoadAsync<T>(string path, UnityAction<T> callBack) where T : UnityEngine.Object
    {
        // 通过协同程序去异步加载资源
        MonoMgr.Instance.StartCoroutine(ReallyLoadAsync<T>(path, callBack));
    }

    /// <summary>
    /// 真正的异步加载协程
    /// </summary>
    private IEnumerator ReallyLoadAsync<T>(string path, UnityAction<T> callBack) where T : UnityEngine.Object
    {
        // 异步加载资源
        ResourceRequest rq = Resources.LoadAsync<T>(path);
        // 等待资源加载结束后才会继续执行yield return后面的代码
        yield return rq;
        // 资源加载结束,将资源传到外部的委托函数去进行使用
        callBack(rq.asset as T);
    }

    /// <summary>
    /// 指定卸载一个资源
    /// </summary>
    public void UnloadAsset(UnityEngine.Object assetToUnload)
    {
        Resources.UnloadAsset(assetToUnload);
    }

    /// <summary>
    /// 异步卸载对应没有使用的Resources相关的资源
    /// 【重要提示】这是昂贵操作,建议只在场景切换或明确的内存整理阶段调用
    /// </summary>
    public void UnloadUnusedAssets(UnityAction callBack)
    {
        MonoMgr.Instance.StartCoroutine(ReallyUnloadUnusedAssets(callBack));
    }

    private IEnumerator ReallyUnloadUnusedAssets(UnityAction callBack)
    {
        AsyncOperation ao = Resources.UnloadUnusedAssets();
        yield return ao;
        // 卸载完毕后通知外部
        callBack();
    }
}

测试代码:

using UnityEngine;

public class ResourceLoadingTest : MonoBehaviour
{
    void Start()
    {
        Debug.Log("Resources资源管理测试:开始");
        
        // 测试同步加载
        TestSynchronousLoad();
        
        // 测试异步加载
        TestAsynchronousLoad();
    }

    private void TestSynchronousLoad()
    {
        Debug.Log("测试同步加载资源");
        GameObject prefab = ResMgr.Instance.Load<GameObject>("Test");
        Debug.Log("同步加载完成:" + prefab.name);
        // 输出:同步加载完成:Test
    }

    private void TestAsynchronousLoad()
    {
        Debug.Log("测试异步加载资源");
        ResMgr.Instance.LoadAsync<GameObject>("Test", (obj) => {
            Debug.Log("异步加载完成:" + obj.name);
            // 输出:异步加载完成:Test
            Instantiate(obj);
        });
    }
}

异步加载问题

基础实现存在一个问题:每次异步加载都会开启新的协程,虽然Resources内部有缓存机制,但频繁开启协程仍会浪费性能。

问题分析:

  • 协程开销:每次异步管理都会开启新的协程,造成性能浪费
  • 重复加载:虽然Resources内部缓存了资源,但仍需检查内存,有性能消耗

解决方案:

  • 自建缓存:通过字典记录已加载的资源,避免重复加载
  • 委托管理:通过委托管理异步回调,避免重复协程
  • 加载状态:记录资源加载状态,区分”正在加载”和”已加载完成”

优化后的源码实现:ResMgr.cs (添加字典缓存优化)

优化点:

  1. 新增ResInfoBase基类和ResInfo泛型类:用于存储资源信息和加载状态
  2. 添加字典缓存机制resDic用于记录已加载或正在加载的资源
  3. 协程管理:记录异步加载的协程,支持中断和复用
  4. 委托复用:多个异步请求共享同一个加载协程
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// 资源信息基类
/// </summary>
public abstract class ResInfoBase { }

/// <summary>
/// 资源信息对象
/// </summary>
/// <typeparam name="T">资源类型</typeparam>
public class ResInfo<T> : ResInfoBase
{
    // 资源
    public T asset;
    // 主要用于异步加载结束后传递资源到外部的委托
    public UnityAction<T> callBack;
    // 用于存储异步加载时开启的协同程序
    public Coroutine coroutine;
}

    /// <summary>
    /// Resources资源管理模块管理器(添加字典缓存)
    /// </summary>
public class ResMgr : Singleton<ResMgr>
{
    // 【优化点1】添加字典缓存:用于存储加载过的资源或者加载中的资源
    private Dictionary<string, ResInfoBase> resDic = new Dictionary<string, ResInfoBase>();

    private ResMgr() { }

    // 【保持不变】同步加载方法保持简单
    public T Load<T>(string path) where T : UnityEngine.Object
    {
        return Resources.Load<T>(path);
    }

    /// <summary>
    /// 异步加载资源的方法
    /// 【优化点2】添加资源状态检查和重复请求处理
    /// </summary>
    public void LoadAsync<T>(string path, UnityAction<T> callBack) where T : UnityEngine.Object
    {
        // 资源的唯一ID,是通过路径名_资源类型拼接而成的
        string resName = path + "_" + typeof(T).Name;
        ResInfo<T> info;
        
        if (!resDic.ContainsKey(resName))
        {
            // 【新】第一次加载:创建资源信息,记录到字典
            info = new ResInfo<T>();
            resDic.Add(resName, info);
            // 【新】记录回调委托
            info.callBack += callBack;
            // 【新】启动协程并记录
            info.coroutine = MonoMgr.Instance.StartCoroutine(ReallyLoadAsync<T>(path));
        }
        else
        {
            // 【新】已经请求过:检查加载状态
            info = resDic[resName] as ResInfo<T>;
            if (info.asset == null)
                // 【新】正在加载中:添加到回调列表
                info.callBack += callBack;
            else
                // 【新】已加载完成:立即执行回调
                callBack?.Invoke(info.asset);
        }
    }

    /// <summary>
    /// 真正的异步加载协程
    /// 【优化点3】加载完成后更新资源状态并触发所有回调
    /// </summary>
    private IEnumerator ReallyLoadAsync<T>(string path) where T : UnityEngine.Object
    {
        ResourceRequest rq = Resources.LoadAsync<T>(path);
        yield return rq;

        string resName = path + "_" + typeof(T).Name;
        if (resDic.ContainsKey(resName))
        {
            ResInfo<T> resInfo = resDic[resName] as ResInfo<T>;
            // 【新】记录加载完成的资源
            resInfo.asset = rq.asset as T;
            // 【新】触发所有等待的回调(支持多个异步请求共享)
            resInfo.callBack?.Invoke(resInfo.asset);
            // 【新】清空引用,避免内存泄漏
            resInfo.callBack = null;
            resInfo.coroutine = null;
        }
    }

    /// <summary>
    /// 指定卸载一个资源
    /// </summary>
    public void UnloadAsset(UnityEngine.Object assetToUnload)
    {
        Resources.UnloadAsset(assetToUnload);
    }

    /// <summary>
    /// 异步卸载对应没有使用的Resources相关的资源
    /// 【重要提示】这是昂贵操作,建议只在场景切换或明确的内存整理阶段调用
    /// </summary>
    public void UnloadUnusedAssets(UnityAction callBack)
    {
        MonoMgr.Instance.StartCoroutine(ReallyUnloadUnusedAssets(callBack));
    }

    private IEnumerator ReallyUnloadUnusedAssets(UnityAction callBack)
    {
        AsyncOperation ao = Resources.UnloadUnusedAssets();
        yield return ao;
        // 卸载完毕后通知外部
        callBack();
    }
}

优化后存在的问题:

  • 在卸载资源时,我们并不知道是否还有地方使用着该资源
  • UnloadUnusedAssets是卸载没有使用的资源,我们无法判断是否使用

关于异步加载中的同步加载行为:
如果资源正在异步加载中(协程已启动),又立即调用同步加载会怎么样?

Resources的处理机制:
ResMgr的同步加载方法会检查资源是否正在异步加载(info.asset == null)。如果是,会:

  1. 停止异步加载:调用StopCoroutine停止正在执行的协程
  2. 立即同步加载:直接使用Resources.Load<T>加载资源
  3. 执行回调:触发所有等待异步加载完成的回调
  4. 清理引用:清除异步加载的回调和协程记录

这样可以避免资源重复加载,但会浪费已经开始的异步加载工作。

AB包的处理机制:
ABMgr中,如果依赖包或目标包正在异步加载(字典中值为null),同步加载会等待异步加载完成后再继续。这可以避免AssetBundle重复加载导致的错误。

加入引用计数

为了解决资源管理和卸载问题,我们需要引入引用计数机制。

引用计数是什么?
引用计数是一种内存管理技术,用于跟踪资源被引用的次数。我们通过一个整型变量来记录资源的使用次数。当有对象引用该资源时,计数器会增加;当对象不再引用该资源时,计数器会减少。

如何向ResMgr中加入引用计数功能:

  1. 为ResInfo类加入引用计数成员变量和方法:添加refCount变量和AddRefCount()SubRefCount()方法
  2. 使用资源时加:每次加载资源时,引用计数加1
  3. 不使用资源时减:每次卸载资源时,引用计数减1
  4. 处理异步回调问题:某一个异步不使用资源了应该移除回调函数的记录
  5. 修改移除资源函数逻辑:引用计数为0时才真正移除资源
  6. 考虑资源频繁移除问题:加入”马上移除”bool标签
  7. 修改移除不使用资源函数逻辑:释放时清除引用计数为0的记录

加入引用计数后的源码实现:ResInfo.csResMgr.cs

// ResInfoBase.cs
/// <summary>
/// 资源信息基类
/// </summary>
public abstract class ResInfoBase 
{
    // 引用计数
    public int refCount;
}

// ResInfo.cs
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// 资源信息对象
/// </summary>
/// <typeparam name="T">资源类型</typeparam>
public class ResInfo<T> : ResInfoBase
{
    // 资源
    public T asset;
    // 主要用于异步加载结束后传递资源到外部的委托
    public UnityAction<T> callBack;
    // 用于存储异步加载时开启的协同程序
    public Coroutine coroutine;
    // 决定引用计数为0时是否真正需要移除
    public bool isDel;

    public void AddRefCount()
    {
        ++refCount;
    }

    public void SubRefCount()
    {
        --refCount;
        if (refCount < 0)
            Debug.LogError("引用计数小于0了,请检查使用和卸载是否配对执行");
    }
}

加入引用计数后的测试代码:

using UnityEngine;

public class ReferenceCountingTest : MonoBehaviour
{
    void Start()
    {
        Debug.Log("引用计数测试:开始");
        
        // 第一次加载
        GameObject obj1 = ResMgr.Instance.Load<GameObject>("Test");
        Debug.Log("引用计数:" + ResMgr.Instance.GetRefCount<GameObject>("Test"));
        // 输出:引用计数:1

        // 第二次加载(应该复用)
        GameObject obj2 = ResMgr.Instance.Load<GameObject>("Test");
        Debug.Log("引用计数:" + ResMgr.Instance.GetRefCount<GameObject>("Test"));
        // 输出:引用计数:2

        // 卸载一次
        ResMgr.Instance.UnloadAsset<GameObject>("Test");
        Debug.Log("引用计数:" + ResMgr.Instance.GetRefCount<GameObject>("Test"));
        // 输出:引用计数:1

        // 卸载一次
        ResMgr.Instance.UnloadAsset<GameObject>("Test");
        Debug.Log("引用计数:" + ResMgr.Instance.GetRefCount<GameObject>("Test"));
        // 输出:引用计数:0(资源已被移除)
    }
}

注意事项:

  1. 使用资源时需要有增有减,当使用某个资源的对象移除时,一定要记得调用移除方法
  2. 如果觉得卸载资源的功能麻烦,也完全可以不使用卸载的相关方法,加载相关逻辑不会有任何影响,只需要再添加一个主动清空字典的方法即可

Android/iOS平台特殊处理:
在Android和iOS平台上,StreamingAssets 文件夹中的文件位于压缩包内,无法直接使用 AssetBundle.LoadFromFile 加载。建议的解决方案:

  1. 使用UnityWebRequest加载:改用 UnityWebRequestAssetBundle.GetAssetBundle(filePath)
  2. 首次运行拷贝:启动时将AB包从 StreamingAssets 拷贝到 persistentDataPath,然后从永久路径加载

Editor资源加载模块

Editor资源加载的主要作用



Editor资源管理基本原理

主要使用的API:

  1. AssetDatabase.LoadAssetAtPath:用于加载单个资源
  2. AssetDatabase.LoadAllAssetRepresentationsAtPath:用于加载图集资源中的内容(该API用于加载所有子资源)

注意:
这两个API加载资源的路径都是从Assets/...开始的。

Editor资源管理实现

创建用于放置资源的文件夹:
我们可以创建Editor文件夹,以后我们最终会打包为AB包的资源都放置在此处。

实现Editor资源管理器:
封装API,主要提供管理单个资源以及管理图集资源的API。

基础源码实现:EditorResMgr.cs

using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

/// <summary>
/// 编辑器资源管理器
/// 注意:只有在开发时能使用该管理器加载资源,用于开发功能
/// 发布后是无法使用该管理器的,因为它需要用到编辑器相关功能
/// </summary>
public class EditorResMgr : Singleton<EditorResMgr>
{
    // 用于放置需要打包进AB包中的资源路径
    private string rootPath = "Assets/Editor/ArtRes/";

    private EditorResMgr() { }

    // 1. 加载单个资源的
    public T LoadEditorRes<T>(string path) where T : Object
    {
#if UNITY_EDITOR
        string suffixName = "";
        // 预设体、纹理(图片)、材质球、音效等等
        if (typeof(T) == typeof(GameObject))
            suffixName = ".prefab";
        else if (typeof(T) == typeof(Material))
            suffixName = ".mat";
        else if (typeof(T) == typeof(Texture))
            suffixName = ".png";
        else if (typeof(T) == typeof(AudioClip))
            suffixName = ".mp3";
        T res = AssetDatabase.LoadAssetAtPath<T>(rootPath + path + suffixName);
        return res;
#else
        return null;
#endif
    }

    // 2. 加载图集相关资源的
    public Sprite LoadSprite(string path, string spriteName)
    {
#if UNITY_EDITOR
        // 加载图集中的所有子资源
        Object[] sprites = AssetDatabase.LoadAllAssetRepresentationsAtPath(rootPath + path);
        // 遍历所有子资源,得到同名图片返回
        foreach (var item in sprites)
        {
            if (spriteName == item.name)
                return item as Sprite;
        }
        return null;
#else
        return null;
#endif
    }

    // 加载图集文件中的所有子图片并返回给外部
    public Dictionary<string, Sprite> LoadSprites(string path)
    {
#if UNITY_EDITOR
        Dictionary<string, Sprite> spriteDic = new Dictionary<string, Sprite>();
        Object[] sprites = AssetDatabase.LoadAllAssetRepresentationsAtPath(rootPath + path);
        foreach (var item in sprites)
        {
            spriteDic.Add(item.name, item as Sprite);
        }
        return spriteDic;
#else
        return null;
#endif
    }
}

测试代码:

using UnityEngine;

public class EditorResourceLoadingTest : MonoBehaviour
{
    void Start()
    {
        Debug.Log("Editor资源管理测试:开始");
        
        // 测试加载Prefab
        GameObject prefab = EditorResMgr.Instance.LoadEditorRes<GameObject>("TestPrefab");
        Debug.Log("加载Prefab:" + prefab.name);
        // 输出:加载Prefab:TestPrefab
        
        // 测试加载图集中的单个精灵
        Sprite sprite = EditorResMgr.Instance.LoadSprite("TestAtlas", "TestSprite");
        Debug.Log("加载精灵:" + sprite.name);
        // 输出:加载精灵:TestSprite
        
        // 测试加载图集中的所有精灵
        Dictionary<string, Sprite> sprites = EditorResMgr.Instance.LoadSprites("TestAtlas");
        Debug.Log("加载精灵数量:" + sprites.Count);
        // 输出:加载精灵数量:10
    }
}

AssetBundle资源管理模块

AssetBundle知识回顾

关于AssetBundle的预备知识:
AssetBundle相关知识主要是之前写了AB包基本打包以及管理器,上传下载等。

AssetBundle资源管理模块我们主要做什么:
目前已有的ABMgr中的异步管理部分,只是从AB包中异步管理资源,并没有真正地去异步管理AB包本身。我们要做的事情,就是对ABMgr进行优化,让其中的异步管理真正意义上的实现异步,就是将AB包本身的管理也变为异步的。

ABMgr的基础实现:ABMgr.cs

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

public class ABMgr : SingletonAutoMono<ABMgr>
{
    // 主包
    private AssetBundle mainAB = null;
    // 主包依赖获取配置文件
    private AssetBundleManifest manifest = null;
    // 选择存储AB包的容器
    // AB包不能够重复加载,否则会报错
    private Dictionary<string, AssetBundle> abDic = new Dictionary<string, AssetBundle>();

    /// <summary>
    /// 获取AB包加载路径
    /// </summary>
    private string PathUrl
    {
        get
        {
            return Application.streamingAssetsPath + "/";
        }
    }

    /// <summary>
    /// 主包名,根据平台不同包名不同
    /// </summary>
    private string MainName
    {
        get
        {
#if UNITY_IOS
            return "IOS";
#elif UNITY_ANDROID
            return "Android";
#else
            return "PC";
#endif
        }
    }

    /// <summary>
    /// 加载主包和配置文件
    /// </summary>
    private void LoadMainAB()
    {
        if (mainAB == null)
        {
            mainAB = AssetBundle.LoadFromFile(PathUrl + MainName);
            manifest = mainAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
        }
    }

    /// <summary>
    /// 加载指定包的依赖包
    /// </summary>
    private void LoadDependencies(string abName)
    {
        // 加载主包
        LoadMainAB();
        // 获取依赖包
        string[] strs = manifest.GetAllDependencies(abName);
        for (int i = 0; i < strs.Length; i++)
        {
            if (!abDic.ContainsKey(strs[i]))
            {
                AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + strs[i]);
                abDic.Add(strs[i], ab);
            }
        }
    }

    /// <summary>
    /// 泛型异步加载资源
    /// </summary>
    public void LoadResAsync<T>(string abName, string resName, UnityAction<T> callBack) where T : Object
    {
        StartCoroutine(ReallyLoadResAsync<T>(abName, resName, callBack));
    }

    // 真正的协程函数
    private IEnumerator ReallyLoadResAsync<T>(string abName, string resName, UnityAction<T> callBack) where T : Object
    {
        // 加载依赖包
        LoadDependencies(abName);
        // 加载目标包
        if (!abDic.ContainsKey(abName))
        {
            AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
            abDic.Add(abName, ab);
        }
        // 异步加载包中资源
        AssetBundleRequest abq = abDic[abName].LoadAssetAsync<T>(resName);
        yield return abq;

        if (abq.asset is GameObject)
            callBack(Instantiate(abq.asset) as T);
        else
            callBack(abq.asset as T);
    }

    /// <summary>
    /// 卸载AB包的方法
    /// </summary>
    public void UnLoadAB(string name)
    {
        if (abDic.ContainsKey(name))
        {
            abDic[name].Unload(false);
            abDic.Remove(name);
        }
    }

    /// <summary>
    /// 清空AB包的方法
    /// </summary>
    public void ClearAB()
    {
        AssetBundle.UnloadAllAssetBundles(false);
        abDic.Clear();
        // 卸载主包
        mainAB = null;
    }
}

存在的问题:

  1. AB包本身的加载是同步的,只有从AB包中加载资源才是异步的,不够彻底
  2. Android平台StreamingAssets加载问题LoadFromFile(StreamingAssets/...) 在 Android/iOS 会失败,因为 StreamingAssets 在压缩包里。需要使用 UnityWebRequest 加载,或首次运行时拷贝到 persistentDataPath

AssetBundle潜在问题考虑

异步加载AB包可能带来的问题:
如果我们想要将ABMgr中的异步加载方法改为真正意义上的异步(不仅从AB包中加载资源是异步的,还需要在加载AB包时也采用异步),那么我们就需要考虑一个问题:如果当我们正在异步加载AB包时,又进行了一次同步加载AB包,是否会报错(因为AB包不允许重复加载)?

实验验证:
通过验证我们得出结论:异步加载AB包时,再同步加载AB包,会因重复加载而报错。因此我们在修改逻辑时应该考虑到这个问题。

ABMgr异步加载修改

ABMgr异步加载修改主要目标:
将ABMgr中的异步加载方法彻底异步化,让其中的依赖包加载、资源包加载、资源加载都变为异步加载。

注意:主包可以保留同步加载。

需要考虑的主要问题:

  1. 某个AB包当正在异步加载时又进行重复加载:遇到这种情况时,我们需要避免重复加载报错。因此我们不应再次加载,而是等待之前的异步加载结束后直接使用。
  2. 正在加载某个AB包时卸载AB包:如果正在加载中,不允许卸载。
  3. 正在加载时清空AB包:停止所有协同程序,再清理AB包。
  4. 异步加载失败的处理LoadFromFileAsync 失败时会返回 assetBundle == null,如果直接跳过会进入永久等待。需要添加失败分支处理。

优化后的源码实现:ABMgr.cs

优化点:

  1. 字典值支持nullnull表示正在异步加载中,非null表示已加载完成
  2. 等待机制:通过协程轮询等待正在加载的AB包
  3. 防重复加载:避免AssetBundle重复加载导致的错误
  4. Android/WebGL平台支持:使用UnityWebRequest在移动平台加载StreamingAssets
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
#if UNITY_ANDROID || UNITY_WEBGL
using UnityEngine.Networking;
#endif

public class ABMgr : SingletonAutoMono<ABMgr>
{
    private AssetBundle mainAB = null;
    private AssetBundleManifest manifest = null;
    // 【优化点1】字典值可以是null,表示正在异步加载中
    private Dictionary<string, AssetBundle> abDic = new Dictionary<string, AssetBundle>();

    /// <summary>
    /// 获取AB包加载路径
    /// </summary>
    private string PathUrl => Application.streamingAssetsPath + "/";

    /// <summary>
    /// 主包名,根据平台不同包名不同
    /// </summary>
    private string MainName
    {
        get
        {
#if UNITY_IOS
            return "IOS";
#elif UNITY_ANDROID
            return "Android";
#else
            return "PC";
#endif
        }
    }

    /// <summary>
    /// 加载主包和配置文件
    /// </summary>
    private void LoadMainAB()
    {
        if (mainAB == null)
        {
            mainAB = AssetBundle.LoadFromFile(PathUrl + MainName);
            manifest = mainAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
        }
    }

    /// <summary>
    /// 泛型异步加载资源
    /// </summary>
    public void LoadResAsync<T>(string abName, string resName, UnityAction<T> callBack) where T : Object
    {
        StartCoroutine(ReallyLoadResAsync<T>(abName, resName, callBack));
    }

    /// <summary>
    /// 确保主包已加载(协程版本)
    /// </summary>
    private IEnumerator EnsureMainAB()
    {
        if (mainAB != null) yield break;
        
#if UNITY_ANDROID || UNITY_WEBGL
        // Android/WebGL平台:使用UnityWebRequest加载主包
        string url = PathUrl + MainName;
        using (UnityWebRequest req = UnityWebRequestAssetBundle.GetAssetBundle(url))
        {
            yield return req.SendWebRequest();
            if (req.result != UnityWebRequest.Result.Success)
            {
                Debug.LogError($"主包加载失败: {req.error}");
                yield break;
            }
            mainAB = DownloadHandlerAssetBundle.GetContent(req);
        }
#else
        // 其他平台:使用LoadFromFile(同步但效率高)
        mainAB = AssetBundle.LoadFromFile(PathUrl + MainName);
#endif
        
        manifest = mainAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
    }

    /// <summary>
    /// 真正的异步加载协程
    /// </summary>
    private IEnumerator ReallyLoadResAsync<T>(string abName, string resName, UnityAction<T> callBack) where T : Object
    {
        // 【重要修复】确保主包已加载(支持Android/WebGL)
        yield return EnsureMainAB();
        
        // 获取依赖包
        string[] strs = manifest.GetAllDependencies(abName);
        for (int i = 0; i < strs.Length; i++)
        {
            if (!abDic.ContainsKey(strs[i]))
            {
            // 【优化点2】先记录null,表示开始异步加载
            abDic.Add(strs[i], null);
            
#if UNITY_ANDROID || UNITY_WEBGL
            // 【重要修复】Android/WebGL平台:使用UnityWebRequest加载StreamingAssets中的AB包
            string url = PathUrl + strs[i];
            using (UnityWebRequest req = UnityWebRequestAssetBundle.GetAssetBundle(url))
            {
                yield return req.SendWebRequest();
                if (req.result != UnityWebRequest.Result.Success)
                {
                    Debug.LogError($"AB加载失败:{strs[i]}, error: {req.error}");
                    abDic.Remove(strs[i]);
                    yield break;
                }
                abDic[strs[i]] = DownloadHandlerAssetBundle.GetContent(req);
            }
#else
            AssetBundleCreateRequest req = AssetBundle.LoadFromFileAsync(PathUrl + strs[i]);
            yield return req;
            // 【重要修复】检查加载是否成功,失败则移除字典并跳过
            if (req.assetBundle == null)
            {
                Debug.LogError($"AssetBundle加载失败:{strs[i]}");
                abDic.Remove(strs[i]);
                yield break; // 加载失败,退出协程
            }
            // 【优化点2】加载完成后更新字典值
            abDic[strs[i]] = req.assetBundle;
#endif
            }
            else
            {
                // 【优化点3】正在加载中:轮询等待直至加载完成
                while (abDic[strs[i]] == null)
                {
                    yield return 0;  // 每帧检查一次
                }
            }
        }
        
        // 加载目标包
        if (!abDic.ContainsKey(abName))
        {
            abDic.Add(abName, null);
            
#if UNITY_ANDROID || UNITY_WEBGL
            // 【重要修复】Android/WebGL平台:使用UnityWebRequest加载
            string url = PathUrl + abName;
            using (UnityWebRequest req = UnityWebRequestAssetBundle.GetAssetBundle(url))
            {
                yield return req.SendWebRequest();
                if (req.result != UnityWebRequest.Result.Success)
                {
                    Debug.LogError($"AB加载失败:{abName}, error: {req.error}");
                    abDic.Remove(abName);
                    yield break;
                }
                abDic[abName] = DownloadHandlerAssetBundle.GetContent(req);
            }
#else
            AssetBundleCreateRequest req = AssetBundle.LoadFromFileAsync(PathUrl + abName);
            yield return req;
            // 【重要修复】检查加载是否成功
            if (req.assetBundle == null)
            {
                Debug.LogError($"AssetBundle加载失败:{abName}");
                abDic.Remove(abName);
                yield break;
            }
            abDic[abName] = req.assetBundle;
#endif
        }
        else
        {
            // 【优化点3】同样等待机制
            while (abDic[abName] == null)
            {
                yield return 0;
            }
        }

        // 异步加载包中资源
        AssetBundleRequest abq = abDic[abName].LoadAssetAsync<T>(resName);
        yield return abq;

        callBack(abq.asset as T);
    }

    /// <summary>
    /// 卸载AB包的方法
    /// </summary>
    public void UnLoadAB(string name, UnityAction<bool> callBackResult)
    {
        if (abDic.ContainsKey(name))
        {
            if (abDic[name] == null)
            {
                // 代表正在异步加载,没有卸载成功
                callBackResult(false);
                return;
            }
            abDic[name].Unload(false);
            abDic.Remove(name);
            // 卸载成功
            callBackResult(true);
        }
    }

    /// <summary>
    /// 清空AB包的方法
    /// </summary>
    public void ClearAB()
    {
        // 由于AB包都是异步加载了,因此在清理之前停止协同程序
        StopAllCoroutines();
        AssetBundle.UnloadAllAssetBundles(false);
        abDic.Clear();
        // 卸载主包
        mainAB = null;
    }
}

ABMgr同步加载修改

修改同步加载主要面对的问题:
在进行同步加载时如果遇到有AB包正在被异步加载应该如何解决?

通过我们之前学习的潜在问题考虑得知,在进行异步加载时再重复加载相同AB包是会报错的。即使是同步加载,我们也必须等待异步加载结束,再进行下一步。

修改ABMgr中的同步加载相关逻辑:

  1. 注释之前的同步加载代码
  2. 在异步加载的基础上进行修改

优化后的源码实现:ABMgr.cs

关键优化:添加isSync参数,支持同步和异步两种加载方式

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

public class ABMgr : SingletonAutoMono<ABMgr>
{
    private AssetBundle mainAB = null;
    private AssetBundleManifest manifest = null;
    private Dictionary<string, AssetBundle> abDic = new Dictionary<string, AssetBundle>();

    private string PathUrl => Application.streamingAssetsPath + "/";

    private string MainName
    {
        get
        {
#if UNITY_IOS
            return "IOS";
#elif UNITY_ANDROID
            return "Android";
#else
            return "PC";
#endif
        }
    }

    /// <summary>
    /// 加载主包和配置文件
    /// </summary>
    private void LoadMainAB()
    {
        if (mainAB == null)
        {
            mainAB = AssetBundle.LoadFromFile(PathUrl + MainName);
            manifest = mainAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
        }
    }

    /// <summary>
    /// 泛型异步加载资源(支持同步开关)
    /// </summary>
    /// <param name="isSync">是否同步加载AB包</param>
    public void LoadResAsync<T>(string abName, string resName, UnityAction<T> callBack, bool isSync = false) where T : Object
    {
        StartCoroutine(ReallyLoadResAsync<T>(abName, resName, callBack, isSync));
    }

    /// <summary>
    /// 确保主包已加载(协程版本,支持Android/WebGL)
    /// </summary>
    private IEnumerator EnsureMainAB()
    {
        if (mainAB != null) yield break;
        
#if UNITY_ANDROID || UNITY_WEBGL
        // Android/WebGL平台:使用UnityWebRequest加载主包
        string url = PathUrl + MainName;
        using (UnityWebRequest req = UnityWebRequestAssetBundle.GetAssetBundle(url))
        {
            yield return req.SendWebRequest();
            if (req.result != UnityWebRequest.Result.Success)
            {
                Debug.LogError($"主包加载失败: {req.error}");
                yield break;
            }
            mainAB = DownloadHandlerAssetBundle.GetContent(req);
        }
#else
        // 其他平台:使用LoadFromFile(同步但效率高)
        mainAB = AssetBundle.LoadFromFile(PathUrl + MainName);
#endif
        
        manifest = mainAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
    }

    /// <summary>
    /// 真正的异步加载协程(支持同步开关)
    /// </summary>
    private IEnumerator ReallyLoadResAsync<T>(string abName, string resName, UnityAction<T> callBack, bool isSync) where T : Object
    {
        // 【重要修复】确保主包已加载(支持Android/WebGL)
        yield return EnsureMainAB();
        
        // 获取依赖包
        string[] strs = manifest.GetAllDependencies(abName);
        for (int i = 0; i < strs.Length; i++)
        {
            if (!abDic.ContainsKey(strs[i]))
            {
                // 【新增】根据isSync参数选择同步或异步加载
                // 【重要修复】Android/WebGL平台统一使用UWR,isSync仅影响资源加载方式
#if UNITY_ANDROID || UNITY_WEBGL
                // Android/WebGL:即使是"同步"也要用UWR加载AB包
                abDic.Add(strs[i], null);
                string url = PathUrl + strs[i];
                using (UnityWebRequest req = UnityWebRequestAssetBundle.GetAssetBundle(url))
                {
                    yield return req.SendWebRequest();
                    if (req.result != UnityWebRequest.Result.Success)
                    {
                        Debug.LogError($"AB加载失败:{strs[i]}, error: {req.error}");
                        abDic.Remove(strs[i]);
                        yield break;
                    }
                    abDic[strs[i]] = DownloadHandlerAssetBundle.GetContent(req);
                }
#else
                // 其他平台:isSync决定是否等待协程加载AB包
                if (isSync)
                {
                    AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + strs[i]);
                    abDic.Add(strs[i], ab);
                }
                else
                {
                    abDic.Add(strs[i], null);
                    AssetBundleCreateRequest req = AssetBundle.LoadFromFileAsync(PathUrl + strs[i]);
                    yield return req;
                    if (req.assetBundle == null)
                    {
                        Debug.LogError($"AssetBundle加载失败:{strs[i]}");
                        abDic.Remove(strs[i]);
                        yield break;
                    }
                    abDic[strs[i]] = req.assetBundle;
                }
#endif
            }
            else
            {
                // 【保持不变】等待机制
                while (abDic[strs[i]] == null)
                {
                    yield return 0;
                }
            }
        }
        
        // 加载目标包
        if (!abDic.ContainsKey(abName))
        {
#if UNITY_ANDROID || UNITY_WEBGL
            // Android/WebGL:统一使用UWR
            abDic.Add(abName, null);
            string url = PathUrl + abName;
            using (UnityWebRequest req = UnityWebRequestAssetBundle.GetAssetBundle(url))
            {
                yield return req.SendWebRequest();
                if (req.result != UnityWebRequest.Result.Success)
                {
                    Debug.LogError($"AB加载失败:{abName}, error: {req.error}");
                    abDic.Remove(abName);
                    yield break;
                }
                abDic[abName] = DownloadHandlerAssetBundle.GetContent(req);
            }
#else
            // 其他平台:根据isSync决定
            if (isSync)
            {
                AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
                abDic.Add(abName, ab);
            }
            else
            {
                abDic.Add(abName, null);
                AssetBundleCreateRequest req = AssetBundle.LoadFromFileAsync(PathUrl + abName);
                yield return req;
                if (req.assetBundle == null)
                {
                    Debug.LogError($"AssetBundle加载失败:{abName}");
                    abDic.Remove(abName);
                    yield break;
                }
                abDic[abName] = req.assetBundle;
            }
#endif
        }
        else
        {
            while (abDic[abName] == null)
            {
                yield return 0;
            }
        }

        // 【新增】根据isSync参数选择同步或异步加载资源
        if (isSync)
        {
            T res = abDic[abName].LoadAsset<T>(resName);
            callBack(res);
        }
        else
        {
            AssetBundleRequest abq = abDic[abName].LoadAssetAsync<T>(resName);
            yield return abq;
            callBack(abq.asset as T);
        }
    }

    /// <summary>
    /// 卸载AB包的方法
    /// </summary>
    public void UnLoadAB(string name, UnityAction<bool> callBackResult)
    {
        if (abDic.ContainsKey(name))
        {
            if (abDic[name] == null)
            {
                callBackResult(false);
                return;
            }
            abDic[name].Unload(false);
            abDic.Remove(name);
            callBackResult(true);
        }
    }

    /// <summary>
    /// 清空AB包的方法
    /// </summary>
    public void ClearAB()
    {
        StopAllCoroutines();
        AssetBundle.UnloadAllAssetBundles(false);
        abDic.Clear();
        mainAB = null;
    }
}

测试代码:

using UnityEngine;

public class ABLoadingTest : MonoBehaviour
{
    void Start()
    {
        Debug.Log("AB包管理测试:开始");
        
        // 测试异步加载
        ABMgr.Instance.LoadResAsync<GameObject>("TestAB", "TestPrefab", (obj) => {
            Debug.Log("异步加载完成:" + obj.name);
            // 输出:异步加载完成:TestPrefab
        });
        
        // 测试同步加载
        ABMgr.Instance.LoadResAsync<GameObject>("TestAB", "TestPrefab", (obj) => {
            Debug.Log("同步加载完成:" + obj.name);
            // 输出:同步加载完成:TestPrefab
        }, true);
    }
}

UnityWebRequest资源管理模块

UnityWebRequest知识回顾

关于知识回顾选修内容:
UnityWebRequest相关知识是我们在Unity网络开发基础中进行讲解的。选修内容包括WWW和UnityWebRequest获取数据的常用操作。WWW在新版本中已经是过时的方案,但是还能够使用,可以作为了解。UnityWebRequest是需要着重回顾的。

重要内容回顾:

  • WWW和UnityWebRequest都是Unity提供给我们的简单的访问网页的类
  • 我们可以利用它们来进行数据的上传和下载
  • 它们都支持file://本地文件传输协议,我们可以利用该协议来异步加载本地文件
  • 特别是在Android平台时,我们无法直接通过C#中的File公共类加载StreamingAssets文件夹中的内容,我们需要使用WWW或UnityWebRequest类来加载

UnityWebRequest资源管理模块我们主要做什么:
我们主要封装UnityWebRequest当中的获取文本或二进制数据方法、获取纹理数据方法、获取AB包数据方法,制作UWQResMgr,让外部使用UnityWebRequest管理资源时更加方便。

UnityWebRequest具体实现

主要用到的知识点:

  1. UnityWebRequest相关知识(Unity网络开发基础)
  2. 泛型相关知识(C#四部曲C#进阶中)
  3. 协同程序相关知识(Unity四部曲之Unity基础中)
  4. 反射中的Type相关知识(C#四部曲C#进阶中)
  5. 委托相关知识(C#四部曲C#进阶中)

封装UnityWebRequest中的方法:
主要将加载文本、加载文本字节数组、加载纹理、加载AB包的几个方法合并在一个泛型异步方法中,方便外部使用。

源码实现:UWQResMgr.cs

核心特性:泛型加载 + 类型判断 + 统一接口

using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Networking;

public class UWQResMgr : SingletonAutoMono<UWQResMgr>
{
    public void LoadRes<T>(string path, UnityAction<T> callBack, UnityAction failCallBack) where T : class
    {
        StartCoroutine(ReallyLoadRes<T>(path, callBack, failCallBack));
    }

    private IEnumerator ReallyLoadRes<T>(string path, UnityAction<T> callBack, UnityAction failCallBack) where T : class
    {
        Type type = typeof(T);
        UnityWebRequest req = null;
        
        // 【核心1】根据类型选择不同的UnityWebRequest加载方式
        if (type == typeof(string) || type == typeof(byte[]))
        {
            req = UnityWebRequest.Get(path);
        }
        else if (type == typeof(Texture2D) || type == typeof(Texture))
        {
            // 【重要修复】Texture2D 是 Texture 的子类,UnityWebRequestTexture.GetTexture 返回 Texture2D
            req = UnityWebRequestTexture.GetTexture(path);
        }
        else if (type == typeof(AssetBundle))
        {
            req = UnityWebRequestAssetBundle.GetAssetBundle(path);
        }
        else
        {
            failCallBack?.Invoke();
            yield break;
        }

        yield return req.SendWebRequest();
        
        // 【核心2】根据类型使用不同的获取方法
        if (req.result == UnityWebRequest.Result.Success)
        {
            if (type == typeof(string))
            {
                callBack?.Invoke(req.downloadHandler.text as T);
            }
            else if (type == typeof(byte[]))
            {
                callBack?.Invoke(req.downloadHandler.data as T);
            }
            else if (type == typeof(Texture2D) || type == typeof(Texture))
            {
                // 【重要修复】Texture 实际上返回的是 Texture2D
                callBack?.Invoke(DownloadHandlerTexture.GetContent(req) as T);
            }
            else if (type == typeof(AssetBundle))
            {
                callBack?.Invoke(DownloadHandlerAssetBundle.GetContent(req) as T);
            }
        }
        else
        {
            failCallBack?.Invoke();
        }
        
        // 【核心3】及时释放请求对象
        req.Dispose();
    }
}

测试代码:

using UnityEngine;

public class UWQLoadingTest : MonoBehaviour
{
    void Start()
    {
        Debug.Log("UnityWebRequest管理测试:开始");
        
        // 测试加载本地文件
        TestLoadLocalFile();
        
        // 测试加载网络文件
        TestLoadNetworkFile();
    }

    private void TestLoadLocalFile()
    {
        // 使用file协议加载本地文件
        string localPath = "file://" + Application.streamingAssetsPath + "/test.txt";
        UWQResMgr.Instance.LoadRes<string>(localPath, 
            (text) => {
                Debug.Log("加载文本成功:" + text);
                // 输出:加载文本成功:(文件内容)
            },
            () => {
                Debug.Log("加载文本失败");
            }
        );
    }

    private void TestLoadNetworkFile()
    {
        // 加载网络资源
        string url = "http://example.com/image.png";
        UWQResMgr.Instance.LoadRes<Texture>(url,
            (texture) => {
                Debug.Log("加载纹理成功:" + texture.width + "x" + texture.height);
                // 输出:加载纹理成功:1920x1080
            },
            () => {
                Debug.Log("加载纹理失败");
            }
        );
    }
}

EditorResMgr和ABMgr整合

EditorResMgr的主要目的是方便资源管理,避免频繁的打AB包进行测试。它主要是为AB包预备资源服务的,因此我们可以将它和ABMgr整合在一起。
类似YooAsset的编辑器模式和其他模式。编辑器模式下不打AB包也可以正常加载。

整合后的源码实现:ABResMgr.cs

using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// AssetBundle资源管理器
/// 提供统一的资源加载接口,在编辑器和运行时之间切换
/// 编辑器模式下可选择使用EditorResMgr或ABMgr
/// </summary>
public class ABResMgr : Singleton<ABResMgr>
{
    /// <summary>
    /// 是否开启调试模式
    /// true:编辑器模式下使用EditorResMgr
    /// false:使用ABMgr
    /// </summary>
    private bool isDebug = false;

    private ABResMgr() { }

    /// <summary>
    /// 异步加载资源
    /// </summary>
    /// <typeparam name="T">资源类型</typeparam>
    /// <param name="abName">AB包名称</param>
    /// <param name="resName">资源名称</param>
    /// <param name="callBack">加载完成回调</param>
    /// <param name="isSync">是否同步加载</param>
    public void LoadResAsync<T>(string abName, string resName, UnityAction<T> callBack, bool isSync = false) where T : Object
    {
#if UNITY_EDITOR
        if (isDebug)
        {
            // 编辑器模式下使用EditorResMgr加载资源
            // 我们自定义了一个AB包中资源的管理方式,对应文件夹名就是包名
            T res = EditorResMgr.Instance.LoadEditorRes<T>($"{abName}/{resName}");
            callBack?.Invoke(res);
        }
        else
        {
            // 使用ABMgr加载资源
            ABMgr.Instance.LoadResAsync<T>(abName, resName, callBack, isSync);
        }
#else
        // 非编辑器模式下直接使用ABMgr
        ABMgr.Instance.LoadResAsync<T>(abName, resName, callBack, isSync);
#endif
    }
}

使用示例:

using UnityEngine;

public class ABResLoadingTest : MonoBehaviour
{
    void Start()
    {
        Debug.Log("AB资源管理测试:开始");
        
        // 使用ABResMgr管理资源(会自动根据是否编辑器和isDebug切换管理方式)
        ABResMgr.Instance.LoadResAsync<GameObject>("TestAB", "TestPrefab", (obj) => {
            Debug.Log("加载完成:" + obj.name);
            // 编辑器+调试模式:使用EditorResMgr(输出:管理完成:TestPrefab)
            // 其他情况:使用ABMgr(输出:管理完成:TestPrefab)
        });
    }
}

拓展与进阶

现在其实有了一个很好的资源管理工具叫YooAsset。他有分成各种模式,比如编辑器模式和联网模式这种。加载资源的时候可以传入Asset下的全路径,打包时也有各种配置。可以说是资源管理中非常好用的了。


6.2 知识点代码

ResMgr.cs(Resources资源管理模块管理器)

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

/// <summary>
/// Resources资源管理模块管理器
/// 提供同步和异步资源管理功能,支持引用计数管理
/// </summary>
public class ResMgr : Singleton<ResMgr>
{
    #region 字段

    /// <summary>
    /// 资源字典
    /// 用于存储加载过的资源或者加载中的资源
    /// </summary>
    private Dictionary<string, ResInfoBase> resDic = new Dictionary<string, ResInfoBase>();

    #endregion

    #region 构造函数

    private ResMgr()
    {
    }

    #endregion

    #region 同步加载

    /// <summary>
    /// 同步加载Resources下资源的方法
    /// 【引用计数优化】增加引用计数管理
    /// </summary>
    public T Load<T>(string path) where T : UnityEngine.Object
    {
        string resName = path + "_" + typeof(T).Name;
        ResInfo<T> info;

        if (!resDic.ContainsKey(resName))
        {
            T res = Resources.Load<T>(path);
            info = new ResInfo<T>();
            info.asset = res;
            info.AddRefCount(); // 【新增】引用计数+1
            resDic.Add(resName, info);
            return res;
        }
        else
        {
            info = resDic[resName] as ResInfo<T>;
            info.AddRefCount(); // 【新增】引用计数+1
            if (info.asset == null)
            {
                // 【新增】关键优化:同步加载时可以中断异步加载
                MonoMgr.Instance.StopCoroutine(info.coroutine);
                T res = Resources.Load<T>(path);
                info.asset = res;
                info.callBack?.Invoke(res); // 【新增】触发所有等待的回调
                info.callBack = null;
                info.coroutine = null;
                return res;
            }
            else
            {
                return info.asset;
            }
        }
    }

    #endregion

    #region 异步加载

    /// <summary>
    /// 异步加载资源的方法
    /// </summary>
    /// <typeparam name="T">资源类型</typeparam>
    /// <param name="path">资源路径(Resources下的)</param>
    /// <param name="callBack">加载结束后的回调函数,当异步加载资源结束后才会调用</param>
    public void LoadAsync<T>(string path, UnityAction<T> callBack) where T : UnityEngine.Object
    {
        // 资源的唯一ID,是通过路径名_资源类型拼接而成的
        string resName = path + "_" + typeof(T).Name;
        ResInfo<T> info;

        if (!resDic.ContainsKey(resName))
        {
            // 声明一个资源信息对象
            info = new ResInfo<T>();
            // 引用计数增加
            info.AddRefCount();
            // 将资源记录添加到字典中(资源还没有加载成功)
            resDic.Add(resName, info);
            // 记录传入的委托函数,一会儿加载完成了再使用
            info.callBack += callBack;
            // 开启协程去进行异步加载,并且记录协同程序(用于之后可能的停止)
            info.coroutine = MonoMgr.Instance.StartCoroutine(ReallyLoadAsync<T>(path));
        }
        else
        {
            // 从字典中取出资源信息
            info = resDic[resName] as ResInfo<T>;
            // 引用计数增加
            info.AddRefCount();
            // 如果资源还没有加载完,意味着还在进行异步加载
            if (info.asset == null)
            {
                info.callBack += callBack;
            }
            else
            {
                callBack?.Invoke(info.asset);
            }
        }
    }

    /// <summary>
    /// 真正的异步加载协程
    /// </summary>
    /// <typeparam name="T">资源类型</typeparam>
    /// <param name="path">资源路径</param>
    /// <returns>协程迭代器</returns>
    private IEnumerator ReallyLoadAsync<T>(string path) where T : UnityEngine.Object
    {
        // 异步加载资源
        ResourceRequest rq = Resources.LoadAsync<T>(path);
        // 等待资源加载结束后才会继续执行yield return后面的代码
        yield return rq;

        string resName = path + "_" + typeof(T).Name;
        // 资源加载结束,将资源传到外部的委托函数去进行使用
        if (resDic.ContainsKey(resName))
        {
            ResInfo<T> resInfo = resDic[resName] as ResInfo<T>;
            // 取出资源信息并且记录加载完成的资源
            resInfo.asset = rq.asset as T;

            // 如果发现需要删除,再去移除资源
            // 引用计数为0才真正去移除
            if (resInfo.refCount == 0)
            {
                UnloadAsset<T>(path, resInfo.isDel, null, false);
            }
            else
            {
                // 将加载完成的资源传递出去
                resInfo.callBack?.Invoke(resInfo.asset);
                // 加载完毕后,这些引用就可以清空,避免引用的占用可能带来的潜在的内存泄漏问题
                resInfo.callBack = null;
                resInfo.coroutine = null;
            }
        }
    }

    #endregion

    #region 资源卸载

    /// <summary>
    /// 指定卸载一个资源(泛型版本)
    /// </summary>
    /// <typeparam name="T">资源类型</typeparam>
    /// <param name="path">资源路径</param>
    /// <param name="isDel">是否立即删除</param>
    /// <param name="callBack">回调函数</param>
    /// <param name="isSub">是否减少引用计数</param>
    public void UnloadAsset<T>(string path, bool isDel = false, UnityAction<T> callBack = null, bool isSub = true)
    {
        string resName = path + "_" + typeof(T).Name;
        // 判断是否存在对应资源
        if (resDic.ContainsKey(resName))
        {
            ResInfo<T> resInfo = resDic[resName] as ResInfo<T>;
            // 引用计数-1
            if (isSub)
            {
                resInfo.SubRefCount();
            }

            // 记录引用计数为0时是否马上移除标签
            resInfo.isDel = isDel;
            // 资源已经加载结束
            if (resInfo.asset != null && resInfo.refCount == 0 && resInfo.isDel)
            {
                // 从字典移除
                resDic.Remove(resName);
                // 通过API卸载资源
                Resources.UnloadAsset(resInfo.asset as UnityEngine.Object);
            }
            else if (resInfo.asset == null) // 资源正在异步加载中
            {
                // 当异步加载不想使用时,我们应该移除它的回调记录而不是直接去卸载资源
                if (callBack != null)
                {
                    resInfo.callBack -= callBack;
                }
            }
        }
    }

    /// <summary>
    /// 异步卸载对应没有使用的Resources相关的资源
    /// 【重要提示】这是昂贵操作,建议只在场景切换或明确的内存整理阶段调用
    /// </summary>
    /// <param name="callBack">回调函数</param>
    public void UnloadUnusedAssets(UnityAction callBack)
    {
        MonoMgr.Instance.StartCoroutine(ReallyUnloadUnusedAssets(callBack));
    }

    /// <summary>
    /// 真正的异步卸载未使用资源协程
    /// </summary>
    /// <param name="callBack">回调函数</param>
    /// <returns>协程迭代器</returns>
    private IEnumerator ReallyUnloadUnusedAssets(UnityAction callBack)
    {
        // 在真正移除不使用的资源之前,应该把我们自己记录的那些引用计数为0并且没有被移除记录的资源移除掉
        List<string> list = new List<string>();
        foreach (string path in resDic.Keys)
        {
            if (resDic[path].refCount == 0)
            {
                list.Add(path);
            }
        }

        foreach (string path in list)
        {
            resDic.Remove(path);
        }

        AsyncOperation ao = Resources.UnloadUnusedAssets();
        yield return ao;
        // 卸载完毕后通知外部
        callBack();
    }

    #endregion

    #region 工具方法

    /// <summary>
    /// 获取当前某个资源的引用计数
    /// </summary>
    /// <typeparam name="T">资源类型</typeparam>
    /// <param name="path">资源路径</param>
    /// <returns>引用计数</returns>
    public int GetRefCount<T>(string path)
    {
        string resName = path + "_" + typeof(T).Name;
        if (resDic.ContainsKey(resName))
        {
            return (resDic[resName] as ResInfo<T>).refCount;
        }

        return 0;
    }

    /// <summary>
    /// 清空字典
    /// </summary>
    /// <param name="callBack">回调函数</param>
    public void ClearDic(UnityAction callBack)
    {
        MonoMgr.Instance.StartCoroutine(ReallyClearDic(callBack));
    }

    /// <summary>
    /// 真正的清空字典协程
    /// </summary>
    /// <param name="callBack">回调函数</param>
    /// <returns>协程迭代器</returns>
    private IEnumerator ReallyClearDic(UnityAction callBack)
    {
        resDic.Clear();
        AsyncOperation ao = Resources.UnloadUnusedAssets();
        yield return ao;
        // 卸载完毕后通知外部
        callBack();
    }

    #endregion
}

ResInfo.cs(资源信息类)

using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// 资源信息对象
/// 主要用于存储资源信息、异步加载委托信息、异步加载协程信息
/// </summary>
/// <typeparam name="T">资源类型</typeparam>
public class ResInfo<T> : ResInfoBase
{
    #region 字段

    /// <summary>
    /// 资源对象
    /// </summary>
    public T asset;

    /// <summary>
    /// 异步加载结束后的回调委托
    /// </summary>
    public UnityAction<T> callBack;

    /// <summary>
    /// 异步加载时开启的协程
    /// </summary>
    public Coroutine coroutine;

    /// <summary>
    /// 决定引用计数为0时是否真正需要移除
    /// </summary>
    public bool isDel;

    #endregion

    #region 引用计数管理

    /// <summary>
    /// 增加引用计数
    /// </summary>
    public void AddRefCount()
    {
        ++refCount;
    }

    /// <summary>
    /// 减少引用计数
    /// </summary>
    public void SubRefCount()
    {
        --refCount;
        if (refCount < 0)
        {
            Debug.LogError("引用计数小于0了,请检查使用和卸载是否配对执行");
        }
    }

    #endregion
}

ResInfoBase.cs(资源信息基类)

/// <summary>
/// 资源信息基类
/// 主要用于里式替换原则,父类容器装子类对象
/// </summary>
public abstract class ResInfoBase
{
    /// <summary>
    /// 引用计数
    /// </summary>
    public int refCount;
}

EditorResMgr.cs(编辑器资源管理器)

using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

/// <summary>
/// 编辑器资源管理器
/// 仅在编辑器模式下使用,用于管理编辑器中的资源
/// 提供与AssetBundle相同的管理接口,方便开发测试
/// </summary>
public class EditorResMgr : Singleton<EditorResMgr>
{
    #region 字段

    /// <summary>
    /// 编辑器资源根路径
    /// 用于放置需要打包进AB包中的资源路径
    /// </summary>
    private string rootPath = "Assets/Editor/ArtRes/";

    #endregion

    #region 构造函数

    private EditorResMgr() { }

    #endregion

    #region 资源加载

    /// <summary>
    /// 加载单个编辑器资源
    /// </summary>
    /// <typeparam name="T">资源类型</typeparam>
    /// <param name="path">资源路径</param>
    /// <returns>加载的资源</returns>
    public T LoadEditorRes<T>(string path) where T : Object
    {
#if UNITY_EDITOR
        string suffixName = "";
        // 根据资源类型添加对应的后缀名
        if (typeof(T) == typeof(GameObject))
        {
            suffixName = ".prefab";
        }
        else if (typeof(T) == typeof(Material))
        {
            suffixName = ".mat";
        }
        else if (typeof(T) == typeof(Texture))
        {
            suffixName = ".png";
        }
        else if (typeof(T) == typeof(AudioClip))
        {
            suffixName = ".mp3";
        }
        
        T res = AssetDatabase.LoadAssetAtPath<T>(rootPath + path + suffixName);
        return res;
#else
        return null;
#endif
    }

    /// <summary>
    /// 加载图集中的指定精灵
    /// </summary>
    /// <param name="path">图集路径</param>
    /// <param name="spriteName">精灵名称</param>
    /// <returns>加载的精灵</returns>
    public Sprite LoadSprite(string path, string spriteName)
    {
#if UNITY_EDITOR
        // 加载图集中的所有子资源
        Object[] sprites = AssetDatabase.LoadAllAssetRepresentationsAtPath(rootPath + path);
        // 遍历所有子资源,得到同名图片返回
        foreach (Object item in sprites)
        {
            if (spriteName == item.name)
            {
                return item as Sprite;
            }
        }
        return null;
#else
        return null;
#endif
    }

    /// <summary>
    /// 加载图集文件中的所有子图片并返回给外部
    /// </summary>
    /// <param name="path">图集路径</param>
    /// <returns>精灵字典</returns>
    public Dictionary<string, Sprite> LoadSprites(string path)
    {
#if UNITY_EDITOR
        Dictionary<string, Sprite> spriteDic = new Dictionary<string, Sprite>();
        Object[] sprites = AssetDatabase.LoadAllAssetRepresentationsAtPath(rootPath + path);
        foreach (Object item in sprites)
        {
            spriteDic.Add(item.name, item as Sprite);
        }
        return spriteDic;
#else
        return null;
#endif
    }

    #endregion
}

ABMgr.cs(AssetBundle管理器)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
#if UNITY_ANDROID || UNITY_WEBGL
using UnityEngine.Networking;
#endif

/// <summary>
/// AssetBundle管理器
/// 提供AssetBundle资源的管理、卸载和缓存功能
/// 支持同步和异步管理,自动处理依赖包
/// 注意:Android/WebGL平台自动使用UnityWebRequest加载StreamingAssets
/// </summary>
public class ABMgr : SingletonAutoMono<ABMgr>
{
    #region 字段

    /// <summary>
    /// 主包
    /// </summary>
    private AssetBundle mainAB = null;

    /// <summary>
    /// 主包依赖获取配置文件
    /// </summary>
    private AssetBundleManifest manifest = null;

    /// <summary>
    /// AssetBundle包字典
    /// AB包不能够重复加载,否则会报错
    /// 注意:值可以为null,表示正在异步加载中
    /// </summary>
    private Dictionary<string, AssetBundle> abDic = new Dictionary<string, AssetBundle>();

    #endregion

    #region 属性

    /// <summary>
    /// 获取AB包加载路径
    /// </summary>
    private string PathUrl
    {
        get
        {
            return Application.streamingAssetsPath + "/";
        }
    }

    /// <summary>
    /// 主包名,根据平台不同包名不同
    /// </summary>
    private string MainName
    {
        get
        {
#if UNITY_IOS
            return "IOS";
#elif UNITY_ANDROID
            return "Android";
#else
            return "PC";
#endif
        }
    }

    #endregion

    #region 主包和依赖包管理

    /// <summary>
    /// 确保主包已加载(协程版本,支持Android/WebGL)
    /// </summary>
    private IEnumerator EnsureMainAB()
    {
        if (mainAB != null) yield break;
        
#if UNITY_ANDROID || UNITY_WEBGL
        // Android/WebGL平台:使用UnityWebRequest加载主包
        string url = PathUrl + MainName;
        using (UnityWebRequest req = UnityWebRequestAssetBundle.GetAssetBundle(url))
        {
            yield return req.SendWebRequest();
            if (req.result != UnityWebRequest.Result.Success)
            {
                Debug.LogError($"主包加载失败: {req.error}");
                yield break;
            }
            mainAB = DownloadHandlerAssetBundle.GetContent(req);
        }
#else
        // 其他平台:使用LoadFromFile(同步但效率高)
        mainAB = AssetBundle.LoadFromFile(PathUrl + MainName);
#endif
        
        manifest = mainAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
    }

    #endregion

    #region 异步加载

    /// <summary>
    /// 泛型异步加载资源(支持同步开关)
    /// </summary>
    /// <typeparam name="T">资源类型</typeparam>
    /// <param name="abName">AB包名称</param>
    /// <param name="resName">资源名称</param>
    /// <param name="callBack">加载完成回调</param>
    /// <param name="isSync">是否同步加载资源(Android/WebGL的AB包加载仍用UWR)</param>
    public void LoadResAsync<T>(string abName, string resName, UnityAction<T> callBack, bool isSync = false) where T : Object
    {
        StartCoroutine(ReallyLoadResAsync<T>(abName, resName, callBack, isSync));
    }

    /// <summary>
    /// 真正的异步加载协程(泛型版本)
    /// </summary>
    private IEnumerator ReallyLoadResAsync<T>(string abName, string resName, UnityAction<T> callBack, bool isSync) where T : Object
    {
        // 【重要修复】确保主包已加载(支持Android/WebGL)
        yield return EnsureMainAB();
        
        // 获取依赖包
        string[] strs = manifest.GetAllDependencies(abName);
        for (int i = 0; i < strs.Length; i++)
        {
            // 还没有加载过该AB包
            if (!abDic.ContainsKey(strs[i]))
            {
                // 【重要修复】Android/WebGL平台统一使用UWR,isSync仅影响资源加载方式
#if UNITY_ANDROID || UNITY_WEBGL
                // Android/WebGL:即使是"同步"也要用UWR加载AB包
                abDic.Add(strs[i], null);
                string url = PathUrl + strs[i];
                using (UnityWebRequest req = UnityWebRequestAssetBundle.GetAssetBundle(url))
                {
                    yield return req.SendWebRequest();
                    if (req.result != UnityWebRequest.Result.Success)
                    {
                        Debug.LogError($"AB加载失败:{strs[i]}, error: {req.error}");
                        abDic.Remove(strs[i]);
                        yield break;
                    }
                    abDic[strs[i]] = DownloadHandlerAssetBundle.GetContent(req);
                }
#else
                // 其他平台:isSync决定是否等待协程加载AB包
                if (isSync)
                {
                    AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + strs[i]);
                    abDic.Add(strs[i], ab);
                }
                else
                {
                    // 【新增】异步加载:先记录null表示开始加载
                    abDic.Add(strs[i], null);
                    AssetBundleCreateRequest req = AssetBundle.LoadFromFileAsync(PathUrl + strs[i]);
                    yield return req;
                    // 【重要修复】检查加载是否成功
                    if (req.assetBundle == null)
                    {
                        Debug.LogError($"AssetBundle加载失败:{strs[i]}");
                        abDic.Remove(strs[i]);
                        yield break;
                    }
                    abDic[strs[i]] = req.assetBundle;
                }
#endif
            }
            else
            {
                // 【核心优化】等待机制:避免重复加载
                while (abDic[strs[i]] == null)
                {
                    yield return 0;
                }
            }
        }
        
        // 加载目标包
        if (!abDic.ContainsKey(abName))
        {
#if UNITY_ANDROID || UNITY_WEBGL
            // Android/WebGL:统一使用UWR
            abDic.Add(abName, null);
            string url = PathUrl + abName;
            using (UnityWebRequest req = UnityWebRequestAssetBundle.GetAssetBundle(url))
            {
                yield return req.SendWebRequest();
                if (req.result != UnityWebRequest.Result.Success)
                {
                    Debug.LogError($"AB加载失败:{abName}, error: {req.error}");
                    abDic.Remove(abName);
                    yield break;
                }
                abDic[abName] = DownloadHandlerAssetBundle.GetContent(req);
            }
#else
            // 其他平台:根据isSync决定
            if (isSync)
            {
                AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
                abDic.Add(abName, ab);
            }
            else
            {
                abDic.Add(abName, null);
                AssetBundleCreateRequest req = AssetBundle.LoadFromFileAsync(PathUrl + abName);
                yield return req;
                if (req.assetBundle == null)
                {
                    Debug.LogError($"AssetBundle加载失败:{abName}");
                    abDic.Remove(abName);
                    yield break;
                }
                abDic[abName] = req.assetBundle;
            }
#endif
        }
        else
        {
            // 如果字典中记录的信息是null,那就证明正在加载中
            while (abDic[abName] == null)
            {
                yield return 0;
            }
        }

        // 同步加载AB包中的资源
        if (isSync)
        {
            // 即使是同步加载也需要使用回调函数传给外部进行使用
            T res = abDic[abName].LoadAsset<T>(resName);
            callBack(res);
        }
        // 异步加载包中资源
        else
        {
            AssetBundleRequest abq = abDic[abName].LoadAssetAsync<T>(resName);
            yield return abq;

            callBack(abq.asset as T);
        }
    }

    #endregion

    #region 卸载和清理

    /// <summary>
    /// 卸载AB包的方法
    /// </summary>
    /// <param name="name">AB包名称</param>
    /// <param name="callBackResult">卸载结果回调</param>
    public void UnLoadAB(string name, UnityAction<bool> callBackResult)
    {
        if (abDic.ContainsKey(name))
        {
            if (abDic[name] == null)
            {
                // 代表正在异步加载,没有卸载成功
                callBackResult(false);
                return;
            }
            abDic[name].Unload(false);
            abDic.Remove(name);
            // 卸载成功
            callBackResult(true);
        }
    }

    /// <summary>
    /// 清空AB包的方法
    /// </summary>
    public void ClearAB()
    {
        // 由于AB包都是异步加载了,因此在清理之前停止协同程序
        StopAllCoroutines();
        AssetBundle.UnloadAllAssetBundles(false);
        abDic.Clear();
        // 卸载主包
        mainAB = null;
    }

    #endregion
}

ABResMgr.cs(AssetBundle资源管理器)

using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// AssetBundle资源管理器
/// 提供统一的资源加载接口,在编辑器和运行时之间切换
/// 编辑器模式下可选择使用EditorResMgr或ABMgr
/// </summary>
public class ABResMgr : Singleton<ABResMgr>
{
    #region 字段

    /// <summary>
    /// 是否开启调试模式
    /// true:编辑器模式下使用EditorResMgr
    /// false:使用ABMgr
    /// </summary>
    private bool isDebug = false;

    #endregion

    #region 构造函数

    private ABResMgr() { }

    #endregion

    #region 资源加载

    /// <summary>
    /// 异步加载资源
    /// </summary>
    /// <typeparam name="T">资源类型</typeparam>
    /// <param name="abName">AB包名称</param>
    /// <param name="resName">资源名称</param>
    /// <param name="callBack">加载完成回调</param>
    /// <param name="isSync">是否同步加载</param>
    public void LoadResAsync<T>(string abName, string resName, UnityAction<T> callBack, bool isSync = false) where T : Object
    {
#if UNITY_EDITOR
        if (isDebug)
        {
            // 编辑器模式下使用EditorResMgr加载资源
            // 我们自定义了一个AB包中资源的管理方式,对应文件夹名就是包名
            T res = EditorResMgr.Instance.LoadEditorRes<T>($"{abName}/{resName}");
            callBack?.Invoke(res);
        }
        else
        {
            // 使用ABMgr加载资源
            ABMgr.Instance.LoadResAsync<T>(abName, resName, callBack, isSync);
        }
#else
        // 非编辑器模式下直接使用ABMgr
        ABMgr.Instance.LoadResAsync<T>(abName, resName, callBack, isSync);
#endif
    }

    #endregion
}

UWQResMgr.cs(UnityWebRequest资源管理器)

using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Networking;

/// <summary>
/// UnityWebRequest资源管理器
/// 使用UnityWebRequest管理网络资源
/// 支持管理字符串、字节数组、纹理、AssetBundle等资源
/// </summary>
public class UWQResMgr : SingletonAutoMono<UWQResMgr>
{
    #region 资源加载

    /// <summary>
    /// 利用UnityWebRequest去加载资源
    /// </summary>
    /// <typeparam name="T">类型只能是string、byte[]、Texture、AssetBundle</typeparam>
    /// <param name="path">资源路径,要自己加上协议http、ftp、file</param>
    /// <param name="callBack">加载成功的回调函数</param>
    /// <param name="failCallBack">加载失败的回调函数</param>
    public void LoadRes<T>(string path, UnityAction<T> callBack, UnityAction failCallBack) where T : class
    {
        StartCoroutine(ReallyLoadRes<T>(path, callBack, failCallBack));
    }

    /// <summary>
    /// 真正的网络资源加载协程
    /// </summary>
    private IEnumerator ReallyLoadRes<T>(string path, UnityAction<T> callBack, UnityAction failCallBack) where T : class
    {
        Type type = typeof(T);
        // 用于加载的对象
        UnityWebRequest req = null;
        
        if (type == typeof(string) || type == typeof(byte[]))
        {
            req = UnityWebRequest.Get(path);
        }
        else if (type == typeof(Texture2D) || type == typeof(Texture))
        {
            // 【重要修复】Texture2D 是 Texture 的子类,UnityWebRequestTexture.GetTexture 返回 Texture2D
            req = UnityWebRequestTexture.GetTexture(path);
        }
        else if (type == typeof(AssetBundle))
        {
            req = UnityWebRequestAssetBundle.GetAssetBundle(path);
        }
        else
        {
            failCallBack?.Invoke();
            yield break;
        }

        yield return req.SendWebRequest();
        
        // 如果加载成功
        if (req.result == UnityWebRequest.Result.Success)
        {
            if (type == typeof(string))
            {
                callBack?.Invoke(req.downloadHandler.text as T);
            }
            else if (type == typeof(byte[]))
            {
                callBack?.Invoke(req.downloadHandler.data as T);
            }
            else if (type == typeof(Texture2D) || type == typeof(Texture))
            {
                // 【重要修复】Texture 实际上返回的是 Texture2D
                callBack?.Invoke(DownloadHandlerTexture.GetContent(req) as T);
            }
            else if (type == typeof(AssetBundle))
            {
                callBack?.Invoke(DownloadHandlerAssetBundle.GetContent(req) as T);
            }
        }
        else
        {
            failCallBack?.Invoke();
        }
        
        // 释放UWQ对象
        req.Dispose();
    }

    #endregion
}

ResourceLoadingTest.cs(Resources资源管理测试)

using UnityEngine;

public class ResourceLoadingTest : MonoBehaviour
{
    void Start()
    {
        Debug.Log("Resources资源管理测试:开始");
        
        // 测试同步加载
        TestSynchronousLoad();
        
        // 测试异步加载
        TestAsynchronousLoad();
        
        // 测试引用计数
        TestReferenceCounting();
    }

    /// <summary>
    /// 测试同步加载资源
    /// </summary>
    private void TestSynchronousLoad()
    {
        Debug.Log("测试同步加载资源");
        GameObject prefab = ResMgr.Instance.Load<GameObject>("Test");
        Debug.Log("同步加载完成:" + prefab.name);
        // 输出:同步加载完成:Test
    }

    /// <summary>
    /// 测试异步加载资源
    /// </summary>
    private void TestAsynchronousLoad()
    {
        Debug.Log("测试异步加载资源");
        ResMgr.Instance.LoadAsync<GameObject>("Test", (obj) => {
            Debug.Log("异步加载完成:" + obj.name);
            // 输出:异步加载完成:Test
            Instantiate(obj);
        });
    }

    /// <summary>
    /// 测试引用计数
    /// </summary>
    private void TestReferenceCounting()
    {
        // 第一次加载
        GameObject obj1 = ResMgr.Instance.Load<GameObject>("Test");
        Debug.Log("引用计数:" + ResMgr.Instance.GetRefCount<GameObject>("Test"));
        // 输出:引用计数:1

        // 第二次加载(应该复用)
        GameObject obj2 = ResMgr.Instance.Load<GameObject>("Test");
        Debug.Log("引用计数:" + ResMgr.Instance.GetRefCount<GameObject>("Test"));
        // 输出:引用计数:2

        // 卸载一次
        ResMgr.Instance.UnloadAsset<GameObject>("Test");
        Debug.Log("引用计数:" + ResMgr.Instance.GetRefCount<GameObject>("Test"));
        // 输出:引用计数:1

        // 再次卸载
        ResMgr.Instance.UnloadAsset<GameObject>("Test");
        Debug.Log("引用计数:" + ResMgr.Instance.GetRefCount<GameObject>("Test"));
        // 输出:引用计数:0(资源已被移除)
    }
}

ABLoadingTest.cs(AssetBundle资源管理测试)

using UnityEngine;

public class ABLoadingTest : MonoBehaviour
{
    void Start()
    {
        Debug.Log("AB包管理测试:开始");
        
        // 测试异步加载
        ABMgr.Instance.LoadResAsync<GameObject>("TestAB", "TestPrefab", (obj) => {
            Debug.Log("异步加载完成:" + obj.name);
            // 输出:异步加载完成:TestPrefab
        });
        
        // 测试同步加载
        ABMgr.Instance.LoadResAsync<GameObject>("TestAB", "TestPrefab", (obj) => {
            Debug.Log("同步加载完成:" + obj.name);
            // 输出:同步加载完成:TestPrefab
        }, true);
    }
}

UWQLoadingTest.cs(UnityWebRequest资源管理测试)

using UnityEngine;

public class UWQLoadingTest : MonoBehaviour
{
    void Start()
    {
        Debug.Log("UnityWebRequest管理测试:开始");
        
        // 测试加载本地文件
        TestLoadLocalFile();
        
        // 测试加载网络文件
        TestLoadNetworkFile();
    }

    /// <summary>
    /// 测试加载本地文件
    /// </summary>
    private void TestLoadLocalFile()
    {
        // 使用file协议加载本地文件
        string localPath = "file://" + Application.streamingAssetsPath + "/test.txt";
        UWQResMgr.Instance.LoadRes<string>(localPath, 
            (text) => {
                Debug.Log("加载文本成功:" + text);
                // 输出:加载文本成功:(文件内容)
            },
            () => {
                Debug.Log("加载文本失败");
            }
        );
    }

    /// <summary>
    /// 测试加载网络文件
    /// </summary>
    private void TestLoadNetworkFile()
    {
        // 加载网络资源
        string url = "http://example.com/image.png";
        UWQResMgr.Instance.LoadRes<Texture>(url,
            (texture) => {
                Debug.Log("加载纹理成功:" + texture.width + "x" + texture.height);
                // 输出:加载纹理成功:1920x1080
            },
            () => {
                Debug.Log("加载纹理失败");
            }
        );
    }
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏