6.资源管理模块
6.1 知识点
资源管理模块的主要作用
在Unity开发中,资源管理是不可或缺的核心环节。不同类型的资源需要不同的加载策略,而原生的Unity资源加载方式往往存在各种问题。
核心问题:
- Resources同步加载会卡顿:每次加载都会阻塞主线程
- 频繁调用异步加载浪费性能:每次都会开启新的协程
- 资源管理混乱:无法判断资源是否被使用,难以释放
- 平台差异:不同平台资源加载方式不同
- 代码冗余:资源加载逻辑分散在各个地方
解决方案:
资源管理模块通过统一管理、缓存优化、引用计数等机制,为Unity游戏开发提供高效、灵活的资源管理解决方案。
主要功能:
- Resources资源管理:封装同步和异步加载,提供缓存和引用计数
- Editor资源管理:编辑器模式下快速加载资源,方便开发测试
- AssetBundle管理:支持同步和异步加载,自动处理依赖包
- UnityWebRequest管理:支持网络资源和本地文件加载
- 统一接口:提供统一的资源管理接口,简化使用



资源管理模块的基本原理
设计理念:
- 统一管理:通过管理器统一管理所有资源操作
- 缓存机制:避免重复加载,提高性能
- 引用计数:通过引用计数判断资源是否被使用
- 异步优化:通过协程实现真正的异步加载
- 平台适配:支持不同平台和不同资源加载方式
工作流程:
- 资源加载:调用管理器加载资源
- 缓存检查:检查资源是否已加载
- 加载策略:根据资源状态选择加载方式
- 引用管理:通过引用计数管理资源生命周期
- 资源释放:引用计数为0时释放资源

Resources资源管理模块基础实现
基础实现
重要提示:
Unity 官方明确建议:不要将大量资源放在 Resources 文件夹中。Resources 会让内存管理变难、启动/构建变慢。建议仅作少量通用或开发期兜底资源,主流程走 AB/Addressables/YooAsset 等方案。
Resources资源管理的四个步骤:
- 创建ResMgr继承单例模式基类:继承
Singleton<T>,确保全局唯一 - 封装同步加载方法:封装
Resources.Load<T>,简化调用 - 封装异步加载方法:封装
Resources.LoadAsync<T>,配合协程实现异步加载 - 封装资源卸载方法:封装
Resources.UnloadAsset和UnloadUnusedAssets
基础源码实现: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 (添加字典缓存优化)
优化点:
- 新增ResInfoBase基类和ResInfo泛型类:用于存储资源信息和加载状态
- 添加字典缓存机制:
resDic用于记录已加载或正在加载的资源 - 协程管理:记录异步加载的协程,支持中断和复用
- 委托复用:多个异步请求共享同一个加载协程
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)。如果是,会:
- 停止异步加载:调用
StopCoroutine停止正在执行的协程 - 立即同步加载:直接使用
Resources.Load<T>加载资源 - 执行回调:触发所有等待异步加载完成的回调
- 清理引用:清除异步加载的回调和协程记录
这样可以避免资源重复加载,但会浪费已经开始的异步加载工作。
AB包的处理机制:
ABMgr中,如果依赖包或目标包正在异步加载(字典中值为null),同步加载会等待异步加载完成后再继续。这可以避免AssetBundle重复加载导致的错误。
加入引用计数
为了解决资源管理和卸载问题,我们需要引入引用计数机制。
引用计数是什么?
引用计数是一种内存管理技术,用于跟踪资源被引用的次数。我们通过一个整型变量来记录资源的使用次数。当有对象引用该资源时,计数器会增加;当对象不再引用该资源时,计数器会减少。
如何向ResMgr中加入引用计数功能:
- 为ResInfo类加入引用计数成员变量和方法:添加
refCount变量和AddRefCount()、SubRefCount()方法 - 使用资源时加:每次加载资源时,引用计数加1
- 不使用资源时减:每次卸载资源时,引用计数减1
- 处理异步回调问题:某一个异步不使用资源了应该移除回调函数的记录
- 修改移除资源函数逻辑:引用计数为0时才真正移除资源
- 考虑资源频繁移除问题:加入”马上移除”bool标签
- 修改移除不使用资源函数逻辑:释放时清除引用计数为0的记录
加入引用计数后的源码实现:ResInfo.cs 和 ResMgr.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(资源已被移除)
}
}
注意事项:
- 使用资源时需要有增有减,当使用某个资源的对象移除时,一定要记得调用移除方法
- 如果觉得卸载资源的功能麻烦,也完全可以不使用卸载的相关方法,加载相关逻辑不会有任何影响,只需要再添加一个主动清空字典的方法即可
Android/iOS平台特殊处理:
在Android和iOS平台上,StreamingAssets 文件夹中的文件位于压缩包内,无法直接使用 AssetBundle.LoadFromFile 加载。建议的解决方案:
- 使用UnityWebRequest加载:改用
UnityWebRequestAssetBundle.GetAssetBundle(filePath) - 首次运行拷贝:启动时将AB包从
StreamingAssets拷贝到persistentDataPath,然后从永久路径加载
Editor资源加载模块
Editor资源加载的主要作用



