• ( 资源管理器03 )Prefab加载自动化管理引用计数管理器


    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

  • 相关阅读:
    AI芯片:高性能卷积计算中的数据复用
    矩阵乘法加速器的设计框架
    NVDLA中Winograd卷积的设计
    神经网络加速器应用实例:图像分类
    Simple TPU的设计和性能评估
    TPU中的指令并行和数据并行
    TPU中的脉动阵列及其实现
    动手写一个简单版的谷歌TPU
    利用Xilinx HLS实现LDPC译码器
    FPGA上如何求32个输入的最大值和次大值:分治
  • 原文地址:https://www.cnblogs.com/rollingyouandme/p/14563895.html
Copyright © 2020-2023  润新知