资源加载

目录

[TOC]

一、写在前面

1.使用版本

Unity版本:2018.3.Of2

二、AssetBundle打包管理

1.打包策略

  • 设置编辑器工具统一设置AB包名、路径
  • 根据依赖关系生成不冗余AB包
  • 基于Asset的全路径生成自己的依赖关系表
  • 根据自己的依赖关系表加载AB包,编辑器下直接加载资源

优点:

  • 不需要在打包编辑器可以直接运行游戏
  • 不会产生冗余AB包
  • 文件夹或文件AB包设置简单方便容易管理

缺点:

  • 长时间未打包的情况下,打包的时候时间较长

2.定义打包配置表

分为两种类型的打包:

  • 类型一:基于文件夹下所有单个文件进行打包(用于打包Prefab)

    记录该情况下的所有文件夹的路径,遍历文件夹下的Prefab,逐一打包。

  • 类型二:基于文件夹进行打包(用于打包Shader、Sound…)

    记录文件夹的Info:AB包名、文件夹路径

    [CreateAssetMenu(fileName = "ABConfig", menuName = "CreateABConfig", order = 1)]
    public class EditABConfig : ScriptableObject
    {
        //路径:基于Assets下的路径(eg:Assets/xxx/xxx...)

        //类型一:基于文件夹下所有单个文件进行打包(用于打包Prefab)
        //记录该情况下的所有文件夹的路径
        //遍历文件夹下的Prefab,逐一打包
        public List<string> PrefabsFolderPathList = new List<string>();

        //类型二:基于文件夹进行打包(用于打包Shader、Sound...)
        //记录文件夹的Info:AB包名、文件夹路径
        public List<FilesFolderInfo> FilesFolderInfoList = new List<FilesFolderInfo>();

        [System.Serializable]
        public struct FilesFolderInfo
        {
            public string ABName;
            public string Path;
        }
    }

创建出来的ABConfig如下图:

picture0

3.打包

流程思路:

  • Step1.设置AB包配置工具。

    AB包打包分为两种类型的AB包:

    类型一:基于文件夹下所有单个文件进行打包,一般用于打包Perfab,以Prefab名为AB包名,所以Prefab名需要唯一。

    类型二:基于文件夹进行打包,以该文件夹的名称作为AB包名,该文件夹下所有文件打到同一个AB包中,一般用于打包sound、shader等。

  • Step2.读取AB包配置文件,将类型二、类型一的相关信息缓存到两个字典中,通过缓存过滤用的列表来防止冗余AB包

  • Step3.通过Step2中缓存好的两个字典来设置AB包名

  • Step4.打包:

    • 缓存需要动态加载的AB包的信息
    • 删除多余的AB包
    • 生成AB包配置表(以供加载AB包)
    • 打包
  • Step5.清除AB包名(防止meta文件变动导致Git需要提交)

  • Step6.刷新编辑器,完成打包

使用打包工具步骤:

  • Step1.创建ABConfig,完成两种类型的AB包路径配置。

    类型一:基于文件夹下所有单个文件进行打包,一般用于打包Perfab,以Prefab名为AB包名,所以Prefab名需要唯一。

    类型二:基于文件夹进行打包,以该文件夹的名称作为AB包名,该文件夹下所有文件打到同一个AB包中,一般用于打包sound、shader等。

    例如: 2.定义打包配置表 中的图所示。

  • Step2.(可选)在BuildAssetBundleEditor中设置AB包、配置表路径

  • Step3.打包

using System.IO;
using System.Xml.Serialization;
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters.Binary;

namespace ResLoadFramework
{
    public class BuildAssetBundleEditor
    {
        #region Config
        private const string PATH_ABCONFIG = "Assets/Configs/ABConfig.asset";
        private static string PATH_AB = Application.streamingAssetsPath;
        private static string PATH_AB_CONFIG_XML = Application.dataPath + "/Configs/AssetBundleConfig.xml";
        private static string PATH_AB_CONFIG_BINARY = Application.dataPath + "/Resources/Configs/AssetBundleConfig.bytes";
        /// <summary>
        /// AB配置表名称(用于打包、加载AB配置表)
        /// </summary>
        private static string CONFIG_ABNAME = "assetbundleconfig";
        #endregion
        /// <summary>
        /// 缓存  类型二 —— 基于文件夹进行打包的
        /// Key:ABName
        /// Value:文件夹路径
        /// </summary>
        private static Dictionary<string, string> mFilesFolderDict = new Dictionary<string, string>();
        /// <summary>
        /// 缓存  类型一 —— 基于文件夹下所有单个文件进行打包
        /// Key:ABName(Prefab名字)
        /// Value:依赖项路径(含后缀)
        /// </summary>
        private static Dictionary<string, List<string>> mPrefabDict = new Dictionary<string, List<string>>();
        /// <summary>
        /// 过滤用的List(缓存  类型二的文件夹路径、类型一的Prefab的依赖项路径(含后缀))
        /// 用于保证AB包无冗余
        /// </summary>
        private static List<string> mFliterList = new List<string>();
        /// <summary>
        /// 缓存需要动态加载的AB包全路径
        /// 用于优化AB配置表,不需要动态加载的就不用写入表内
        /// </summary>
        private static List<string> mDynamicLoadABPathList = new List<string>();

