天天看點

Unity開發(五) Asset同步異步引用計數資源加載管理器

文章目錄

    • 前言
    • Asset加載架構
      • Unity的資源加載
      • 加載架構設計
      • Update才是王道
    • 開始異步加載
      • 加載
      • 回調
      • 解除安裝
    • 我還要同步加載
    • 預加載——其實我是空閑加載
    • ResourcesLoadMgr資源加載
    • 沒說的小技巧
    • 完整代碼——拿來即用

前言

本文是Asset資源加載,是文章(AssetBundle加載)的後續,讀者可以先閱讀上篇文章,再了解和使用這篇文章。

Asset加載架構

任何架構,都是從原理,到設計,然後實作,最後優化。

先講講原理。

Unity的資源加載

Unity最通用的資源加載方式,就三種

  1. Resources

    資源加載(Runtime和Editor模式)
  2. AssetBundle

    資源加載(Runtime和Editor模式)
  3. AssetDataBase

    資源加載(Editor模式)

大部分遊戲,為了熱更和效率考慮,都是——

  1. Runtime時,絕大部分資源使用

    AssetBundle

    ,極少數資源使用

    Resources

  2. 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; //異步預加載,空閑時加載
           

主體就三個隊列,

加載隊列

完成隊列

銷毀隊列

,跟大部分開發者的資源管理大同小異

  1. 當一個異步加載開始,建立Asset單元放入

    加載隊列

  2. 當異步加載結束,将Asset單元移入

    完成隊列

  3. 外部調用解除安裝,引用計數為0的Asset單元放入

    解除安裝隊列

  4. 解除安裝隊列

    中延期解除安裝時間結束,真正解除安裝

這邊還有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部分

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

這段代碼的疑惑,請看AssetBundle同步異步引用計數資源加載管理器的我要異步加載和同步加載一起用部分内容。

預加載——其實我是空閑加載

預加載,顧名思義,先加載到記憶體,需要的時候可以直接拿到結果,不用經曆加載不卡頓。

筆者這裡這個意義更寬泛一點,主要幾個含義:

  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加載那篇文章,可以無縫嵌入任何遊戲工程。