Editor资源管理基本原理
主要使用的API:
AssetDatabase.LoadAssetAtPath:用于加载单个资源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;
}
}
存在的问题:
- AB包本身的加载是同步的,只有从AB包中加载资源才是异步的,不够彻底
- Android平台StreamingAssets加载问题:
LoadFromFile(StreamingAssets/...)在 Android/iOS 会失败,因为 StreamingAssets 在压缩包里。需要使用UnityWebRequest加载,或首次运行时拷贝到persistentDataPath
AssetBundle潜在问题考虑
异步加载AB包可能带来的问题:
如果我们想要将ABMgr中的异步加载方法改为真正意义上的异步(不仅从AB包中加载资源是异步的,还需要在加载AB包时也采用异步),那么我们就需要考虑一个问题:如果当我们正在异步加载AB包时,又进行了一次同步加载AB包,是否会报错(因为AB包不允许重复加载)?
实验验证:
通过验证我们得出结论:异步加载AB包时,再同步加载AB包,会因重复加载而报错。因此我们在修改逻辑时应该考虑到这个问题。
ABMgr异步加载修改
ABMgr异步加载修改主要目标:
将ABMgr中的异步加载方法彻底异步化,让其中的依赖包加载、资源包加载、资源加载都变为异步加载。
注意:主包可以保留同步加载。
需要考虑的主要问题:
- 某个AB包当正在异步加载时又进行重复加载:遇到这种情况时,我们需要避免重复加载报错。因此我们不应再次加载,而是等待之前的异步加载结束后直接使用。
- 正在加载某个AB包时卸载AB包:如果正在加载中,不允许卸载。
- 正在加载时清空AB包:停止所有协同程序,再清理AB包。
- 异步加载失败的处理:
LoadFromFileAsync失败时会返回assetBundle == null,如果直接跳过会进入永久等待。需要添加失败分支处理。
优化后的源码实现:ABMgr.cs
优化点:
- 字典值支持null:
null表示正在异步加载中,非null表示已加载完成 - 等待机制:通过协程轮询等待正在加载的AB包
- 防重复加载:避免AssetBundle重复加载导致的错误
- 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中的同步加载相关逻辑:
- 注释之前的同步加载代码
- 在异步加载的基础上进行修改
优化后的源码实现: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具体实现
主要用到的知识点:
- UnityWebRequest相关知识(Unity网络开发基础)
- 泛型相关知识(C#四部曲C#进阶中)
- 协同程序相关知识(Unity四部曲之Unity基础中)
- 反射中的Type相关知识(C#四部曲C#进阶中)
- 委托相关知识(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