        [MenuItem("自定义工具/打AB包", false, 0)]
        public static void Build()
        {
            mFilesFolderDict.Clear();//清空缓存
            mPrefabDict.Clear();
            mFliterList.Clear();
            mDynamicLoadABPathList.Clear();
            CreateDirectory(PATH_AB);
            PrepareDirectory();//为(xml、二进制)配置表创建文件夹
            EditABConfig config = LoadAsset<EditABConfig>(PATH_ABCONFIG);//加载配置
            //缓存类型二:
            foreach (var filesFolderInfo in config.FilesFolderInfoList)
            {
                if (mFilesFolderDict.ContainsKey(filesFolderInfo.ABName))
                {
                    Debug.LogErrorFormat("AB包名重复!AB包名:{0}", filesFolderInfo.ABName);
                }
                else
                {
                    mFilesFolderDict.Add(filesFolderInfo.ABName, filesFolderInfo.Path);
                    //缓存过滤用的List(放入类型二的文件夹路径)
                    mFliterList.Add(filesFolderInfo.Path);
                    //类型二的全部需要动态加载
                    mDynamicLoadABPathList.Add(filesFolderInfo.Path);
                }
            }

            //缓存类型一:
            string[] guidArray = AssetDatabase.FindAssets("t:Prefab", config.PrefabsFolderPathList.ToArray());
            for (int i = 0; i < guidArray.Length; i++)
            {
                string prefabPath = AssetDatabase.GUIDToAssetPath(guidArray[i]);//含后缀
                EditorUtility.DisplayProgressBar("查找Prefab路径", string.Format("路径:{0}", prefabPath), i * 1.0f / guidArray.Length);
                //所有Prefab都需要动态加载
                mDynamicLoadABPathList.Add(prefabPath);
                //防止:1.类型二中混合有类型一;2.因类型一中Prefab和Prefab依赖项之间存在重复而导致的AB包冗余
                if (ContainFliterListPath(prefabPath) == false)
                {
                    //获取Prefab的依赖文件路径(含后缀,含自身)
                    string[] dependencies = AssetDatabase.GetDependencies(prefabPath);
                    List<string> dependencyList = new List<string>();//缓存当前Prefab依赖项路径(含后缀)
                    //剔除  和过滤List中重复的、脚本
                    for (int j = 0; j < dependencies.Length; j++)
                    {
                        if (ContainFliterListPath(dependencies[j]) == false && dependencies[j].EndsWith(".cs") == false)
                        {
                            dependencyList.Add(dependencies[j]);
                            //更新过滤用的List(放入类型一Prefab的依赖项的路径(含后缀))
                            mFliterList.Add(dependencies[j]);
                        }
                    }
                    GameObject prefabGo = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
                    if (mPrefabDict.ContainsKey(prefabGo.name))
                    {
                        Debug.LogErrorFormat("存在重复的Prefab名称:{0}", prefabGo.name);
                    }
                    else
                    {
                        mPrefabDict.Add(prefabGo.name, dependencyList);//缓存  类型一
                    }
                }
            }
            //mPrefabDict.Keys.ToList().ForEach(key => Debug.Log(key));//打印所有Prefab名

            #region 设置AB包名
            foreach (var filesFolder in mFilesFolderDict)
            {
                SetABName(filesFolder.Key, filesFolder.Value);
            }
            foreach (var prefabPackage in mPrefabDict)
            {
                SetABName(prefabPackage.Key, prefabPackage.Value);
            }
            #endregion

            //打包
            BuildAssetBundle();

            //清除AB包名(防止meta文件变动导致Git需要提交)
            DeleteABNames();

            AssetDatabase.Refresh();
            //AB配置表打包
            BuildABCfg();
            AssetDatabase.Refresh();
            EditorUtility.ClearProgressBar();
            Debug.Log("打包完成!");
        }
        /// <summary>
        /// AB配置表打包
        /// </summary>
        private static void BuildABCfg()
        {
            //设置AB包名
            string path = "Assets" + PATH_AB_CONFIG_BINARY.Replace(Application.dataPath, string.Empty);
            SetABName(CONFIG_ABNAME, path);
            //打包
            //使用LZ4压缩,压缩率不如LZMA;可以加载指定资源,无需一次解压全部资源
            BuildPipeline.BuildAssetBundles(PATH_AB, BuildAssetBundleOptions.ChunkBasedCompression, EditorUserBuildSettings.activeBuildTarget);
            //清除AB包名(防止meta文件变动导致Git需要提交)
            DeleteABNames();
            Debug.LogFormat("{0}打包完成!", CONFIG_ABNAME);
        }
        /// <summary>
        /// 打包
        /// </summary>
        private static void BuildAssetBundle()
        {
            string[] allABNames = AssetDatabase.GetAllAssetBundleNames();
            //Key:全路径;Value:包名(所有的AB包信息)
            Dictionary<string, string> abInfoDict = new Dictionary<string, string>();
            for (int i = 0; i < allABNames.Length; i++)
            {
                string[] paths = AssetDatabase.GetAssetPathsFromAssetBundle(allABNames[i]);
                for (int j = 0; j < paths.Length; j++)
                {
                    if (paths[j].EndsWith(".cs"))
                        continue;
                    Debug.LogFormat("AB包 {0} 内资源全路径:{1}", allABNames[i], paths[j]);
                    if (ContainedByDynamicLoadABPathList(paths[j]) == true)
                    {
                        abInfoDict.Add(paths[j], allABNames[i]);//需要动态加载的AB包信息
                    }
                }
            }

            //删除多余的AB包
            DeleteRedundantAB();

            //写入AB包配置表(以供加载AB包)
            WriteABConfig(abInfoDict);

            //使用LZ4压缩,压缩率不如LZMA;可以加载指定资源,无需一次解压全部资源
            BuildPipeline.BuildAssetBundles(PATH_AB, BuildAssetBundleOptions.ChunkBasedCompression, EditorUserBuildSettings.activeBuildTarget);
        }
        /// <summary>
        /// 写入AB包配置表
        /// </summary>
        /// <param Key:全路径;Value:包名="abInfoDict"></param>
        private static void WriteABConfig(Dictionary<string, string> abInfoDict)
        {
            AssetBundleConfig config = new AssetBundleConfig();
            config.ABDataList = new List<ABData>();
            foreach (var abInfo in abInfoDict)
            {
                ABData abData = new ABData();
                abData.Path = abInfo.Key;
                abData.Crc = CRC32.GetCrc32(abInfo.Key);
                abData.ABName = abInfo.Value;
                //AssetName带后缀
                abData.AssetName = abInfo.Key.Remove(0, abInfo.Key.LastIndexOf("/") + 1);
                abData.ABDependencies = new List<string>();//依赖项AB包名
                //获取当前AB包所有依赖项
                string[] dependencies = AssetDatabase.GetDependencies(abInfo.Key);
                for (int i = 0; i < dependencies.Length; i++)
                {
                    string dependency = dependencies[i];//依赖项全路径
                    //AB包的依赖项不包含AB包自身、脚本
                    if (dependency == abInfo.Key || dependency.EndsWith(".cs"))
                        continue;
                    string abName = "";//依赖项AB包名
                    if (abInfoDict.TryGetValue(dependency, out abName))
                    {
                        //AB包的依赖项不包含AB包自身
                        if (abName == abInfo.Value)
                            continue;
                        //防止重复添加依赖项
                        if (abData.ABDependencies.Contains(abName) == false)
                        {
                            abData.ABDependencies.Add(abName);
                        }
                    }
                }
                config.ABDataList.Add(abData);
            }
            //写入Xml
            ToXml(config, PATH_AB_CONFIG_XML);
            //写入二进制
            //优化:二进制配置表文件不需要Path,加载只需要Crc即可
            foreach (ABData abData in config.ABDataList)
            {
                abData.Path = "";
            }
            ToBinary(config, PATH_AB_CONFIG_BINARY);
        }
        private static void ToXml<T>(T obj, string fullPath)
        {
            CheckFileExist(fullPath);
            //Step1:文件流——创建xml文件
            FileStream fs = new FileStream(fullPath, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite);
            //Step2:写入流
            StreamWriter sw = new StreamWriter(fs, System.Text.Encoding.UTF8);
            //Step3:创建xml序列化器
            XmlSerializer xmlSerializer = new XmlSerializer(obj.GetType());
            //Step4:序列化对象到写入流中
            xmlSerializer.Serialize(sw, obj);
            //Step5:关闭流
            sw.Close();
            fs.Close();
        }
        private static void ToBinary<T>(T obj, string fullPath)
        {
            CheckFileExist(fullPath);
            //Step1:文件流——创建二进制文件
            FileStream fs = new FileStream(fullPath, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite);
            //Step2:创建二进制序列化器
            BinaryFormatter binaryFormatter = new BinaryFormatter();
            //Step3:序列化对象到文件流中
            binaryFormatter.Serialize(fs, obj);
            //Step4:关闭流
            fs.Close();
        }
        private static void CheckFileExist(string fullPath)
        {
            //存在则删除
            if (File.Exists(fullPath)) File.Delete(fullPath);
        }
        /// <summary>
        /// 删除多余的AB包
        /// </summary>
        private static void DeleteRedundantAB()
        {
            string[] allABNames = AssetDatabase.GetAllAssetBundleNames();
            DirectoryInfo directoryInfo = new DirectoryInfo(PATH_AB);
            //获取AB文件夹下所有文件的fileInfo
            FileInfo[] fileInfos = directoryInfo.GetFiles("*", SearchOption.AllDirectories);
            for (int i = 0; i < fileInfos.Length; i++)
            {
                //meta文件可以不清除
                if (ContainedByAB(fileInfos[i].Name, allABNames) || fileInfos[i].Name.EndsWith(".meta"))
                    continue;
                else//可以清除
                {
                    Debug.LogFormat("AB包 {0} 已经删除或改名", fileInfos[i].Name);
                    if (File.Exists(fileInfos[i].FullName))
                    {
                        File.Delete(fileInfos[i].FullName);
                    }
                }
            }
        }
        /// <summary>
        /// 当前file文件是否已经包含在AB包文件内
        /// 若不是则可以清除
        /// </summary>
        private static bool ContainedByAB(string fileName, string[] allABNames)
        {
            foreach (var abName in allABNames)
            {
                if (fileName == abName)
                    return true;
            }
            return false;
        }
        /// <summary>
        /// 设置类型二(基于文件夹进行打包的)的AB包名
        /// path:文件夹路径(Assets/xxx...)
        /// </summary>
        private static void SetABName(string abName, string path)
        {
            AssetImporter assetImporter = AssetImporter.GetAtPath(path);
            if (assetImporter == null)
                Debug.LogErrorFormat("AB包路径出错,路径:{0}", path);
            else
                assetImporter.assetBundleName = abName;
        }
        /// <summary>
        /// 设置类型一(prefab及其依赖项)的AB包名
        /// 依赖项是经过类型二剔除后的
        /// </summary>
        private static void SetABName(string abName, List<string> dependenciesName)
        {
            for (int i = 0; i < dependenciesName.Count; i++)
            {
                SetABName(abName, dependenciesName[i]);
            }
        }
        private static T LoadAsset<T>(string path)
            where T : ScriptableObject
        {
            return AssetDatabase.LoadAssetAtPath<T>(path);
        }
        /// <summary>
        /// 是否包含过滤List中的路径
        /// 用于剔除冗余AB包
        /// </summary>
        private static bool ContainFliterListPath(string path)
        {
            for (int i = 0; i < mFliterList.Count; i++)
            {
                //path.Replace(mFliterList[i], string.Empty)[0] == '/'是用于避免:
                //Assets/GameData 和 Assets/GameDataxxx/atk.prefab
                //第二个包含第一个,但第二个和第一个不属于同一个文件夹
                if (path == mFliterList[i] || path.Contains(mFliterList[i]) && path.Replace(mFliterList[i], string.Empty)[0] == '/')
                    return true;
            }
            return false;
        }
        /// <summary>
        /// 路径是否包含于  需要动态加载的AB路径List
        /// 只有需要动态加载的AB的Path才需要写入AB配置表
        /// </summary>
        private static bool ContainedByDynamicLoadABPathList(string fullPath)
        {
            for (int i = 0; i < mDynamicLoadABPathList.Count; i++)
            {
                //类型一为==;类型二为Contains
                if (fullPath.Contains(mDynamicLoadABPathList[i]) && (fullPath.Replace(mDynamicLoadABPathList[i], string.Empty) == string.Empty || fullPath.Replace(mDynamicLoadABPathList[i], string.Empty)[0] == '/'))
                    return true;
            }
            return false;
        }
        /// <summary>
        /// 清除AB包名(防止meta文件变动导致Git需要提交)
        /// </summary>
        private static void DeleteABNames()
        {
            string[] oldABNames = AssetDatabase.GetAllAssetBundleNames();
            for (int i = 0; i < oldABNames.Length; i++)
            {
                AssetDatabase.RemoveAssetBundleName(oldABNames[i], true);
                EditorUtility.DisplayProgressBar("清除AB包名", string.Format("AB包名:{0}", oldABNames[i]), i * 1.0f / oldABNames.Length);
            }
        }
        private static void CreateDirectory(string folderFullPath)
        {
            if (Directory.Exists(folderFullPath) == false)
            {
                Directory.CreateDirectory(folderFullPath);
                Debug.LogFormat("创建文件夹,文件夹路径:{0}", folderFullPath);
            }
        }
        private static void PrepareDirectory()
        {
            int xmlIndex = PATH_AB_CONFIG_XML.LastIndexOf("/");
            CreateDirectory(PATH_AB_CONFIG_XML.Remove(xmlIndex, PATH_AB_CONFIG_XML.Length - xmlIndex));
            int binaryIndex = PATH_AB_CONFIG_BINARY.LastIndexOf("/");
            CreateDirectory(PATH_AB_CONFIG_BINARY.Remove(binaryIndex, PATH_AB_CONFIG_BINARY.Length - binaryIndex));
        }
    }
}

