首页 > 技术文章 > ( 资源管理器03 )Prefab加载自动化管理引用计数管理器

rollingyouandme 2021-09-27 13:28 原文

Prefab实例化
我们都知道Prefab是一种存储格式,加载后是一个asset格式文件,需要实例化后才是一个可用的GameObject。
而如果每次我们都去加载asset,然后实例化,然后代码管理每一个GameObject的内存,这当然不是我们想要的。那我们想要的是什么?
我想要的是——自动化管理

提供一个接口,加载到指定GameObject
不用关心销毁,内存管理,实例克隆等操作
可以实现同步加载,异步加载和手动销毁
Unity自身肯定不包含上述功能,需要我们自己实现框架

自动销毁核心——ObjInfo
我们先不关心管理器内部逻辑,先关心如何实现自动销毁。

我们都知道Awake和OnDestroy函数,顾名思义,创建和销毁的时候调用,那么我们很简单地构造一个MonoBehaviour,实现如下
 

using UnityEngine;

public class ObjInfo : MonoBehaviour
{
    void OnDestroy()
    {
        //被动销毁,保证引用计数正确
        PrefabLoadMgr.I.Destroy(this.gameObject);
    }
}
 

 

我们可以对每个通过Prefab加载管理器创建的GameObject添加一个ObjInfo,这个GameObject在销毁的时候,会调用ObjInfo的OnDestroy方法,会通知管理器,销毁自身,,这样就实现了自动销毁

如果GameObject被克隆了呢,就会造成引用计数出错,所以还要修正引用计数的正确性,如下
 

 public int InstanceId = -1;
    public string AssetName = string.Empty;

    void Awake()
    {
        if (string.IsNullOrEmpty(AssetName)) return;
        //非空,说明通过克隆实例化,添加引用计数

        InstanceId = gameObject.GetInstanceID();
        PrefabLoadMgr.I.AddAssetRef(AssetName, this.gameObject);
    }
 

我们很开心地以为可以了,但Unity并没有按我们理想化的方式运行。

 

实例化——必须的active
Unity提供的Awake和OnDestroy函数,必须在GameObject节点被active的情况下,才会触发运行,也就是说新加载的节点GameObject, 必须挂在启用的GameObject节点下。
那这样我们就 先将节点创建在一个通用节点_assetParent.transform下,然后再把它移到目标节点上,我们就得到了如下代码
 

private GameObject InstanceAsset(PrefabObject _prefabObj, Transform _parent)
{
    GameObject go = GameObject.Instantiate(_prefabObj._asset, _assetParent.transform;) as GameObject;
    go.name = go.name.Replace("(Clone)", "");
    ObjInfo obgInfo = go.AddComponent<ObjInfo>();
    
    if(!go.activeSelf)
    {//保证GameObject active一次,ObjInfo才能触发Awake,未Awake的脚本不能触发OnDestroy
        go.SetActive(true);
        go.SetActive(false);
    }

    if (obgInfo != null)
    {
        obgInfo.InstanceId = go.GetInstanceID();
        obgInfo.AssetName = _prefabObj._assetName;
    }

    if (_parent != null)
        go.transform.SetParent(_parent);

    return go;
}
 

 

这样终于实现了自动销毁,不过笔者测试发现,go.transform.SetParent是一个非常耗时的操作,而我们大部分情况下,挂载的父节点都是active的,所以我们优化成可以挂载在目标节点下,直接挂载,不能再挂载在公用节点上,具体代码见下文。

Prefab加载管理器
加载管理器,在前面几篇描述了很多了,关键词是——加载单元,队列,外部接口。这一部分也不例外,只不过简单了很多。


 

 

 

 

加载单元

加载单元的数据结构

public class PrefabObject
{
    public string _assetName;

    public int _lockCallbackCount; //记录回调当前数量,保证异步是下一帧回调
    public List<PrefabLoadCallback> _callbackList = new List<PrefabLoadCallback>();
    public List<Transform> _callParentList = new List<Transform>();

