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

     

    我们能在后续代码中看到很多如下的写法

    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部分

    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;
    }
     
     

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

    逻辑不复杂,就是分类讨论。

    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 + "
    ";
            }
    
            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("
    ", "
    ");
    
            foreach (var line in txt.Split('
    '))
            {
                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

  • 相关阅读:
    Go语言学习之1 基本概念、环境搭建、第一个Go程序
    go环境搭建
    go 圣经阅读笔记之-入门
    RabbitMQ
    图片选择并使用base64展示
    关于c# hashtable的一个注意点
    js image to base64 摘录
    linq to sql 获取sql与参数添加到日志中
    时钟的实现
    无限级菜单的实现
  • 原文地址:https://www.cnblogs.com/rollingyouandme/p/14563684.html
Copyright © 2020-2023  润新知