补充:

  • 设置mFliterList的目的是用于防止AB包冗余的:先缓存类型二的,再缓存类型一;在缓存类型一的时候,类型一的依赖项需经过mFliterList过滤后才可加入缓存中。

4.AB包配置表

using System;
using System.Xml.Serialization;
using System.Collections.Generic;

namespace ResLoadFramework
{
    /// <summary>
    /// AB包配置
    /// </summary>
    [Serializable]
    public class AssetBundleConfig
    {
        [XmlElement("ABDataList")]
        public List<ABData> ABDataList { get; set; }
    }

    /// <summary>
    /// AB包数据
    /// </summary>
    [Serializable]
    public class ABData
    {
        [XmlAttribute("Path")]
        public string Path { get; set; }
        /// <summary>
        /// 唯一标识(精度低于MD5)
        /// </summary>
        [XmlAttribute("Crc")]
        public uint Crc { get; set; }
        [XmlAttribute("ABName")]
        public string ABName { get; set; }
        [XmlAttribute("AssetName")]
        public string AssetName { get; set; }
        [XmlElement("ABDependencies")]
        public List<string> ABDependencies { get; set; }
    }
}

5.CRC 效验(快速检测算法)

using System;

namespace ResLoadFramework
{
    /// <summary>
    /// CRC 效验(快速检测算法)
    /// </summary>
    public class CRC32
    {
        private static UInt32[] crcTable =
        {
          0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9, 0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005,
          0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61, 0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd,
          0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9, 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75,
          0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011, 0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd,
          0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039, 0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5,
          0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81, 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d,
          0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49, 0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95,
          0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1, 0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d,
          0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae, 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072,
          0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16, 0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca,
          0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde, 0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02,
          0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066, 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba,
          0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e, 0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692,
          0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6, 0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a,
          0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e, 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2,
          0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686, 0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a,
          0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637, 0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb,
          0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f, 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53,
          0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47, 0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b,
          0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff, 0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623,
          0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7, 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b,
          0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f, 0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3,
          0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7, 0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b,
          0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f, 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3,
          0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640, 0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c,
          0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8, 0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24,
          0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30, 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec,
          0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088, 0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654,
          0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0, 0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c,
          0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18, 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4,
          0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0, 0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c,
          0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668, 0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4
        };