    public UnityEngine.Object _asset;

    public int _refCount;
    public HashSet<int> _goInstanceIDSet = new HashSet<int>(); //实例化的GameObject引用列表
}
 

这里回调_callParentList需要保存目标节点,是为了性能考虑。_goInstanceIDSet用于记录当前已经创建和使用的节点。

队列

private Dictionary<string, PrefabObject> _loadedList;
private List<PrefabObject> _loadedAsyncList; //异步加载,延迟回调
private Dictionary<int, PrefabObject> _goInstanceIDList; //创建的实例对应的asset

 

这边相对于上一篇文章,简化了很多不必要的队列,只保留了加载队列异步加载队列。实现的内容与上一篇文章大同小异,就不重复阐述了,具体看代码。
这里增加了实例的hash保存,方便查找。

异步加载

异步加载代码如下

public void LoadAsync(string _assetName, PrefabLoadCallback _callFun, Transform _parent = null)
{
    PrefabObject prefabObj = null;
    if (_loadedList.ContainsKey(_assetName))
    {
        prefabObj = _loadedList[_assetName];
        prefabObj._callbackList.Add(_callFun);
        prefabObj._callParentList.Add(_parent);
        prefabObj._refCount++;

        if(prefabObj._asset != null) _loadedAsyncList.Add(prefabObj);
        return;
    }

    prefabObj = new PrefabObject();
    prefabObj._assetName = _assetName;
    prefabObj._callbackList.Add(_callFun);
    prefabObj._callParentList.Add(_parent);
    prefabObj._refCount = 1;

    _loadedList.Add(_assetName, prefabObj);
    
    AssetsLoadMgr.I.LoadAsync(_assetName, (string name, UnityEngine.Object obj) =>
    {
        prefabObj._asset = obj;

        prefabObj._lockCallbackCount = prefabObj._callbackList.Count;
        DoInstanceAssetCallback(prefabObj);
    }
    );
}
 

逻辑比较简单,讲几个关键点

1. 代码LoadAsync(string _assetName, PrefabLoadCallback _callFun, Transform _parent = null)这里需要三个参数,第三个也就是GameObject需要挂载的父节点,是为了加载性能。当然,如果不传_parent,也可以在回调中设置父节点。
2.  _loadedAsyncList当且仅当prefabObj._asset已经加载完成,需要异步回调才会启动,如果读者能够接受异步回调直接回调,可以去掉相关代码,直接回调
3. 引用计数与上文不同的是,有请求就增加,销毁就减少,是这边简化了逻辑,不需要大量队列操作,没必要提取到使用时
4. asset的加载依赖AssetsLoadMgr.I.LoadAsync,回调后,会将所有请求回调,具体看DoInstanceAssetCallback。

回调逻辑

private void DoInstanceAssetCallback(PrefabObject _prefabObj)
{
    if (_prefabObj._callbackList.Count == 0) return;

    //先将回掉提取保存,再回调,保证回调中加载和销毁不出错
    int count = _prefabObj._lockCallbackCount; 
    var callbackList = _prefabObj._callbackList.GetRange(0, count);
    var callParentList = _prefabObj._callParentList.GetRange(0, count);

    _prefabObj._lockCallbackCount = 0;
    _prefabObj._callbackList.RemoveRange(0, count);
    _prefabObj._callParentList.RemoveRange(0, count);

    for (int i = 0; i < count; i++)
    {
        if (callbackList[i] != null)
        {
            GameObject newObj = InstanceAsset(_prefabObj, callParentList[i]);//prefab需要实例化

            try
            {
                callbackList[i](_prefabObj._assetName, newObj);    
            }
            catch (System.Exception e)
            {
                Utils.LogError(e);
            }

            //如果回调之后,节点挂在默认节点下,认为该节点无效,销毁
            if (newObj.transform.parent == _assetParent.transform)
                Destroy(newObj);
        }
    }
}
 

 

这里面向大量外部加载代码,所以要考虑稳健性,做了很多极端考虑

