文章目录
-
- 前言
- Asset加载框架
-
- Unity的资源加载
- 加载框架设计
- Update才是王道
- 开始异步加载
-
- 加载
- 回调
- 卸载
- 我还要同步加载
- 预加载——其实我是空闲加载
- ResourcesLoadMgr资源加载
- 没说的小技巧
- 完整代码——拿来即用
前言
本文是Asset资源加载,是文章(AssetBundle加载)的后续,读者可以先阅读上篇文章,再理解和使用这篇文章。
Asset加载框架
任何框架,都是从原理,到设计,然后实现,最后优化。
先讲讲原理。
Unity的资源加载
Unity最通用的资源加载方式,就三种
-
资源加载(Runtime和Editor模式)Resources
-
资源加载(Runtime和Editor模式)AssetBundle
-
资源加载(Editor模式)AssetDataBase
大部分游戏,为了热更和效率考虑,都是——
- Runtime时,绝大部分资源使用
,极少数资源使用AssetBundle
Resources
- Editor时,使用
为主,AssetDataBase
为辅Resources
那么,我们的设计就要包含这三种加载方式。
Unity资源类型,按加载流程顺序,有三种
-
资源以压缩包文件存在(Resources目录下资源打成包体后也是以ab格式存在)AssetBundle
-
资源在内存中的存在格式Asset
-
针对Prefab导出的Asset,可实例化GameObject
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单元放入
加载队列
异步加载队列
实现的是——当资源已经加载完成,但需要异步回调时,延帧回调
具体代码实现,查看底部代码,具体就不详细说明啦。
TIP:为什么要有
异步加载队列
?
我们可以考虑,我们异步加载一个资源,资源已存在,直接运行回调函数。
而这个时候,外部代码很可能还没设定好必要的逻辑,代码逻辑实际希望的是异步回调,在运行完逻辑设定之后。
所以,为了保证外部逻辑的正确性,就算资源已经加载好,也要异步回调,所以必须要有
。
异步加载队列
开始异步加载
先来看一下加载单元的数据结构
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);
这段代码的疑惑,请看AssetBundle同步异步引用计数资源加载管理器的我要异步加载和同步加载一起用部分内容。
预加载——其实我是空闲加载
预加载,顾名思义,先加载到内存,需要的时候可以直接拿到结果,不用经历加载不卡顿。
笔者这里这个意义更宽泛一点,主要几个含义:
- 预加载为空闲时加载,优先级低于主加载
- 预加载不影响主加载,且异步加载
- 预加载资源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个进行预加载
- 取预加载时,需要评定是否已经加载,所以用了while
-
后要改变强弱引用关系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加载那篇文章,可以无缝嵌入任何游戏工程。