        public static uint GetCrc32(string msg)
        {
            byte[] bytes = System.Text.Encoding.Default.GetBytes(msg);//解决中文的问题
            //byte[] bytes = System.Text.Encoding.UTF8.GetBytes(msg);//只能英文
            uint iCount = (uint)bytes.Length;
            uint crc = 0xFFFFFFFF;

            for (uint i = 0; i < iCount; i++)
            {
                crc = (crc << 8) ^ crcTable[(crc >> 24) ^ bytes[i]];
            }

            return crc;
        }
    }
}

三、对象管理

1.类对象池 ClassObjectPool

  • 构造函数public ClassObjectPool(int maxCount)maxCount为类对象池的最大对象个数(<=0表示不限个数)
  • API:
    • (1)public T Spawn(bool createIfPoolEmpty):从类对象池中取类对象(createIfPoolEmpty:若类对象池中的对象用完了 是否创建对象)
    • (2)public bool Recycle(T obj):回收对象到类对象池(只有对象真的回收到类对象池中 才返回true)
using System.Collections.Generic;

namespace ResLoadFramework
{
    public class ClassObjectPool<T> where T : class, new()
    {
        protected Stack<T> mPool = new Stack<T>();
        /// <summary>
        /// 池的最大对象个数(<=0表示不限个数)
        /// </summary>
        protected int mMaxCount = 0;
        /// <summary>
        /// 未回收的对象个数
        /// </summary>
        protected int mNoRecycleCount = 0;

        public ClassObjectPool(int maxCount)
        {
            mMaxCount = maxCount;
            //填满类对象池
            for (int i = 0; i < mMaxCount; i++)
            {
                mPool.Push(new T());
            }
        }

        /// <summary>
        /// 从类对象池中取类对象(createIfPoolEmpty:若类对象池中的对象用完了 是否创建对象)
        /// </summary>
        /// <param 如果类对象池为空是否创建对象="createIfPoolEmpty"></param>
        /// <returns></returns>
        public T Spawn(bool createIfPoolEmpty)
        {
            //池中有对象
            if (mPool.Count > 0)
            {
                T ret = mPool.Pop();
                //如果池中的对象为空对象=>池已空
                if (ret == null)
                {
                    if (createIfPoolEmpty)
                    {
                        ret = new T();
                    }
                }
                mNoRecycleCount++;
                return ret;
            }
            //池中无对象
            else
            {
                if (createIfPoolEmpty)
                {
                    T ret = new T();
                    mNoRecycleCount++;
                    return ret;
                }
            }
            return null;//池空,无对象可取
        }

        /// <summary>
        /// 回收对象到类对象池(只有对象真的回收到类对象池中 才返回true)
        /// </summary>
        /// <param 待回收的对象="obj"></param>
        /// <returns>只有对象真的回收到类对象池中 才返回true</returns>
        public bool Recycle(T obj)
        {
            //空对象不需要回收
            if (obj == null)
                return false;
            //超出池容量部分的对象
            if (mPool.Count >= mMaxCount && mMaxCount > 0)
            {
                //直接置空,等待GC释放内存
                obj = null;
                mNoRecycleCount--;
                return false;
            }
            //对象真的回收到类对象池中
            mPool.Push(obj);
            mNoRecycleCount--;
            return true;
        }
    }
}

2.对象管理器 ObjectMgr

  • 继承自单例基类 Singleton<ObjectMgr>
  • API:
    • (1)public ClassObjectPool<T> GetOrCreateClassObjectPool<T>(int maxCount) where T : class, new():获取或创建类对象池(maxCount:类对象池的最大池容量,若小于等于0则不限制容量)
using System;
using System.Collections.Generic;
using UnityEngine;

namespace ResLoadFramework
{
    public class ObjectMgr : Singleton<ObjectMgr>
    {
        #region 类对象池的缓存、获取
        /// <summary>
        /// 缓存所有的类对象池
        /// </summary>
        protected Dictionary<Type, object> mClassObjectPoolDict = new Dictionary<Type, object>();

        /// <summary>
        /// 获取或创建类对象池
        /// maxCount:类对象池的最大池容量,若小于等于0则不限制容量
        /// </summary>
        /// <typeparam 类对象的类型="T"></typeparam>
        /// <param 类对象池的最大池容量="maxCount"></param>
        /// <returns></returns>
        public ClassObjectPool<T> GetOrCreateClassObjectPool<T>(int maxCount) where T : class, new()
        {
            Type type = typeof(T);
            object poolObj = null;
            //缓存中不存在  或者  存在但是为null
            if (mClassObjectPoolDict.TryGetValue(type, out poolObj) == false || poolObj == null)
            {
                //创建类对象池
                ClassObjectPool<T> newPool = new ClassObjectPool<T>(maxCount);
                mClassObjectPoolDict[type] = newPool;
                return newPool;
            }
            return poolObj as ClassObjectPool<T>;
        }
        #endregion


    }
}