1.回调前,先提取所有回调函数,保证回调中调用加载和销毁操作队列不出错(划重点)
2.回调时,使用try...catch...保证正常运行不出错
3.回调后,要考虑GameObject节点不使用后,回收节点问题


销毁

销毁分2种,销毁GameObject和销毁CallBack
 

public void Destroy(GameObject _obj)
    {
        if (_obj == null) return;
        
        int instanceID = _obj.GetInstanceID();
        if (!_goInstanceIDList.ContainsKey(instanceID))
        {//非从本类创建的资源,直接销毁即可
            UnityEngine.Object.Destroy(_obj);
            return;
        }

        var prefabObj = _goInstanceIDList[instanceID];
        if (prefabObj._goInstanceIDSet.Contains(instanceID))
        {//实例化的GameObject
            prefabObj._refCount--;
            prefabObj._goInstanceIDSet.Remove(instanceID);
            _goInstanceIDList.Remove(instanceID);
            UnityEngine.Object.Destroy(_obj);
        }
        if (prefabObj._refCount == 0)
        {
            _loadedList.Remove(prefabObj._assetName);

            AssetsLoadMgr.I.Unload(prefabObj._asset);
            prefabObj._asset = null;
        }
    }
 

 

销毁GameObject,就是简单地判断和减引用计数,比较简单。注意,这边能够直接通过引用计数销毁,是因为引用计数是在创建的时候就加上的。

再来看看销毁CallBack

public void RemoveCallBack(string _assetName, PrefabLoadCallback _callFun)
{
    if (_callFun == null) return;

    PrefabObject prefabObj = null;
    if (_loadedList.ContainsKey(_assetName))
        prefabObj = _loadedList[_assetName];

    if (prefabObj != null)
    {
        int index = prefabObj._callbackList.IndexOf(_callFun);
        if (index >= 0)
        {
            prefabObj._refCount--;
            prefabObj._callbackList.RemoveAt(index);
            prefabObj._callParentList.RemoveAt(index);

            if (index < prefabObj._lockCallbackCount)
            {//说明是加载回调过程中解绑回调,需要降低lock个数
                prefabObj._lockCallbackCount--;
            }
        }

        if (prefabObj._refCount == 0)
        {
            _loadedList.Remove(prefabObj._assetName);

            AssetsLoadMgr.I.Unload(prefabObj._asset);
            prefabObj._asset = null;
        }
    }
}
 

 

代码简单,关注一下

if (index < prefabObj._lockCallbackCount)
{//说明是加载回调过程中解绑回调,需要降低lock个数
    prefabObj._lockCallbackCount--;
}

这里考虑到,加载结束回调可能导致销毁CallBack的情况(比较极端啦),很可能影响原先回调队列的稳定,所以要保证批量回调的队列操作正确。

增加引用计数

public void AddAssetRef(string _assetName, GameObject _gameObject)
    {
        if (!_loadedList.ContainsKey(_assetName))
            return;

        PrefabObject prefabObj = _loadedList[_assetName];

        int instanceID = _gameObject.GetInstanceID();
        if(_goInstanceIDList.ContainsKey(instanceID))
        {
            string errormsg = string.Format("PrefabLoadMgr AddAssetRef error ! assetName:{0}", _assetName);
            Utils.LogError(errormsg);
            return;
        }

        prefabObj._refCount++;

        prefabObj._goInstanceIDSet.Add(instanceID);
        _goInstanceIDList.Add(instanceID, prefabObj);
    }
 

 

用于外部克隆导致Prefab引用计数增加的情况。外部克隆,一般用于UI列表类节点,大量重复特效等,还是比较常见的。

同步回调

讲完异步,还是必须要有同步的。

