引用:
https://blog.csdn.net/wowo1gt/article/details/101296295
https://blog.csdn.net/wowo1gt/article/details/100561236 (单独另一篇)
Unity的资源加载
Unity最通用的资源加载方式,就三种
1. Resources资源加载(Runtime和Editor模式)
2. AssetBundle资源加载(Runtime和Editor模式)
3. AssetDataBase资源加载(Editor模式)
大部分游戏,为了热更和效率考虑,都是——
Runtime时,绝大部分资源使用AssetBundle,极少数资源使用Resources
Editor时,使用AssetDataBase为主,Resources为辅
那么,我们的设计就要包含这三种加载方式。
Unity资源类型,按加载流程顺序,有三种:
1. AssetBundle 资源以压缩包文件存在(Resources目录下资源打成包体后也是以ab格式存在)
2. Asset 资源在内存中的存在格式
3. GameObject 针对Prefab导出的Asset,可实例化
Unity启动的时候,会将Resources目录下资源的ab加载到内存中,所以我们能直接使用Resources.Load()来加载资源。
针对AssetBundle 的加载,读者可以参阅AssetBundle同步异步引用计数资源加载管理器,下文中的
针对Asset 的加载,本文会作讲解,并提供整套方案和代码
针对GameObject的加载,读者可以参阅Prefab加载自动化管理引用计数管理器。
依据上面的需求,我们来设计并实现一套Asset资源加载管理器吧。
加载框架设计
Asset加载,要内部衔接多种资源加载方式,对外部隐藏底层资源加载逻辑。
内部主要管理三种加载方式
我们能在后续代码中看到很多如下的写法
public bool IsAssetExist(string _assetName) { #if UNITY_EDITOR && !TEST_AB return EditorAssetLoadMgr.I.IsFileExist(_assetName); #else if (ResourcesLoadMgr.I.IsFileExist(_assetName)) return true; return AssetBundleLoadMgr.I.IsABExist(_assetName); #endif }
宏 UNITY_EDITOR 和 TEST_AB 限定了 Runtime 和 Editor 模式,隐藏了内部逻辑,在Editor下也可以打开ab加载的开关,测试ab加载是否正确,逻辑是否正常。
EditorAssetLoadMgr,ResourcesLoadMgr和AssetBundleLoadMgr都有通用的4个接口——IsFileExist,LoadAsync,LoadSync和Unload。
内部是对资源方式的封装,外部接口提供了类似的通用接口
当然,本文底部源码里,还有很多函数实现了特定功能,例如RemoveCallBack,AddAsset, AddAssetRef等,这些只是功能性的函数,并不影响主体结构,就不展开讲了。
外部结构,内部结构都定义好了,我们开始实现逻辑。
Update才是王道
要Update,先从队列开始
private Dictionary<string, AssetObject> _loadingList; //加载队列 private Dictionary<string, AssetObject> _loadedList; //完成队列 private Dictionary<string, AssetObject> _unloadList; //卸载队列 private List<AssetObject> _loadedAsyncList; //异步加载队列,延迟回调 private Queue<PreloadAssetObject> _preloadedAsyncList; //异步预加载,空闲时加载
主体就三个队列,加载队列、完成队列和销毁队列,跟大部分开发者的资源管理大同小异
当一个异步加载开始,创建Asset单元放入加载队列
当异步加载结束,将Asset单元移入完成队列
外部调用卸载,引用计数为0的Asset单元放入卸载队列
卸载队列中延期卸载时间结束,真正卸载
这边还有2个特殊队列,预加载队列和异步加载队列
预加载队列实现的是——当加载队列为空情况下,取1个创建Asset单元放入加载队列
异步加载队列实现的是——当资源已经加载完成,但需要异步回调时,延帧回调
开始异步加载
先来看一下加载单元的数据结构
private class AssetObject { public string _assetName; public int _lockCallbackCount; //记录回调当前数量,保证异步是下一帧回调 public List<AssetsLoadCallback> _callbackList = new List<AssetsLoadCallback>(); //回调函数 public int _instanceID; //asset的id public AsyncOperation _request; //异步请求,AssetBundleRequest或ResourceRequest public UnityEngine.Object _asset; //加载的资源Asset public bool _isAbLoad; //标识是否是ab资源加载的 public bool _isWeak = true; //是否是弱引用,用于预加载和释放 public int _refCount; //引用计数 public int _unloadTick; //卸载使用延迟卸载,UNLOAD_DELAY_TICK_BASE + _unloadList.Count }
类成员比较多,标注得很清晰了,分别对应加载
,回调
,卸载
三个部分,先来看加载
部分代码
加载
加载启动的代码
//异步加载,即使资源已经加载完成,也会异步回调。 public void LoadAsync(string _assetName, AssetsLoadCallback _callFun) { AssetObject assetObj = null; if (_loadedList.ContainsKey(_assetName)) { assetObj = _loadedList[_assetName]; assetObj._callbackList.Add(_callFun); _loadedAsyncList.Add(assetObj); return; } else if (_loadingList.ContainsKey(_assetName)) { assetObj = _loadingList[_assetName]; assetObj._callbackList.Add(_callFun); return; } assetObj = new AssetObject(); assetObj._assetName = _assetName; assetObj._callbackList.Add(_callFun); #if UNITY_EDITOR && !TEST_AB _loadingList.Add(_assetName, assetObj); assetObj._request = EditorAssetLoadMgr.I.LoadAsync(_assetName); #else if (AssetBundleLoadMgr.I.IsABExist(_assetName)) { assetObj._isAbLoad = true; _loadingList.Add(hashName, assetObj); AssetBundleLoadMgr.I.LoadAsync(_assetName, (AssetBundle _ab) => { if (_loadingList.ContainsKey(hashName) && assetObj._request == null && assetObj._asset == null) { assetObj._request = _ab.LoadAssetAsync(_ab.GetAllAssetNames()[0]); } } ); } else if (ResourcesLoadMgr.I.IsFileExist(_assetName)) { assetObj._isAbLoad = false; _loadingList.Add(hashName, assetObj); assetObj._request = ResourcesLoadMgr.I.LoadAsync(_assetName); } else return; #endif }
代码逻辑还是很清晰的,分2部分
- 异步加载的资源在队列中,处理不同的队列逻辑
- 异步加载的资源不在队列中,创建一个加载请求,按逻辑从三个不同加载途径加载
三个不同加载途径不同的是,AssetBundle的加载是需要异步等待回调,然后调用_ab.LoadAssetAsync(_ab.GetAllAssetNames()[0]);来提取request,而其他2个途径,直接同步提取request。
TIP:为什么_ab.LoadAssetAsync(string name) 用 _ab.GetAllAssetNames()[0]?
因为name是未知的,并不一定是 _assetName(确实大部分情况)。 当然读者为了追求效率,也可以在打包导出ab资源的时候限定name和_assetName一定关联,并且处理好一些特殊情况,
比如场景和内置资源的处理
加载完成代码,是放在 Update()
下的
private void UpdateLoading() { if (_loadingList.Count == 0) return; //检测加载完的 tempLoadeds.Clear(); foreach (var assetObj in _loadingList.Values) { if (assetObj._request != null && assetObj._request.isDone) { #if UNITY_EDITOR && !TEST_AB assetObj._asset = (assetObj._request as ResourceRequest).asset; #else if (assetObj._isAbLoad) assetObj._asset = (assetObj._request as AssetBundleRequest).asset; else assetObj._asset = (assetObj._request as ResourceRequest).asset; #endif assetObj._instanceID = assetObj._asset.GetInstanceID(); _goInstanceIDList.Add(assetObj._instanceID, assetObj); assetObj._request = null; tempLoadeds.Add(assetObj); } } //回调中有可能对_loadingList进行操作,先移动 foreach (var assetObj in tempLoadeds) { _loadingList.Remove(assetObj._assetName); _loadedList.Add(assetObj._assetName, assetObj); _loadingIntervalCount++; //统计本轮加载的数量 //先锁定回调数量,保证异步成立 assetObj._lockCallbackCount = assetObj._callbackList.Count; } foreach (var assetObj in tempLoadeds) { DoAssetCallback(assetObj); } }
代码逻辑不复杂,遍历_loadingList列表找到异步加载完成的资源,将其提取资源并转换队列。
提取资源,先判断_request.isDone,然后提取_request..asset,并将_asset.GetInstanceID()保存下来用于卸载资源。
转换队列,从_loadingList移除,_loadedList加入,跟大部分开发者大同小异。
TIP:遍历为什么要用3个foreach循环的?
这边用了临时变量tempLoadeds去衔接。 第一个遍历是提取,第二个遍历是改变队列,第三个遍历是回调。 第二个是保证第一个遍历队列操作不出错,第三个是保证回调个数的限制
回调
回调代码
foreach (var assetObj in tempLoadeds) { _loadingList.Remove(assetObj._assetName); _loadedList.Add(assetObj._assetName, assetObj); _loadingIntervalCount++; //统计本轮加载的数量 //先锁定回调数量,保证异步成立 assetObj._lockCallbackCount = assetObj._callbackList.Count; } foreach (var assetObj in tempLoadeds) { DoAssetCallback(assetObj); } private void DoAssetCallback(AssetObject _assetObj) { if (_assetObj._callbackList.Count == 0) return; int count = _assetObj._lockCallbackCount; //先提取count,保证回调中有加载需求不加载 for (int i = 0; i < count; i++) { if (_assetObj._callbackList[i] != null) { _assetObj._refCount++; //每次回调,引用计数+1 try { _assetObj._callbackList[i](_assetObj._assetName, _assetObj._asset); } catch (System.Exception e) { Debug.LogError(e); } } } _assetObj._callbackList.RemoveRange(0, count); }
看关键的两行代码
assetObj._lockCallbackCount = assetObj._callbackList.Count;
和
int count = _assetObj._lockCallbackCount;
加载完成,需要回调的时候,如果在回调里有代码再请求加载呢?
所以,这边要先提取回调的个数,再进行限定次数的回调,这样才能保证回调代码里调用加载不影响当前逻辑。 同时,回调也要不能在原始队列里遍历,导致报错。 如果不作限制,回调的加载导致队列改变,回调数量增加,整个逻辑就会错误
TIP:为什么 _assetObj._refCount++;
引用计数是在回调的时候添加,而不是加载的时候?
最初设计的时候确实是在加载启动的时候添加引用计数。 后来加了RemoveCallBack,AddAsset,AddAssetRef,_loadedAsyncList,PreLoad等功能之后,
引用计数计数的意义由多少次请求加载变成了外部代码有多少引用Asset,那么用回调来作为标准是更合适的,
因为回调是明确的真正的引用。 最重要的,有一个功能是预加载,有请求且无回调,所以引用计数用在回调上,而不是请求加载上!
卸载
卸载分三步,启动卸载、遍历延迟卸载和真正卸载。(以下代码去掉了部分错误判定,只留关键代码)
public void Unload(UnityEngine.Object _obj) {//启动卸载 if (_obj == null) return; int instanceID = _obj.GetInstanceID(); if (!_goInstanceIDList.ContainsKey(instanceID)) {//非从本类创建的资源,直接销毁即可 return; } var assetObj = _goInstanceIDList[instanceID]; assetObj._refCount--; if (assetObj._refCount == 0 && !_unloadList.ContainsKey(assetObj._assetName)) { assetObj._unloadTick = UNLOAD_DELAY_TICK_BASE + _unloadList.Count; _unloadList.Add(assetObj._assetName, assetObj); } }
启动卸载,就是简单地找出对应的资源,放入卸载队列(并不删除其他队列资源)。
这边的延迟卸载 assetObj._unloadTick = UNLOAD_DELAY_TICK_BASE + _unloadList.Count;
你可以看到卸载的时间不是一致的,是穿插开的,这样保证在某个时刻大量卸载的时候,资源卸载的压力平摊到后面一段时间上,兼顾效率和内存。
public const int UNLOAD_DELAY_TICK_BASE = 60 * 60; //卸载最低延迟
这个延迟时间,读者可以根据自己的需求来。
当然,如果读者想立即卸载呢?那你写一个强制卸载函数就行啦,外部调用,并不影响整体逻辑。
private void UpdateUnload() {//遍历卸载,延迟卸载 if (_unloadList.Count == 0) return; tempLoadeds.Clear(); foreach (var assetObj in _unloadList.Values) { if (assetObj._isWeak && assetObj._refCount == 0 && assetObj._callbackList.Count == 0) {//引用计数为0,且没有需要回调的函数,销毁 if (assetObj._unloadTick < 0) { _loadedList.Remove(assetObj._assetName); DoUnload(assetObj); tempLoadeds.Add(assetObj); } else assetObj._unloadTick--; } if (assetObj._refCount > 0 || !assetObj._isWeak) {//引用计数增加(销毁期间有加载) tempLoadeds.Add(assetObj); } } foreach (var assetObj in tempLoadeds) { _unloadList.Remove(assetObj._assetName); } }
遍历延迟卸载,延迟卸载是为了将卸载压力平摊到每一帧上,而不是在一帧上出现卡顿。
同样的,需要保证在卸载期间,如果这个资源再次被请求加载,可以把这个资源从卸载列表移除。
再来看一下真正卸载。
private void DoUnload(AssetObject _assetObj) {//真正卸载 #if UNITY_EDITOR && !TEST_AB EditorAssetLoadMgr.I.Unload(_assetObj._asset); #else if (_assetObj._isAbLoad) AssetBundleLoadMgr.I.Unload(_assetObj._assetName); else ResourcesLoadMgr.I.Unload(_assetObj._asset); #endif _assetObj._asset = null; if (_goInstanceIDList.ContainsKey(_assetObj._instanceID)) { _goInstanceIDList.Remove(_assetObj._instanceID); } }
真正卸载,就是将asset释放,调用三种资源加载方式的接口,比较简单。
我还要同步加载
由于有异步加载,叠加同步加载,需要有异步转同步功能。先来看代码(去掉了错误处理)
public UnityEngine.Object LoadSync(string _assetName) { AssetObject assetObj = null; if (_loadedList.ContainsKey(_assetName)) { assetObj = _loadedList[_assetName]; assetObj._refCount++; return assetObj._asset; } else if (_loadingList.ContainsKey(_assetName)) { assetObj = _loadingList[_assetName]; if (assetObj._request != null) { if (assetObj._request is AssetBundleRequest) assetObj._asset = (assetObj._request as AssetBundleRequest).asset; //直接取,会异步变同步 else assetObj._asset = (assetObj._request as ResourceRequest).asset; assetObj._request = null; } else { #if UNITY_EDITOR && !TEST_AB assetObj._asset = EditorAssetLoadMgr.I.LoadSync(_assetName); #else if (assetObj._isAbLoad) { AssetBundle ab1 = AssetBundleLoadMgr.I.LoadSync(_assetName); assetObj._asset = ab1.LoadAsset(ab1.GetAllAssetNames()[0]); //异步转同步,需要卸载异步的引用计数 AssetBundleLoadMgr.I.Unload(_assetName); } else assetObj._asset = ResourcesLoadMgr.I.LoadSync(_assetName); #endif } assetObj._instanceID = assetObj._asset.GetInstanceID(); _goInstanceIDList.Add(assetObj._instanceID, assetObj); _loadingList.Remove(assetObj._assetName); _loadedList.Add(assetObj._assetName, assetObj); _loadedAsyncList.Add(assetObj); //原先异步加载的,加入异步表 assetObj._refCount++; return assetObj._asset; } assetObj = new AssetObject(); assetObj._assetName = _assetName; #if UNITY_EDITOR && !TEST_AB assetObj._asset = EditorAssetLoadMgr.I.LoadSync(_assetName); #else if (AssetBundleLoadMgr.I.IsABExist(_assetName)) { assetObj._isAbLoad = true; AssetBundle ab1 = AssetBundleLoadMgr.I.LoadSync(_assetName); assetObj._asset = ab1.LoadAsset(ab1.GetAllAssetNames()[0]); } else if (ResourcesLoadMgr.I.IsFileExist(_assetName)) { assetObj._isAbLoad = false; assetObj._asset = ResourcesLoadMgr.I.LoadSync(_assetName); } else return null; #endif assetObj._instanceID = assetObj._asset.GetInstanceID(); _goInstanceIDList.Add(assetObj._instanceID, assetObj); _loadedList.Add(_assetName, assetObj); assetObj._refCount = 1; return assetObj._asset; }
代码比较多,图解比较方便
逻辑不复杂,就是分类讨论。
AssetBundleRequest
和 ResourceRequest
的 asset
属性都可以在异步没有加载完成的情况下,提取其asset,拿到想要的asset,Unity已经帮助我们做了这个事情。所以异步转同步并没有那么麻烦。
对于
AssetBundle ab1 = AssetBundleLoadMgr.I.LoadSync(_assetName); assetObj._asset = ab1.LoadAsset(ab1.GetAllAssetNames()[0]); //异步转同步,需要卸载异步的引用计数 AssetBundleLoadMgr.I.Unload(_assetName);
这段代码的疑惑,请看 下面链接 的 我要异步加载和同步加载一起用 部分内容。
https://blog.csdn.net/wowo1gt/article/details/100561236
预加载——其实我是空闲加载
预加载,顾名思义,先加载到内存,需要的时候可以直接拿到结果,不用经历加载不卡顿。
笔者这里这个意义更宽泛一点,主要几个含义:
1. 预加载为空闲时加载,优先级低于主加载
2. 预加载不影响主加载,且异步加载
3. 预加载资源2种模式——内存常驻型资源和用完卸载型
看一下数据结构
private class PreloadAssetObject { public string _assetName; public bool _isWeak = true; //是否是弱引用 } private Queue<PreloadAssetObject> _preloadedAsyncList; //异步预加载,空闲时加载
_isWeak
是弱引用标识,为true时,表示这个资源可以在没有引用时卸载,否则常驻内存。常驻内存是指引用计数为0也不卸载。
启动预加载
//预加载,isWeak弱引用,true为使用过后会销毁,为false将不会销毁,慎用 public void PreLoad(string _assetName, bool _isWeak = true) { AssetObject assetObj = null; if (_loadedList.ContainsKey(_assetName)) assetObj = _loadedList[_assetName]; else if (_loadingList.ContainsKey(_assetName)) assetObj = _loadingList[_assetName]; //如果已经存在,改变其弱引用关系 if (assetObj != null) { assetObj._isWeak = _isWeak; if (_isWeak && assetObj._refCount == 0 && !_unloadList.ContainsKey(_assetName)) _unloadList.Add(_assetName, assetObj); return; } PreloadAssetObject plAssetObj = new PreloadAssetObject(); plAssetObj._assetName = _assetName; plAssetObj._isWeak = _isWeak; _preloadedAsyncList.Enqueue(plAssetObj); }
预加载是附加功能,不影响加载流程,但会改变强弱引用关系。所以上述代码会在改变强弱引用关系时,需要判断是否卸载资源。
既然预加载需要加入队列,什么时候取出呢?Update的时候
private void UpdatePreload() { //加载队列空闲才需要预加载 if (_loadingList.Count > 0 || _preloadedAsyncList.Count == 0) return; //从队列取出一个,异步加载 PreloadAssetObject plAssetObj = null; while (_preloadedAsyncList.Count > 0 && plAssetObj == null) { plAssetObj = _preloadedAsyncList.Dequeue(); if (_loadingList.ContainsKey(plAssetObj._assetName)) { _loadingList[plAssetObj._assetName]._isWeak = plAssetObj._isWeak; } else if (_loadedList.ContainsKey(plAssetObj._assetName)) { _loadedList[plAssetObj._assetName]._isWeak = plAssetObj._isWeak; plAssetObj = null; //如果当前没开始加载,重新选一个 } else { LoadAsync(plAssetObj._assetName, (AssetsLoadCallback)null); if (_loadingList.ContainsKey(plAssetObj._assetName)) { _loadingList[plAssetObj._assetName]._isWeak = plAssetObj._isWeak; } else if (_loadedList.ContainsKey(plAssetObj._assetName)) { _loadedList[plAssetObj._assetName]._isWeak = plAssetObj._isWeak; } } } }
上述代码说明几个逻辑和设定:
1.限制了加载队列为空时,才会取1个进行预加载
2.取预加载时,需要评定是否已经加载,所以用了while
3. LoadAsync(plAssetObj._assetName, (AssetsLoadCallback)null);后要改变强弱引用关系
预加载一般用于游戏启动的时候和进副本的时候,如果需要取消预加载,读者可以自己实现。
笔者一般是在游戏启动的时候需要常驻内存的资源,而又不想卡顿,所以慢慢在后台偷偷加载。
PS:笔者前面有一篇异步下载文件的博客,也可以实现偷偷下载哦【机智】
ResourcesLoadMgr资源加载
在上文中,看到ResourcesLoadMgr和EditorAssetLoadMgr出现很多次,内部代码是怎么样的呢?
using System.Collections.Generic; using System.IO; using UnityEngine; public class ResourcesLoadMgr { private static ResourcesLoadMgr _instance = null; public static ResourcesLoadMgr I { get { if (_instance == null) _instance = new ResourcesLoadMgr(); return _instance; } } private HashSet<string> _resourcesList; private ResourcesLoadMgr() { _resourcesList = new HashSet<string>(); #if UNITY_EDITOR ExportConfig(); #endif ReadConfig(); } #if UNITY_EDITOR private void ExportConfig() { string path = Application.dataPath + "/Resources/"; string[] files = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories); string txt = ""; foreach (var file in files) { if (file.EndsWith(".meta")) continue; string name = file.Replace(path, ""); name = name.Substring(0, name.LastIndexOf(".")); name = name.Replace("\\", "/"); txt += name + "\n"; } path = path + "FileList.bytes"; if (File.Exists(path)) File.Delete(path); File.WriteAllText(path, txt); } #endif private void ReadConfig() { TextAsset textAsset = Resources.Load<TextAsset>("FileList"); string txt = textAsset.text; txt = txt.Replace("\r\n", "\n"); foreach (var line in txt.Split('\n')) { if (string.IsNullOrEmpty(line)) continue; if (!_resourcesList.Contains(line)) _resourcesList.Add(line); } } public bool IsFileExist(string _assetName) { return _resourcesList.Contains(_assetName); } public ResourceRequest LoadAsync(string _assetName) { if (!_resourcesList.Contains(_assetName)) { Utils.LogError("EditorAssetLoadMgr No Find File " + _assetName); return null; } ResourceRequest request = Resources.LoadAsync(_assetName); return request; } public UnityEngine.Object LoadSync(string _assetName) { if (!_resourcesList.Contains(_assetName)) { Utils.LogError("EditorAssetLoadMgr No Find File " + _assetName); return null; } UnityEngine.Object asset = Resources.Load(_assetName); return asset; } public void Unload(UnityEngine.Object asset) { if (asset is GameObject) { return; } Resources.UnloadAsset(asset); asset = null; } }
Resource下的资源在Runtime情况下是无法判读有什么资源的,所以先要有个配置文件,可以记录所有资源列表。这样就有了ExportConfig()和ReadConfig()对列表的导出和读取。
通用的4个接口—— IsFileExist,LoadAsync,LoadSync和Unload,都只是简单地衔接了Unity提供的函数接口,具体看代码。
EditorAssetLoadMgr的代码跟ResourcesLoadMgr代码几乎一致,就不重复上代码了。
TIP:EditorAssetLoadMgr的关于AssetDataBase的说明
笔者EditorAssetLoadMgr下代码并没有实现AssetDataBase的加载接口,仍然使用的是Resources的加载接口,
因为AssetDataBase没有异步加载函数,但如果读者有需要,可以通过继承AsyncOperation的方式来模拟异步加载,
来实现真正的上述设计。
笔者用的取巧方案是:
Assets目录下有2个Resources目录,
一个路径是Assets/Resources/,
另一个是Assets/Editor/Resources/,
读取两个目录可以通用Resources.Load()接口,
打包时后者目录内资源在Editor下,不会进入包体,这是一个小技巧。
没说的小技巧
这篇文章,还有很多的小技巧,都在代码里,篇幅有限,就不说啦!
Ps:气不气,我就是懒得写了,啦啦啦!!!
完整代码——拿来即用
using System.Collections.Generic; using UnityEngine; public class AssetsLoadMgr { public delegate void AssetsLoadCallback(string name, UnityEngine.Object obj); private class AssetObject { public string _assetName; public int _lockCallbackCount; //记录回调当前数量,保证异步是下一帧回调 public List<AssetsLoadCallback> _callbackList = new List<AssetsLoadCallback>(); public int _instanceID; //asset的id public AsyncOperation _request; public UnityEngine.Object _asset; public bool _isAbLoad; public bool _isWeak = true; //是否是弱引用 public int _refCount; public int _unloadTick; //卸载使用延迟卸载,UNLOAD_DELAY_TICK_BASE + _unloadList.Count } private class PreloadAssetObject { public string _assetName; public bool _isWeak = true; //是否是弱引用 } private static AssetsLoadMgr _instance = null; public static AssetsLoadMgr I { get { if (_instance == null) _instance = new AssetsLoadMgr(); return _instance; } } public const int UNLOAD_DELAY_TICK_BASE = 60 * 60; //卸载最低延迟 private const int LOADING_INTERVAL_MAX_COUNT = 50; //每加载50个后,空闲时进行一次资源清理 private List<AssetObject> tempLoadeds = new List<AssetObject>(); //创建临时存储变量,用于提升性能 private Dictionary<string, AssetObject> _loadingList; private Dictionary<string, AssetObject> _loadedList; private Dictionary<string, AssetObject> _unloadList; private List<AssetObject> _loadedAsyncList; //异步加载,延迟回调 private Queue<PreloadAssetObject> _preloadedAsyncList; //异步预加载,空闲时加载 private Dictionary<int, AssetObject> _goInstanceIDList; //创建的实例对应的asset private int _loadingIntervalCount; //加载的间隔时间 private AssetsLoadMgr() { _loadingList = new Dictionary<string, AssetObject>(); _loadedList = new Dictionary<string, AssetObject>(); _unloadList = new Dictionary<string, AssetObject>(); _loadedAsyncList = new List<AssetObject>(); _preloadedAsyncList = new Queue<PreloadAssetObject>(); _goInstanceIDList = new Dictionary<int, AssetObject>(); } //判断资源是否存在,对打入atlas的图片无法判断,图片请用AtlasLoadMgr public bool IsAssetExist(string _assetName) { #if UNITY_EDITOR && !TEST_AB return EditorAssetLoadMgr.I.IsFileExist(_assetName); #else if (ResourcesLoadMgr.I.IsFileExist(_assetName)) return true; return AssetBundleLoadMgr.I.IsABExist(_assetName); #endif } //预加载,isWeak弱引用,true为使用过后会销毁,为false将不会销毁,慎用 public void PreLoad(string _assetName, bool _isWeak = true) { AssetObject assetObj = null; if (_loadedList.ContainsKey(_assetName)) assetObj = _loadedList[_assetName]; else if (_loadingList.ContainsKey(_assetName)) assetObj = _loadingList[_assetName]; //如果已经存在,改变其弱引用关系 if (assetObj != null) { assetObj._isWeak = _isWeak; if (_isWeak && assetObj._refCount == 0 && !_unloadList.ContainsKey(_assetName)) _unloadList.Add(_assetName, assetObj); return; } PreloadAssetObject plAssetObj = new PreloadAssetObject(); plAssetObj._assetName = _assetName; plAssetObj._isWeak = _isWeak; _preloadedAsyncList.Enqueue(plAssetObj); } //同步加载,一般用于小型文件,比如配置。 public UnityEngine.Object LoadSync(string _assetName) { if (!IsAssetExist(_assetName)) { Debug.LogError("AssetsLoadMgr Asset Not Exist " + _assetName); return null; } AssetObject assetObj = null; if (_loadedList.ContainsKey(_assetName)) { assetObj = _loadedList[_assetName]; assetObj._refCount++; return assetObj._asset; } else if (_loadingList.ContainsKey(_assetName)) { assetObj = _loadingList[_assetName]; if (assetObj._request != null) { if (assetObj._request is AssetBundleRequest) assetObj._asset = (assetObj._request as AssetBundleRequest).asset; //直接取,会异步变同步 else assetObj._asset = (assetObj._request as ResourceRequest).asset; assetObj._request = null; } else { #if UNITY_EDITOR && !TEST_AB assetObj._asset = EditorAssetLoadMgr.I.LoadSync(_assetName); #else if (assetObj._isAbLoad) { AssetBundle ab1 = AssetBundleLoadMgr.I.LoadSync(_assetName); assetObj._asset = ab1.LoadAsset(ab1.GetAllAssetNames()[0]); //异步转同步,需要卸载异步的引用计数 AssetBundleLoadMgr.I.Unload(_assetName); } else { assetObj._asset = ResourcesLoadMgr.I.LoadSync(_assetName); } #endif } if (assetObj._asset == null) {//提取的资源失败,从加载列表删除 _loadingList.Remove(assetObj._assetName); Debug.LogError("AssetsLoadMgr assetObj._asset Null " + assetObj._assetName); return null; } assetObj._instanceID = assetObj._asset.GetInstanceID(); _goInstanceIDList.Add(assetObj._instanceID, assetObj); _loadingList.Remove(assetObj._assetName); _loadedList.Add(assetObj._assetName, assetObj); _loadedAsyncList.Add(assetObj); //原先异步加载的,加入异步表 assetObj._refCount++; return assetObj._asset; } assetObj = new AssetObject(); assetObj._assetName = _assetName; #if UNITY_EDITOR && !TEST_AB assetObj._asset = EditorAssetLoadMgr.I.LoadSync(_assetName); #else if (AssetBundleLoadMgr.I.IsABExist(_assetName)) { assetObj._isAbLoad = true; Debug.LogWarning("AssetsLoadMgr LoadSync doubtful asset=" + assetObj._assetName); AssetBundle ab1 = AssetBundleLoadMgr.I.LoadSync(_assetName); assetObj._asset = ab1.LoadAsset(ab1.GetAllAssetNames()[0]); } else if (ResourcesLoadMgr.I.IsFileExist(_assetName)) { assetObj._isAbLoad = false; assetObj._asset = ResourcesLoadMgr.I.LoadSync(_assetName); } else return null; #endif if (assetObj._asset == null) {//提取的资源失败,从加载列表删除 Debug.LogError("AssetsLoadMgr assetObj._asset Null " + assetObj._assetName); return null; } assetObj._instanceID = assetObj._asset.GetInstanceID(); _goInstanceIDList.Add(assetObj._instanceID, assetObj); _loadedList.Add(_assetName, assetObj); assetObj._refCount = 1; return assetObj._asset; } //用于解绑回调 public void RemoveCallBack(string _assetName, AssetsLoadCallback _callFun) { if (_callFun == null) return; //对于不确定的回调,依据回调函数删除 if (string.IsNullOrEmpty(_assetName)) RemoveCallBackByCallBack(_callFun); AssetObject assetObj = null; if (_loadedList.ContainsKey(_assetName)) assetObj = _loadedList[_assetName]; else if (_loadingList.ContainsKey(_assetName)) assetObj = _loadingList[_assetName]; if (assetObj != null) { int index = assetObj._callbackList.IndexOf(_callFun); if (index >= 0) { assetObj._callbackList.RemoveAt(index); } } } //资源销毁,请保证资源销毁都要调用这个接口 public void Unload(UnityEngine.Object _obj) { if (_obj == null) return; int instanceID = _obj.GetInstanceID(); if (!_goInstanceIDList.ContainsKey(instanceID)) {//非从本类创建的资源,直接销毁即可 if (_obj is GameObject) UnityEngine.Object.Destroy(_obj); #if UNITY_EDITOR else if (UnityEditor.EditorApplication.isPlaying) { Debug.LogError("AssetsLoadMgr destroy NoGameObject name=" + _obj.name + " type=" + _obj.GetType().Name); } #else else Debug.LogError("AssetsLoadMgr destroy NoGameObject name=" + _obj.name+" type="+_obj.GetType().Name); #endif return; } var assetObj = _goInstanceIDList[instanceID]; if (assetObj._instanceID == instanceID) {//_obj不是GameObject,不销毁 assetObj._refCount--; } else {//error string errormsg = string.Format("AssetsLoadMgr Destroy error ! assetName:{0}", assetObj._assetName); Debug.LogError(errormsg); return; } if (assetObj._refCount < 0) { string errormsg = string.Format("AssetsLoadMgr Destroy refCount error ! assetName:{0}", assetObj._assetName); Debug.LogError(errormsg); return; } if (assetObj._refCount == 0 && !_unloadList.ContainsKey(assetObj._assetName)) { assetObj._unloadTick = UNLOAD_DELAY_TICK_BASE + _unloadList.Count; _unloadList.Add(assetObj._assetName, assetObj); } } //异步加载,即使资源已经加载完成,也会异步回调。 public void LoadAsync(string _assetName, AssetsLoadCallback _callFun) { if (!IsAssetExist(_assetName)) { Debug.LogError("AssetsLoadMgr Asset Not Exist " + _assetName); return; } AssetObject assetObj = null; if (_loadedList.ContainsKey(_assetName)) { assetObj = _loadedList[_assetName]; assetObj._callbackList.Add(_callFun); _loadedAsyncList.Add(assetObj); return; } else if (_loadingList.ContainsKey(_assetName)) { assetObj = _loadingList[_assetName]; assetObj._callbackList.Add(_callFun); return; } assetObj = new AssetObject(); assetObj._assetName = _assetName; assetObj._callbackList.Add(_callFun); #if UNITY_EDITOR && !TEST_AB _loadingList.Add(_assetName, assetObj); assetObj._request = EditorAssetLoadMgr.I.LoadAsync(_assetName); #else if (AssetBundleLoadMgr.I.IsABExist(_assetName)) { assetObj._isAbLoad = true; _loadingList.Add(_assetName, assetObj); AssetBundleLoadMgr.I.LoadAsync(_assetName, (AssetBundle _ab) => { if (_ab == null) { string errormsg = string.Format("LoadAsset request error ! assetName:{0}", assetObj._assetName); Debug.LogError(errormsg); _loadingList.Remove(_assetName); //重新添加,保证成功 for (int i = 0; i < assetObj._callbackList.Count; i++) { LoadAsync(assetObj._assetName, assetObj._callbackList[i]); } return; } if (_loadingList.ContainsKey(_assetName) && assetObj._request == null && assetObj._asset == null) { assetObj._request = _ab.LoadAssetAsync(_ab.GetAllAssetNames()[0]); } } ); } else if (ResourcesLoadMgr.I.IsFileExist(_assetName)) { assetObj._isAbLoad = false; _loadingList.Add(_assetName, assetObj); assetObj._request = ResourcesLoadMgr.I.LoadAsync(_assetName); } else return; #endif } //外部加载的资源,加入资源管理,给其他地方调用 public void AddAsset(string _assetName, UnityEngine.Object _asset) { var assetObj = new AssetObject(); assetObj._assetName = _assetName; assetObj._instanceID = _asset.GetInstanceID(); assetObj._asset = _asset; assetObj._refCount = 1; _loadedList.Add(assetObj._assetName, assetObj); _goInstanceIDList.Add(assetObj._instanceID, assetObj); } //针对特定资源需要添加引用计数,保证引用计数正确 public void AddAssetRef(string _assetName) { if (!_loadedList.ContainsKey(_assetName)) { Debug.LogError("AssetsLoadMgr AddAssetRef Error " + _assetName); return; } var assetObj = _loadedList[_assetName]; assetObj._refCount++; } private void RemoveCallBackByCallBack(AssetsLoadCallback _callFun) { foreach (var assetObj in _loadingList.Values) { if (assetObj._callbackList.Count == 0) continue; int index = assetObj._callbackList.IndexOf(_callFun); if (index >= 0) { assetObj._callbackList.RemoveAt(index); } } foreach (var assetObj in _loadedList.Values) { if (assetObj._callbackList.Count == 0) continue; int index = assetObj._callbackList.IndexOf(_callFun); if (index >= 0) { assetObj._callbackList.RemoveAt(index); } } } private void DoAssetCallback(AssetObject _assetObj) { if (_assetObj._callbackList.Count == 0) return; int count = _assetObj._lockCallbackCount; //先提取count,保证回调中有加载需求不加载 for (int i = 0; i < count; i++) { if (_assetObj._callbackList[i] != null) { _assetObj._refCount++; //每次回调,引用计数+1 try { _assetObj._callbackList[i](_assetObj._assetName, _assetObj._asset); } catch (System.Exception e) { Debug.LogError(e); } } } _assetObj._callbackList.RemoveRange(0, count); } private void DoUnload(AssetObject _assetObj) { #if UNITY_EDITOR && !TEST_AB EditorAssetLoadMgr.I.Unload(_assetObj._asset); #else if (_assetObj._isAbLoad) AssetBundleLoadMgr.I.Unload(_assetObj._assetName); else ResourcesLoadMgr.I.Unload(_assetObj._asset); #endif _assetObj._asset = null; if (_goInstanceIDList.ContainsKey(_assetObj._instanceID)) { _goInstanceIDList.Remove(_assetObj._instanceID); } } private void UpdateLoadedAsync() { if (_loadedAsyncList.Count == 0) return; int count = _loadedAsyncList.Count; for (int i = 0; i < count; i++) { //先锁定回调数量,保证异步成立 _loadedAsyncList[i]._lockCallbackCount = _loadedAsyncList[i]._callbackList.Count; } for (int i = 0; i < count; i++) { DoAssetCallback(_loadedAsyncList[i]); } _loadedAsyncList.RemoveRange(0, count); if (_loadingList.Count == 0 && _loadingIntervalCount > LOADING_INTERVAL_MAX_COUNT) {//在连续的大量加载后,强制调用一次gc _loadingIntervalCount = 0; //Resources.UnloadUnusedAssets(); //System.GC.Collect(); } } private void UpdateLoading() { if (_loadingList.Count == 0) return; //检测加载完的 tempLoadeds.Clear(); foreach (var assetObj in _loadingList.Values) { #if UNITY_EDITOR && !TEST_AB if (assetObj._request != null && assetObj._request.isDone) { assetObj._asset = (assetObj._request as ResourceRequest).asset; if (assetObj._asset == null) {//提取的资源失败,从加载列表删除 _loadingList.Remove(assetObj._assetName); Debug.LogError("AssetsLoadMgr assetObj._asset Null " + assetObj._assetName); break; } assetObj._instanceID = assetObj._asset.GetInstanceID(); _goInstanceIDList.Add(assetObj._instanceID, assetObj); assetObj._request = null; tempLoadeds.Add(assetObj); } #else if (assetObj._request != null && assetObj._request.isDone) { //加载完进行数据清理 if (assetObj._request is AssetBundleRequest) assetObj._asset = (assetObj._request as AssetBundleRequest).asset; else assetObj._asset = (assetObj._request as ResourceRequest).asset; if(assetObj._asset == null) {//提取的资源失败,从加载列表删除 _loadingList.Remove(assetObj._assetName); Debug.LogError("AssetsLoadMgr assetObj._asset Null " + assetObj._assetName); break; } assetObj._instanceID = assetObj._asset.GetInstanceID(); _goInstanceIDList.Add(assetObj._instanceID, assetObj); assetObj._request = null; tempLoadeds.Add(assetObj); } #endif } //回调中有可能对_loadingList进行操作,先移动 foreach (var assetObj in tempLoadeds) { _loadingList.Remove(assetObj._assetName); _loadedList.Add(assetObj._assetName, assetObj); _loadingIntervalCount++; //统计本轮加载的数量 //先锁定回调数量,保证异步成立 assetObj._lockCallbackCount = assetObj._callbackList.Count; } foreach (var assetObj in tempLoadeds) { DoAssetCallback(assetObj); } } private void UpdateUnload() { if (_unloadList.Count == 0) return; tempLoadeds.Clear(); foreach (var assetObj in _unloadList.Values) { if (assetObj._isWeak && assetObj._refCount == 0 && assetObj._callbackList.Count == 0) {//引用计数为0,且没有需要回调的函数,销毁 if (assetObj._unloadTick < 0) { _loadedList.Remove(assetObj._assetName); DoUnload(assetObj); tempLoadeds.Add(assetObj); } else assetObj._unloadTick--; } if (assetObj._refCount > 0 || !assetObj._isWeak) {//引用计数增加(销毁期间有加载) tempLoadeds.Add(assetObj); } } foreach (var assetObj in tempLoadeds) { _unloadList.Remove(assetObj._assetName); } } private void UpdatePreload() { if (_loadingList.Count > 0 || _preloadedAsyncList.Count == 0) return; //从队列取出一个,异步加载 PreloadAssetObject plAssetObj = null; while (_preloadedAsyncList.Count > 0 && plAssetObj == null) { plAssetObj = _preloadedAsyncList.Dequeue(); if (_loadingList.ContainsKey(plAssetObj._assetName)) { _loadingList[plAssetObj._assetName]._isWeak = plAssetObj._isWeak; } else if (_loadedList.ContainsKey(plAssetObj._assetName)) { _loadedList[plAssetObj._assetName]._isWeak = plAssetObj._isWeak; plAssetObj = null; //如果当前没开始加载,重新选一个 } else { LoadAsync(plAssetObj._assetName, (AssetsLoadCallback)null); if (_loadingList.ContainsKey(plAssetObj._assetName)) { _loadingList[plAssetObj._assetName]._isWeak = plAssetObj._isWeak; } else if (_loadedList.ContainsKey(plAssetObj._assetName)) { _loadedList[plAssetObj._assetName]._isWeak = plAssetObj._isWeak; } } } } public void Update() { UpdatePreload(); //预加载,空闲时启动 UpdateLoadedAsync(); //已经加载的异步回调 UpdateLoading(); //加载完成,回调 UpdateUnload(); //卸载需要销毁的资源 #if UNITY_EDITOR && !TEST_AB EditorAssetLoadMgr.I.Update(); #else AssetBundleLoadMgr.I.Update(); #endif } }
笔者想要的就是拿来就用,这一套代码衔接AssetBundle加载那篇文章,可以无缝嵌入任何游戏工程。
00