四、AssetBundle管理

1.AssetBundle管理器 AssetBundleMgr

  • API:
    • (1)public bool LoadABConfig():加载、缓存AB包配置表。
    • (2)public ResourceItem LoadResourceItem(uint crc):根据资源路径的 Crc 加载资源项 ResourceItem。
    • (3)public void ReleaseResourceItem(ResourceItem resourceItem):释放资源项 ResourceItem。
    • (4)public ResourceItem FindResourceItem(uint crc):根据资源路径的 Crc 从缓存中查找资源项 ResourceItem。

2.加载、缓存 AssetBundle 配置表

ResourceItem 资源项数据类:存储 AssetBundle 信息、依赖项AB包信息、资源信息,供加载资源使用。

ResourceItem 包含有引用计数、资源使用时间,供 ResourcesMgr 中缓存使用。

    /// <summary>
    /// 资源项,存储资源所在AssetBundle信息、依赖项AB包信息、资源信息
    /// </summary>
    public class ResourceItem
    {
        //-------------------------------AB包相关-------------------------------//
        /// <summary>
        /// 资源路径Crc
        /// </summary>
        public uint Crc;
        /// <summary>
        /// 资源名称
        /// </summary>
        public string AssetName;
        /// <summary>
        /// 资源所在AB包名称
        /// </summary>
        public string ABName;
        /// <summary>
        /// 资源所依赖的AB包列表
        /// </summary>
        public List<string> ABDependencies = null;
        /// <summary>
        /// 该资源项对应的AB包(加载好的)
        /// </summary>
        public AssetBundle AssetBundle = null;
        //-------------------------------加载资源相关-------------------------------//
        /// <summary>
        /// 加载的资源对象
        /// </summary>
        public UnityEngine.Object Obj = null;
        /// <summary>
        /// 资源 Object 的唯一标识
        /// </summary>
        public int Guid = 0;
        /// <summary>
        /// 该资源最后使用的时间
        /// </summary>
        public float LastUseTime = 0f;
        protected int mRefCount = 0;
        /// <summary>
        /// 引用计数
        /// </summary>
        public int RefCount
        {
            get => mRefCount;
            set
            {
                mRefCount = value;
                if (value < 0)
                {
                    Debug.LogErrorFormat("ResourceItem 的引用计数 Erroer!value:{0],加载的资源名称:{1}", value, Obj == null ? "null" : Obj.name);
                    mRefCount = 0;
                }
            }
        }
    }

加载、缓存AB包配置表:public bool LoadABConfig()

        #region Configs
        /// <summary>
        /// AB包配置表名称(用于打包、加载AB包配置表)
        /// </summary>
        private static string CONFIG_ABNAME = "assetbundleconfig";
        /// <summary>
        /// AB包配置表资源名称(用于从AssetBundle中加载)
        /// </summary>
        private static string CONFIG_ASSETNAME = "AssetBundleConfig";
        #endregion
        
        /// <summary>
        /// 缓存从AB包配置表中加载出来的 资源项 
        /// </summary>
        protected Dictionary<uint, ResourceItem> mResourceItemDict = new Dictionary<uint, ResourceItem>();

        /// <summary>
        /// 加载、缓存AB包配置表
        /// </summary>
        /// <returns></returns>
        public bool LoadABConfig()
        {
            mResourceItemDict.Clear();
            mAssetBundleItemDict.Clear();
            //加载AB包配置表
            AssetBundle configBundle = AssetBundle.LoadFromFile(Application.streamingAssetsPath + "/" + CONFIG_ABNAME);
            TextAsset textAsset = configBundle.LoadAsset<TextAsset>(CONFIG_ASSETNAME);
            if (textAsset == null)
            {
                Debug.LogError("LoadABConfig Error!AB包配置表不存在");
                return false;
            }
            MemoryStream memoryStream = new MemoryStream(textAsset.bytes);
            BinaryFormatter binaryFormatter = new BinaryFormatter();
            AssetBundleConfig config = (AssetBundleConfig)binaryFormatter.Deserialize(memoryStream);
            memoryStream.Close();
            //缓存AB包配置表
            for (int i = 0; i < config.ABDataList.Count; i++)
            {
                ABData abData = config.ABDataList[i];
                ResourceItem resItem = new ResourceItem
                {
                    Crc = abData.Crc,
                    AssetName = abData.AssetName,
                    ABName = abData.ABName,
                    ABDependencies = abData.ABDependencies,
                };
                if (mResourceItemDict.ContainsKey(resItem.Crc))
                {
                    Debug.LogErrorFormat("LoadABConfig Error!Crc重复,AssetName:{0};ABName:{1}", resItem.AssetName, resItem.ABName);
                    return false;
                }
                mResourceItemDict.Add(resItem.Crc, resItem);
            }
            Debug.Log("AB包配置表加载、缓存完成");
            return true;
        }

3.根据资源路径的 Crc 加载资源项 ResourceItem

AssetBundleItem AssetBundle资源类,包含引用计数

    /// <summary>
    /// AssetBundle资源
    /// </summary>
    public class AssetBundleItem
    {
        public AssetBundle AssetBundle = null;
        /// <summary>
        /// 引用计数
        /// </summary>
        public int RefCount = 0;
        /// <summary>
        /// 重置
        /// </summary>
        public void Reset()
        {
            AssetBundle = null;
            RefCount = 0;
        }
    }

根据资源路径的 Crc 加载资源项 ResourceItem:public ResourceItem LoadResourceItem(uint crc)

        /// <summary>
        /// 根据资源路径的Crc加载资源项ResourceItem
        /// </summary>
        /// <param 资源路径的Crc="crc"></param>
        /// <returns></returns>
        public ResourceItem LoadResourceItem(uint crc)
        {
            ResourceItem resourceItem = null;
            if (mResourceItemDict.TryGetValue(crc, out resourceItem) == false || resourceItem == null)
            {
                Debug.LogErrorFormat("LoadResourceItem Error!AB包配置表中不存在此Crc:{0}", crc.ToString());
                return resourceItem;
            }
            //资源项对应的AB包已加载
            if (resourceItem.AssetBundle != null)
            {
                return resourceItem;
            }
            //资源项对应的AB包未加载
            resourceItem.AssetBundle = LoadAssetBundle(resourceItem.ABName);
            //加载资源项中记录的依赖项
            if (resourceItem.ABDependencies != null)
            {
                for (int i = 0; i < resourceItem.ABDependencies.Count; i++)
                {
                    LoadAssetBundle(resourceItem.ABDependencies[i]);
                }
            }
            return resourceItem;
        }
        /// <summary>
        /// 根据ABName加载、缓存AB包
        /// </summary>
        /// <param AB包名="abName"></param>
        /// <returns></returns>
        private AssetBundle LoadAssetBundle(string abName)
        {
            AssetBundleItem assetBundleItem = null;
            uint crc = CRC32.GetCrc32(abName);
            if (mAssetBundleItemDict.TryGetValue(crc, out assetBundleItem) == false || assetBundleItem == null)
            {
                string fullPath = Application.streamingAssetsPath + "/" + abName;
                AssetBundle assetBundle = null;
                if (File.Exists(fullPath))//容错
                {
                    assetBundle = AssetBundle.LoadFromFile(fullPath);//加载AB包
                }
                if (assetBundle == null)
                {
                    Debug.LogErrorFormat("LoadAssetBundle Error!AB包全路径:{0}", fullPath);
                }
                assetBundleItem = mAssetBundleItemPool.Spawn(true);//使用类对象池
                assetBundleItem.AssetBundle = assetBundle;
                assetBundleItem.RefCount += 1;
                mAssetBundleItemDict.Add(crc, assetBundleItem);
            }
            else
            {
                assetBundleItem.RefCount += 1;
            }
            return assetBundleItem.AssetBundle;
        }