public GameObject LoadSync(string _assetName, Transform _parent = null)
{
    PrefabObject prefabObj = null;
    if (_loadedList.ContainsKey(_assetName))
    {
        prefabObj = _loadedList[_assetName];
        prefabObj._refCount++;

        if (prefabObj._asset == null)
        {//说明在异步加载中,需要不影响异步加载,加载后要释放
            prefabObj._asset = AssetsLoadMgr.I.LoadSync(_assetName);
            var newGo = InstanceAsset(prefabObj, _parent);
            AssetsLoadMgr.I.Unload(prefabObj._asset);
            prefabObj._asset = null;

            return newGo;
        }
        else return InstanceAsset(prefabObj, _parent);
    }

    prefabObj = new PrefabObject();
    prefabObj._assetName = _assetName;
    prefabObj._refCount = 1;
    prefabObj._asset = AssetsLoadMgr.I.LoadSync(_assetName);

    _loadedList.Add(_assetName, prefabObj);

    return InstanceAsset(prefabObj, _parent);
}
 

 

同步加载遇上异步加载,就要考虑未加载,正在加载,已加载。未加载和已加载都好处理,正在加载才难。

if (prefabObj._asset == null)
{//说明在异步加载中,需要不影响异步加载,加载后要释放
    prefabObj._asset = AssetsLoadMgr.I.LoadSync(_assetName);
    var newGo = InstanceAsset(prefabObj, _parent);
    AssetsLoadMgr.I.Unload(prefabObj._asset);
    prefabObj._asset = null;
    return newGo;
}
 

这里用了先加载,后销毁的方式,不影响异步加载的方式获得需要的GameObject,这种方式依托于AssetsLoadMgr的内部实现。AssetsLoadMgr内部实现了同步异步不冲突的处理方式,具体参见上一篇文章。

整合代码

好啦,让我们看看完整代码吧

using UnityEngine;

public class ObjInfo : MonoBehaviour
{
    public int InstanceId = -1;
    public string AssetName = string.Empty;

    void Awake()
    {
        if (string.IsNullOrEmpty(AssetName)) return;
        //非空,说明通过克隆实例化,添加引用计数

        InstanceId = gameObject.GetInstanceID();
        PrefabLoadMgr.I.AddAssetRef(AssetName, this.gameObject);
    }

    void OnDestroy()
    {
        //被动销毁,保证引用计数正确
        PrefabLoadMgr.I.Destroy(this.gameObject);
    }
}
 

 

using System.Collections.Generic;
using UnityEngine;

public class PrefabLoadMgr
{
    private static PrefabLoadMgr _instance = null;
    public static PrefabLoadMgr I
    {
        get
        {
            if (_instance == null) _instance = new PrefabLoadMgr();
            return _instance;
        }
    }

    public delegate void PrefabLoadCallback(string name, GameObject obj);

    public class PrefabObject
    {
        public string _assetName;

        public int _lockCallbackCount; //记录回调当前数量,保证异步是下一帧回调
        public List<PrefabLoadCallback> _callbackList = new List<PrefabLoadCallback>();
        public List<Transform> _callParentList = new List<Transform>();

        public UnityEngine.Object _asset;

        public int _refCount;
        public HashSet<int> _goInstanceIDSet = new HashSet<int>(); //实例化的GameObject引用列表
    }

    private Dictionary<string, PrefabObject> _loadedList;
    private List<PrefabObject> _loadedAsyncList; //异步加载,延迟回调
    private Dictionary<int, PrefabObject> _goInstanceIDList; //创建的实例对应的asset

    private GameObject _assetParent;

