( 资源管理器 01 )Asset同步异步引用计数资源加载管理器

引用:

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加载,要内部衔接多种资源加载方式,对外部隐藏底层资源加载逻辑。
内部主要管理三种加载方式

 ( 资源管理器 01 )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。

内部是对资源方式的封装,外部接口提供了类似的通用接口
 ( 资源管理器 01 )Asset同步异步引用计数资源加载管理器

 

 

 

当然,本文底部源码里,还有很多函数实现了特定功能,例如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; //异步预加载,空闲时加载
 

 

( 资源管理器 01 )Asset同步异步引用计数资源加载管理器

 

 

 

主体就三个队列,加载队列、完成队列和销毁队列,跟大部分开发者的资源管理大同小异

当一个异步加载开始,创建Asset单元放入加载队列
当异步加载结束,将Asset单元移入完成队列
外部调用卸载,引用计数为0的Asset单元放入卸载队列
卸载队列中延期卸载时间结束,真正卸载
这边还有2个特殊队列,预加载队列和异步加载队列
预加载队列实现的是——当加载队列为空情况下,取1个创建Asset单元放入加载队列
异步加载队列实现的是——当资源已经加载完成,但需要异步回调时,延帧回调
 

( 资源管理器 01 )Asset同步异步引用计数资源加载管理器

 

 

 

( 资源管理器 01 )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部分

  1. 异步加载的资源在队列中,处理不同的队列逻辑
  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;
}
 
 

代码比较多,图解比较方便

 

( 资源管理器 01 )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

 

上一篇:Golang打包配置文件的实现示例


下一篇:微软行星云计算Planetary Computer——可视化数据集有哪些?