4.释放资源项 ResourceItem

释放资源项 ResourceItem:public void ReleaseResourceItem(ResourceItem resourceItem)

        /// <summary>
        /// 释放资源项ResourceItem
        /// </summary>
        /// <param 资源项="resourceItem"></param>
        public void ReleaseResourceItem(ResourceItem resourceItem)
        {
            if (resourceItem == null) return;
            //卸载依赖项
            if (resourceItem.ABDependencies != null && resourceItem.ABDependencies.Count > 0)
            {
                for (int i = 0; i < resourceItem.ABDependencies.Count; i++)
                {
                    UnloadAssetBundle(resourceItem.ABDependencies[i]);
                }
            }
            //卸载自身AB包
            UnloadAssetBundle(resourceItem.ABName);
            resourceItem.AssetBundle = null;
        }
        /// <summary>
        /// 根据ABName卸载AB包
        /// </summary>
        /// <param AB包名="abName"></param>
        private void UnloadAssetBundle(string abName)
        {
            AssetBundleItem assetBundleItem = null;
            uint crc = CRC32.GetCrc32(abName);
            if (mAssetBundleItemDict.TryGetValue(crc, out assetBundleItem) == false || assetBundleItem == null)
            {
                Debug.LogErrorFormat("UnloadAssetBundle Error!{0}AssetBundle尚未加载", abName);
            }
            else
            {
                assetBundleItem.RefCount -= 1;//引用计数--
                if (assetBundleItem.RefCount <= 0 && assetBundleItem.AssetBundle != null)
                {
                    assetBundleItem.AssetBundle.Unload(true);//卸载AB包
                    assetBundleItem.Reset();//重置对象
                    mAssetBundleItemPool.Recycle(assetBundleItem);//类对象池回收
                    mAssetBundleItemDict.Remove(crc);//从缓存中移除
                }
            }
        }

5.根据资源路径的 Crc 从缓存中查找资源项 ResourceItem

根据资源路径的 Crc 从缓存中查找资源项 ResourceItem:public ResourceItem FindResourceItem(uint crc)

        /// <summary>
        /// 根据资源路径的Crc从缓存中查找资源项ResourceItem
        /// </summary>
        /// <param 资源路径的Crc="crc"></param>
        /// <returns></returns>
        public ResourceItem FindResourceItem(uint crc)
        {
            return mResourceItemDict[crc];
        }

五、Resources 管理器

1.ResourcesMgr

API:

  • (1)public T LoadResource<T>(string path):同步加载资源,根据资源路径。仅加载不需要实例化的资源(sprites/audio…)
  • (2)public bool ReleaseResource(UnityEngine.Object obj, bool destroyObj = false):卸载资源(不需要实例化的资源:sprites/audio…)

2.双向链表

双向链表节点:

    /// <summary>
    /// 双向链表节点
    /// </summary>
    public class DoubleLinkedListNode<T> where T : class, new()
    {
        /// <summary>
        /// 前一个节点
        /// </summary>
        public DoubleLinkedListNode<T> Previous = null;
        /// <summary>
        /// 当前节点
        /// </summary>
        public T Current = null;
        /// <summary>
        /// 后一个节点
        /// </summary>
        public DoubleLinkedListNode<T> Next = null;
    }

双向链表:

API:

  • (1)public DoubleLinkedListNode<T> AddToHead(T t):添加到头结点
  • (2)public DoubleLinkedListNode<T> AddToHead(DoubleLinkedListNode<T> node):添加到头结点
  • (3)public DoubleLinkedListNode<T> AddToTail(T t):添加到尾结点
  • (4)public DoubleLinkedListNode<T> AddToTail(DoubleLinkedListNode<T> node):添加到尾结点
  • (5)public void RemoveNode(DoubleLinkedListNode<T> node):移除节点
  • (6)public void MoveToHead(DoubleLinkedListNode<T> node):将指定节点移动到头结点
    /// <summary>
    /// 双向链表(容器)
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class DoubleLinkedList<T> where T : class, new()
    {
        /// <summary>
        /// 表头
        /// </summary>
        public DoubleLinkedListNode<T> Head = null;
        /// <summary>
        /// 表尾
        /// </summary>
        public DoubleLinkedListNode<T> Tail = null;
        /// <summary>
        /// 双向链表节点的类对象池
        /// </summary>
        protected ClassObjectPool<DoubleLinkedListNode<T>> mDoubleLinkedNodePool = ObjectMgr.Instance.GetOrCreateClassObjectPool<DoubleLinkedListNode<T>>(500);
        protected int mCount;
        /// <summary>
        /// 容器内节点个数
        /// </summary>
        public int Count => mCount;

        /// <summary>
        /// 添加到头结点
        /// </summary>
        public DoubleLinkedListNode<T> AddToHead(T t)
        {
            DoubleLinkedListNode<T> node = mDoubleLinkedNodePool.Spawn(true);
            node.Previous = null;
            node.Next = null;
            node.Current = t;
            return AddToHead(node);
        }
        /// <summary>
        /// 添加到头结点
        /// </summary>
        public DoubleLinkedListNode<T> AddToHead(DoubleLinkedListNode<T> node)
        {
            if (node == null) return null;
            node.Previous = null;
            if (Head == null)
            {
                Tail = Head = node;
            }
            else
            {
                node.Next = Head;
                Head.Previous = node;
                Head = node;
            }
            mCount++;
            return node;
        }
        /// <summary>
        /// 添加到尾结点
        /// </summary>
        public DoubleLinkedListNode<T> AddToTail(T t)
        {
            DoubleLinkedListNode<T> node = mDoubleLinkedNodePool.Spawn(true);
            node.Previous = null;
            node.Next = null;
            node.Current = t;
            return AddToTail(node);
        }
        /// <summary>
        /// 添加到尾结点
        /// </summary>
        public DoubleLinkedListNode<T> AddToTail(DoubleLinkedListNode<T> node)
        {
            if (node == null) return null;
            node.Next = null;
            if (Tail == null)
            {
                Head = Tail = node;
            }
            else
            {
                node.Previous = Tail;
                Tail.Next = node;
                Tail = node;
            }
            mCount++;
            return node;
        }
        /// <summary>
        /// 移除节点
        /// </summary>
        public void RemoveNode(DoubleLinkedListNode<T> node)
        {
            if (node == null) return;
            if (node == Head)
                Head = node.Next;
            if (node == Tail)
                Tail = node.Previous;
            if (node.Previous != null)
                node.Previous.Next = node.Next;
            if (node.Next != null)
                node.Next.Previous = node.Previous;
            node.Previous = node.Next = null;//重置节点
            node.Current = null;
            mDoubleLinkedNodePool.Recycle(node);//回收节点对象
            mCount--;
        }
        /// <summary>
        /// 将指定节点移动到头结点
        /// </summary>
        public void MoveToHead(DoubleLinkedListNode<T> node)
        {
            if (node == null || node == Head) return;
            if (node.Previous == null && node.Next == null) return;
            if (node == Tail)
                Tail = node.Previous;
            if (node.Previous != null)
                node.Previous.Next = node.Next;
            if (node.Next != null)
                node.Next.Previous = node.Previous;
            node.Previous = null;
            node.Next = Head;
            Head.Previous = node;
            Head = node;
            //if (Tail == null)//当只有两个节点的时候,此时尾结点为null
            //{
            //    Tail = Head;
            //}
        }
    }