    private PrefabLoadMgr()
    {
        _loadedList = new Dictionary<string, PrefabObject>();
        _loadedAsyncList = new List<PrefabObject>();

        _goInstanceIDList = new Dictionary<int, PrefabObject>();
#if UNITY_EDITOR
        if (UnityEditor.EditorApplication.isPlaying)
        {
            _assetParent = new GameObject("AssetsList");
            GameObject.DontDestroyOnLoad(_assetParent);
        }
#else
        _assetParent = new GameObject("AssetsList");
        GameObject.DontDestroyOnLoad(_assetParent);
#endif
    }
    private GameObject InstanceAsset(PrefabObject _prefabObj, Transform _parent)
    {
        Transform tempParent = _parent;
        if (_parent == null || _parent.gameObject == null || !_parent.gameObject.activeInHierarchy)
            tempParent = _assetParent.transform;

        GameObject go = GameObject.Instantiate(_prefabObj._asset, tempParent) as GameObject;
        go.name = go.name.Replace("(Clone)", "");
        int instanceID = go.GetInstanceID();

        ObjInfo obgInfo = go.AddComponent<ObjInfo>();
        
        if(!go.activeSelf)
        {//保证GameObject active一次,ObjInfo才能触发Awake,未Awake的脚本不能触发OnDestroy
            go.SetActive(true);
            go.SetActive(false);
        }
        
        if (obgInfo != null)
        {
            obgInfo.InstanceId = instanceID;
            obgInfo.AssetName = _prefabObj._assetName;
        }

        _prefabObj._goInstanceIDSet.Add(instanceID);
        _goInstanceIDList.Add(instanceID, _prefabObj);

        if (_parent != null)
            go.transform.SetParent(_parent);

        return go;
    }

    private void DoInstanceAssetCallback(PrefabObject _prefabObj)
    {
        if (_prefabObj._callbackList.Count == 0) return;

        //先将回掉提取保存,再回调,保证回调中加载和销毁不出错
        int count = _prefabObj._lockCallbackCount; 
        var callbackList = _prefabObj._callbackList.GetRange(0, count);
        var callParentList = _prefabObj._callParentList.GetRange(0, count);

        _prefabObj._lockCallbackCount = 0;
        _prefabObj._callbackList.RemoveRange(0, count);
        _prefabObj._callParentList.RemoveRange(0, count);

        for (int i = 0; i < count; i++)
        {
            if (callbackList[i] != null)
            {
                GameObject newObj = InstanceAsset(_prefabObj, callParentList[i]);//prefab需要实例化

                try
                {
                    callbackList[i](_prefabObj._assetName, newObj);    
                }
                catch (System.Exception e)
                {
                    Utils.LogError(e);
                }

                //如果回调之后,节点挂在默认节点下,认为该节点无效,销毁
                if (newObj.transform.parent == _assetParent.transform)
                    Destroy(newObj);
            }
        }
    }


    public GameObject LoadSync(string _assetName, Transform _parent = null)
    {
        PrefabObject prefabObj = null;
        if (_loadedList.ContainsKey(_assetName))
        {
            prefabObj = _loadedList[_assetName];
            prefabObj._refCount++;

            if (prefabObj._asset == null)
            {//说明在异步加载中,需要不影响异步加载,加载后要释放
                prefabObj._asset = AssetsLoadMgr.I.LoadSync(_assetName);
                var newGo = InstanceAsset(prefabObj, _parent);
                AssetsLoadMgr.I.Unload(prefabObj._asset);
                prefabObj._asset = null;

                return newGo;
            }
            else return InstanceAsset(prefabObj, _parent);
        }

        prefabObj = new PrefabObject();
        prefabObj._assetName = _assetName;
        prefabObj._refCount = 1;
        prefabObj._asset = AssetsLoadMgr.I.LoadSync(_assetName);

        _loadedList.Add(_assetName, prefabObj);

        return InstanceAsset(prefabObj, _parent);
    }

    public void LoadAsync(string _assetName, PrefabLoadCallback _callFun, Transform _parent = null)
    {
        PrefabObject prefabObj = null;
        if (_loadedList.ContainsKey(_assetName))
        {
            prefabObj = _loadedList[_assetName];
            prefabObj._callbackList.Add(_callFun);
            prefabObj._callParentList.Add(_parent);
            prefabObj._refCount++;

            if(prefabObj._asset != null) _loadedAsyncList.Add(prefabObj);
            return;
        }

        prefabObj = new PrefabObject();
        prefabObj._assetName = _assetName;
        prefabObj._callbackList.Add(_callFun);
        prefabObj._callParentList.Add(_parent);
        prefabObj._refCount = 1;

        _loadedList.Add(_assetName, prefabObj);
        
        AssetsLoadMgr.I.LoadAsync(_assetName, (string name, UnityEngine.Object obj) =>
        {
            prefabObj._asset = obj;

            prefabObj._lockCallbackCount = prefabObj._callbackList.Count;
            DoInstanceAssetCallback(prefabObj);
        }
        );
    }

    public void Destroy(GameObject _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)
            {
                Utils.LogError("PrefabLoadMgr destroy NoGameObject name=" + _obj.name + " type=" + _obj.GetType().Name);
            }
#else
            else Utils.LogError("PrefabLoadMgr destroy NoGameObject name=" + _obj.name + " type=" + _obj.GetType().Name);
#endif
            return;
        }

        var prefabObj = _goInstanceIDList[instanceID];
        if (prefabObj._goInstanceIDSet.Contains(instanceID))
        {//实例化的GameObject
            prefabObj._refCount--;
            prefabObj._goInstanceIDSet.Remove(instanceID);
            _goInstanceIDList.Remove(instanceID);
            UnityEngine.Object.Destroy(_obj);
        }
        else
        {//error
            string errormsg = string.Format("PrefabLoadMgr Destroy error ! assetName:{0}", prefabObj._assetName);
            Utils.LogError(errormsg);
            return;
        }

        if (prefabObj._refCount < 0)
        {
            string errormsg = string.Format("PrefabLoadMgr Destroy refCount error ! assetName:{0}", prefabObj._assetName);
            Utils.LogError(errormsg);
            return;
        }

        if (prefabObj._refCount == 0)
        {
            _loadedList.Remove(prefabObj._assetName);

            AssetsLoadMgr.I.Unload(prefabObj._asset);
            prefabObj._asset = null;
        }
    }

    //用于解绑回调
    public void RemoveCallBack(string _assetName, PrefabLoadCallback _callFun)
    {
        if (_callFun == null) return;

        PrefabObject prefabObj = null;
        if (_loadedList.ContainsKey(_assetName))
            prefabObj = _loadedList[_assetName];

        if (prefabObj != null)
        {
            int index = prefabObj._callbackList.IndexOf(_callFun);
            if (index >= 0)
            {
                prefabObj._refCount--;
                prefabObj._callbackList.RemoveAt(index);
                prefabObj._callParentList.RemoveAt(index);

                if (index < prefabObj._lockCallbackCount)
                {//说明是加载回调过程中解绑回调,需要降低lock个数
                    prefabObj._lockCallbackCount--;
                }
            }

            if (prefabObj._refCount < 0)
            {
                string errormsg = string.Format("PrefabLoadMgr Destroy refCount error ! assetName:{0}", prefabObj._assetName);
                Utils.LogError(errormsg);
                return;
            }

            if (prefabObj._refCount == 0)
            {
                _loadedList.Remove(prefabObj._assetName);

                AssetsLoadMgr.I.Unload(prefabObj._asset);
                prefabObj._asset = null;
            }
        }


    }

    // 用于外部实例化,增加引用计数
    public void AddAssetRef(string _assetName, GameObject _gameObject)
    {
        if (!_loadedList.ContainsKey(_assetName))
            return;

        PrefabObject prefabObj = _loadedList[_assetName];

        int instanceID = _gameObject.GetInstanceID();
        if(_goInstanceIDList.ContainsKey(instanceID))
        {
            string errormsg = string.Format("PrefabLoadMgr AddAssetRef error ! assetName:{0}", _assetName);
            Utils.LogError(errormsg);
            return;
        }

        prefabObj._refCount++;

        prefabObj._goInstanceIDSet.Add(instanceID);
        _goInstanceIDList.Add(instanceID, prefabObj);
    }

    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++)
        {
            DoInstanceAssetCallback(_loadedAsyncList[i]);
        }
        _loadedAsyncList.RemoveRange(0, count);
    }


    public void Update()
    {
        UpdateLoadedAsync();
    }
}
 

 


 

00

推荐阅读