3.封装双向链表

封装后的双向链表会在销毁时调用析构函数中的 Clear() 方法来清空双向链表。

使用双向链表的原因:

  • (1)

API:

  • (1)public void Clear():清空双向链表
  • (2)public void InsertToHead(T t):插入到双向链表的头结点
  • (3)public void Pop():从双向链表表尾弹出一个节点
  • (4)public void Remove(T t):移除指定节点
  • (5)public T Back():获取双向链表的尾结点
  • (6)public int Size():获取双向链表中节点个数
  • (7)public bool Find(T t):查找指定节点是否存在
  • (8)public bool Refresh(T t):刷新指定节点,将该节点移动到双向链表头结点
    /// <summary>
    /// 封装后的双向链表
    /// </summary>
    public class CustomMapList<T> where T : class, new()
    {
        /// <summary>
        /// 双向链表
        /// </summary>
        private DoubleLinkedList<T> mLink = new DoubleLinkedList<T>();
        /// <summary>
        /// 用于双向链表元素查找的字典
        /// </summary>
        private Dictionary<T, DoubleLinkedListNode<T>> mDict = new Dictionary<T, DoubleLinkedListNode<T>>();

        /// <summary>
        /// 析构函数(清除该对象时清空双向链表)
        /// </summary>
        ~CustomMapList()
        {
            Clear();
        }
        /// <summary>
        /// 清空双向链表
        /// </summary>
        public void Clear()
        {
            while (mLink.Tail != null)
            {
                Remove(mLink.Tail.Current);
            }
        }
        /// <summary>
        /// 插入到双向链表的头结点
        /// </summary>
        public void InsertToHead(T t)
        {
            DoubleLinkedListNode<T> node = null;
            if (mDict.TryGetValue(t, out node) == false || node == null)//字典中没有此node
            {
                mLink.AddToHead(t);
                mDict.Add(t, mLink.Head);
                return;
            }
            //字典中已有此node
            mLink.AddToHead(node);
        }
        /// <summary>
        /// 从双向链表表尾弹出一个节点
        /// </summary>
        public void Pop()
        {
            if (mLink.Tail != null)
            {
                Remove(mLink.Tail.Current);
            }
        }
        /// <summary>
        /// 移除指定节点
        /// </summary>
        public void Remove(T t)
        {
            DoubleLinkedListNode<T> node = null;
            if (mDict.TryGetValue(t, out node) == false || node == null) return;
            mLink.RemoveNode(node);
            mDict.Remove(t);
        }
        /// <summary>
        /// 获取双向链表的尾结点
        /// </summary>
        public T Back()
        {
            return mLink.Tail == null ? null : mLink.Tail.Current;
        }
        /// <summary>
        /// 获取双向链表中节点个数
        /// </summary>
        public int Size()
        {
            return mDict.Count;
        }
        /// <summary>
        /// 查找指定节点是否存在
        /// </summary>
        public bool Find(T t)
        {
            DoubleLinkedListNode<T> node = null;
            if (mDict.TryGetValue(t, out node) == false || node == null)
                return false;
            return true;
        }
        /// <summary>
        ///刷新指定节点,将该节点移动到双向链表头结点
        /// </summary>
        public bool Refresh(T t)
        {
            DoubleLinkedListNode<T> node = null;
            //节点不存在
            if (mDict.TryGetValue(t, out node) == false || node == null)
                return false;
            //节点存在,移动到头结点
            mLink.MoveToHead(node);
            return true;
        }
    }

4.同步加载非实例化资源(shader/sprite/sound…)

流程图:

picture1

        /// <summary>
        /// 缓存资源引用计数为 0 的 ResourceItem,达到最大缓存时,释放最早没有使用的资源
        /// </summary>
        protected CustomMapList<ResourceItem> mNoReferenceMapList = new CustomMapList<ResourceItem>();
        /// <summary>
        /// 缓存正在使用的资源(ResourceItem 引用计数非 0)
        /// </summary>
        public Dictionary<uint, ResourceItem> AssetDict { get; set; } = new Dictionary<uint, ResourceItem>();
        /// <summary>
        /// 是否从AB包中加载资源
        /// </summary>
        public bool IsLoadFromAssetBundle = true;

        /// <summary>
        /// 同步加载资源,根据资源路径。仅加载不需要实例化的资源(sprites/audio...)
        /// </summary>
        /// <typeparam 资源类型="T"></typeparam>
        /// <param 资源路径="path"></param>
        /// <returns></returns>
        public T LoadResource<T>(string path) where T : UnityEngine.Object
        {
            if (string.IsNullOrEmpty(path)) return null;
            uint crc = CRC32.GetCrc32(path);
            //从正在使用的资源缓存中获取
            ResourceItem resItem = GetResourceItemFormCache(crc);
            //正在使用的资源缓存中 存在该资源
            if (resItem != null)
            {
                return resItem.Obj as T;
            }
            //正在使用的资源缓存中 不存在该资源
            else
            {
                T obj = null;
#if UNITY_EDITOR
                //编辑器模式下(非真机运行环境)
                if (IsLoadFromAssetBundle == false)
                {
                    //从 AssetBundleMgr 的缓存中获取 ResourceItem 信息
                    resItem = AssetBundleMgr.Instance.FindResourceItem(crc);
                    if (resItem.Obj != null)
                    {
                        obj = resItem.Obj as T;
                    }
                    else
                    {
                        obj = LoadAssetByEditor<T>(path);
                    }
                }
#endif
                //真机运行环境(使用 AssetBundle 加载资源)
                if (obj == null)
                {
                    //从 AssetBundleMgr 中获取 ResourceItem 信息,并完成AB包加载、依赖项加载
                    resItem = AssetBundleMgr.Instance.LoadResourceItem(crc);
                    if (resItem != null && resItem.AssetBundle != null)//容错
                    {
                        if (resItem.Obj != null)
                        {
                            obj = resItem.Obj as T;
                        }
                        else
                        {
                            //使用 AssetBundle 加载资源
                            obj = resItem.AssetBundle.LoadAsset<T>(resItem.AssetName);
                        }
                    }
                }
                //缓存 ResourceItem 到 AssetDict
                CacheResourceItem(path, ref resItem, obj);
                return obj;
            }
        }
                /// <summary>
        /// 缓存资源项 ResourceItem ,更新资源唯一标识、最后使用时间、引用计数
        /// </summary>
        /// <param 资源路径="path"></param>
        /// <param 资源项="resItem"></param>
        /// <param 加载的资源="obj"></param>
        private void CacheResourceItem(string path, ref ResourceItem resItem, UnityEngine.Object obj, int addRefCount = 1)
        {
            //缓存太多,清除最早没有使用的资源
            WashOut();

            if (resItem == null || obj == null)
                Debug.LogErrorFormat("CacheResourceItem Error!path:{0}", path);
            resItem.Obj = obj;
            resItem.Guid = obj.GetInstanceID();//资源 Object 的唯一标识
            resItem.LastUseTime = Time.realtimeSinceStartup;
            resItem.RefCount += addRefCount;
            ResourceItem oldResItem = null;
            if (AssetDict.TryGetValue(resItem.Crc, out oldResItem))
            {
                AssetDict[resItem.Crc] = resItem;//替换 ResourceItem,理论上不需要
            }
            else
            {
                AssetDict.Add(resItem.Crc, resItem);
            }
        }
        /// <summary>
        /// 清除最早没有使用的资源(缓存太多)
        /// </summary>
        protected void WashOut()
        {
            ////当前内存使用>80%的时候,清除最早没有使用的资源
            //if (mNoReferenceMapList.Size() <= 0)
            //    break;
            //ResourceItem resItem = mNoReferenceMapList.Back();//取出尾部资源
            //DestroyResourceItem(resItem,true);
            //mNoReferenceMapList.Pop();
        }
        /// <summary>
        /// 从 AssetDict 缓存中回收 ResourceItem
        /// </summary>
        /// <param 资源项="resItem"></param>
        /// <param 是否删除缓存="destroyCache"></param>
        protected void DestroyResourceItem(ResourceItem resItem, bool destroyCache = false)
        {
            if (resItem == null || resItem.RefCount > 0) return;
            //不删除缓存:
            if (destroyCache == false)
            {
                //将 AssetDict 中的 ResourceItem 添加到 当前没有使用的资源的缓存列表 中
                mNoReferenceMapList.InsertToHead(resItem);
                return;
            }
            //删除缓存:
            //若该 ResourceItem 不存在于 AssetDict,则返回,不做处理
            if (AssetDict.Remove(resItem.Crc) == false) return;
            //若该 ResourceItem 存在于 AssetDict 中
            //释放 ResourceItem,对应的 AB包、依赖项 根据引用计数情况进行卸载
            AssetBundleMgr.Instance.ReleaseResourceItem(resItem);
            if (resItem.Obj != null)
                resItem.Obj = null;//对Object资源进行引用置空,此后系统会自动进行GC操作
        }

#if UNITY_EDITOR
        /// <summary>
        /// 同步加载资源(编辑器模式)
        /// </summary>
        /// <typeparam 资源类型="T"></typeparam>
        /// <param 资源路径="path"></param>
        /// <returns></returns>
        protected T LoadAssetByEditor<T>(string path) where T : UnityEngine.Object
        {
            return UnityEditor.AssetDatabase.LoadAssetAtPath<T>(path);
        }
#endif

        /// <summary>
        /// 根据资源路径的 crc 从正在使用的资源缓存中获取对应资源
        /// </summary>
        /// <param 资源路径的 crc="crc"></param>
        /// <param 增加引用计数的个数="addRefCount"></param>
        /// <returns></returns>
        private ResourceItem GetResourceItemFormCache(uint crc, int addRefCount = 1)
        {
            ResourceItem resitem = null;
            if (AssetDict.TryGetValue(crc, out resitem) && resitem != null)
            {
                resitem.RefCount += addRefCount;
                resitem.LastUseTime = Time.realtimeSinceStartup;//记录资源使用时间
                //if (resitem.RefCount <= 1)//容错(理论上不会进入此分支)
                //{
                //    mNoReferenceMapList.Remove(resitem);
                //}
            }
            return resitem;
        }

5.卸载资源

编辑器模式下,不能完全卸载资源,即不能从 AssetDict 缓存中清除资源。

流程图:

picture2

        /// <summary>
        /// 卸载资源(不需要实例化的资源:sprites/audio...)
        /// </summary>
        /// <param 资源="obj"></param>
        /// <param 是否从缓存中清除该资源="destroyObj"></param>
        /// <returns></returns>
        public bool ReleaseResource(UnityEngine.Object obj, bool destroyObj = false)
        {
            if (obj == null) return false;
            //从 AssetDict 缓存中通过资源 obj 的唯一标识获取对应的 ResourceItem
            ResourceItem resItem = null;
            foreach (ResourceItem item in AssetDict.Values)
            {
                if (item.Guid == obj.GetInstanceID())
                {
                    resItem = item;
                }
            }
            if (resItem == null)
            {
                Debug.LogErrorFormat("ReleaseResource Error!{0}可能释放了多次", obj.name);
                return false;
            }
            resItem.RefCount -= 1;//引用计数--
            DestroyResourceItem(resItem, destroyObj);//从 AssetDict 中回收 ResourceItem
            return true;
        }