• ( 资源管理器 02 )AssetBundle 同步异步引用计数资源加载管理器


    Unity资源加载

    Unity资源类型,按加载流程顺序,有三种

    1. AssetBundle 资源以压缩包文件存在(Resources目录下资源打成包体后也是以ab格式存在)
    2. Asset 资源在内存中的存在格式
    3. GameObject 针对Prefab导出的Asset,可实例化

             

    针对AssetBundle 的加载,本文会作讲解,并提供整套方案和代码,

    针对Asset 的加载,写在 ( 资源管理器 01 )Asset 同步异步引用计数资源加载管理器 里;

     针对GameObject的加载,写在( 资源管理器 03 )prefab 加载自动化管理引用计数器 里;

    框架

    AssetBundle加载技术选型

    AssetBundle加载有三套接口,WWWUnityWebRequest 和 AssetBundle,大部分文章都推荐AssetBundle,本人也推荐。

    前两者都要经历将整个文件的二进制流下载或读取到内存中,然后对这段内存文件进行ab资源的读取解析操作,

    而AssetBundle可以只读取存储于本地的ab文件的头部部分,在需要的情况下,读取ab中的数据段部分(Asset资源)。

    所以AssetBundle相对的优势是

    1. 不进行下载(不占用下载缓存区内存)
    2. 不读取整个文件到内存(不占用原始文件二进制内存)
    3. 读取非压缩或LZ4的ab,只读取ab的文件头(约5kb/个)
    4. 同步异步加载并行可用


    所以,从内存和效率方面,AssetBundle会是目前最优解,而使用非压缩或LZ4读者自己评断(推荐LZ4)

    AssetBundle加载方式最重要的接口(接口用法读者自己百度学习):

    AssetBundle.LoadFromFile 从本地文件同步加载ab
    AssetBundle.LoadFromFileAsync 从本地文件异步加载ab
    AssetBundle.Unload 卸载,注意true和false区别
    AssetBundle.LoadAsset 从ab同步加载Asset
    AssetBundle.LoadAssetAsync 从ab异步加载Asset

    加载去协程化
    使用异步AssetBundle加载的时候,大部分开发者都喜欢使用协程的方式去加载,当然这已经成为通用做法。但这种做法弊端也很明显:

    1. 大量依赖ab等待加载,逻辑复杂
    2. ab加载状态切换的复杂化
    3. 协程顺序的不确定性,增加难度
    4. ab卸载和加载同时进行处理难
    5. ab同步和异步同时进行处理难


    协程在某些情况确实可以让开发简单化,但在耦合高的代码中非常容易导致逻辑复杂化。
    这里笔者提供一种 使用Update去协程化 的方案。
    我们都知道,使用协程的地方,大部分都是需要等待线程返回逻辑的,而这样的等待逻辑可以使用Update每帧访问的方式,确定线程逻辑是否结束

    AssetBundleCreateRequest request = AssetBundle.LoadFromFileAsync(path);
    
    IEnumerator LoadAssetBundle()
    {
        yield return request;
        //do something
    }
    
    转变为
    
    void Update()
    {
        if(request.isDone)
        {
            //do something
        }
    }
     

    其实协程本质,就是保留现场的回调函数,内部机制也是update的每帧遍历(具体参见IEnumerator原理)。

    Update才是王道

    既然是加载资源,那必然会有队列,笔者这边依据需求和优化要求,设计成四个队列,准备队列加载队列完成队列销毁

    private Dictionary<string, AssetBundleObject> _readyABList; //预备加载的列表
    private Dictionary<string, AssetBundleObject> _loadingABList; //正在加载的列表
    private Dictionary<string, AssetBundleObject> _loadedABList; //加载完成的列表
    private Dictionary<string, AssetBundleObject> _unloadABList; //准备卸载的列表

    队列之间,队列成员的转移需要一个触发点,而这样的触发点如果都写在加载和销毁逻辑里,耦合度过高,而且逻辑复杂还容易出错。

    TIP:为什么没有设计异常队列

    1. 一般资源加载,都是默认资源是存在的
    2. 资源如果不存在,一定是策划没有把资源放进去(嗯,一定是这样)
    3. 设计上是加载了总依赖关系的Mainfest,是对文件存在性可以进行判断的
    4. 从性能的角度,通过File.exists()来判断文件存在性,是效率低下的方式
    5. 代码中对异常是有处理的,会有重复加载,下载和修复完整性的逻辑

    笔者很喜欢的一种设计,就是通过Update来降低耦合度,这种方式代码清晰,逻辑简单,但缺点也很明显,丢失原始现场。

    回到本篇文章,当然是通过Update来运行逻辑,如下

     

     TIP:为什么Update里三个函数的运行顺序跟队列转移顺序不一样?

    1.UpdateReady在UpdateLoad后面,可以实现当前帧就创建新的加载,否则要等到下一帧
    2.UpdateUnLoad放最后,是因为正在加载的资源要等到加载完才能卸载

    外部接口

    根据上面的逻辑,很容易设计下面的接口逻辑

    实现

    加载依赖关系配置

    LoadMainfest是用来加载文件列表和依赖关系的,一般在游戏热更之后,游戏登录界面之前进行游戏初始化的时候。加载的配置文件是Unity导出AssetBundle时生成的主Mainfest文件,具体逻辑如下

    _dependsDataList.Clear();
    AssetBundle ab = AssetBundle.LoadFromFile(path);
    AssetBundleManifest mainfest = ab.LoadAsset("AssetBundleManifest") as AssetBundleManifest;
    
    foreach(string assetName in mainfest.GetAllAssetBundles())
    {
        string hashName = assetName.Replace(".ab", "");
        string[] dps = mainfest.GetAllDependencies(assetName);
        for (int i = 0; i < dps.Length; i++)
            dps[i] = dps[i].Replace(".ab", "");
        _dependsDataList.Add(hashName, dps);
    }
    
    ab.Unload(true);
    ab = null;
     

    这部分,大部分游戏都大同小异,就是将配置转化成类结构。注意ab.Unload(true);用完要销毁。

    加载节点数据结构

    public delegate void AssetBundleLoadCallBack(AssetBundle ab);
    
    private class AssetBundleObject
    {
        public string _hashName; //hash标识符
    
        public int _refCount; //引用计数
        public List<AssetBundleLoadCallBack> _callFunList = new List<AssetBundleLoadCallBack>(); //回调函数
    
        public AssetBundleCreateRequest _request; //异步加载请求
        public AssetBundle _ab; //加载到的ab
    
        public int _dependLoadingCount; //依赖计数
        public List<AssetBundleObject> _depends = new List<AssetBundleObject>(); //依赖项
    }
     

    加载节点的数据结构不复杂,看代码就很容易理解。

    依赖加载——递归&引用计数&队列&回调
    依赖加载,是ab加载逻辑里最难最复杂最容易出bug的地方,也是本文的难点。

    难点为以下几点:

    1.加载时,root节点和depend节点引用计数的正确增加
    2.卸载时,root节点和depend节点引用计数的正确减少
    3.还未加载,准备加载,正在加载,已经加载节点关系处理
    4.节点加载完成,回调逻辑的高效和正确性


    我们来一一分解
    首先,看一下ab节点的引用计数要实现的逻辑
     

    注: 上图显示加载和销毁都需要递归标记依赖节点的依赖节点
    TIP:为什么引用计数一定要递归标记所有子节点?

    我们需要确定一个节点是否需要销毁,是通过引用计数是否为零来判断的,很多语言使用的内存回收机制就是引用计数。
    如果只标记当前节点和其一层依赖项,当其依赖项也作为主加载节点,我就没办法判断二层依赖节点是否需要销毁了。
    
    所以,带依赖关系的引用计数,需要递归标记所有子节点,才能确认任意一个节点是否需要卸载。
    每次加载,都要递归标记,会不会有效率问题?
    很幸运,在绝大多数情况,依赖节点关系不会超过三层,依赖节点总数量不超过10个(生成最小依赖树情况下)
    一般游戏至少一半以上ab节点都是单节点,不包含需要拆分的依赖关系。

     用引用计数的方法,可以确定一个资源是否需要销毁。代码逻辑表示为(代码简化了部分逻辑)

    private void DoDependsRef(AssetBundleObject abObj)
    {
        abObj._refCount++;
        foreach (var dpObj in abObj._depends)
        {
            DoDependsRef(dpObj); //递归依赖项,加载完
        }
    }
    private AssetBundleObject LoadAssetBundleAsync(string _hashName)
    {
        AssetBundleObject abObj = null;
        if (_ABList.ContainsKey(_hashName)) //队列有
        {
            abObj = _ABList[_hashName];
            DoDependsRef(abObj); //递归引用计数
            return abObj;
        }
    
        //创建一个加载节点
        abObj = new AssetBundleObject();
        abObj._hashName = _hashName;
        abObj._refCount = 1;
    
        //加载依赖项
        string[] dependsData = _dependsDataList[_hashName];
        abObj._dependLoadingCount = dependsData.Length;
    
        foreach(var dpAssetName in dependsData)
        {
            var dpObj = LoadAssetBundleAsync(dpAssetName);
            abObj._depends.Add(dpObj);
        }
    
        DoLoad(abObj); //调用unity接口开始加载
        _ABList.Add(_hashName, abObj); //加入队列
    
        return abObj;
    }
     

    上述代码构造了引用计数,递归,加入队列。理解起来其实不难,难在写出符合设想的逻辑代码。

    上面构造了递归引用计数的逻辑,我们再加入队列的逻辑。

    队列逻辑在上文已经描述过了,总结几个要点

    1.当一个节点引用计数由0变为1时,需要创建ab节点,加入准备队列或加载队列。
    2.当一个节点加载完ab,将其加入完成队列
    3.当一个节点引用计数由1变为0时,需要加入销毁队列。


    对应到开启异步加载和销毁时,代码如下
     

    private AssetBundleObject LoadAssetBundleAsync(string _hashName)
        AssetBundleObject abObj = null;
        if (_loadedABList.ContainsKey(_hashName)) //已经加载
        {
            abObj = _loadedABList[_hashName];
            DoDependsRef(abObj);
            return abObj;
        }
        else if (_loadingABList.ContainsKey(_hashName)) //在加载中
        {
            abObj = _loadingABList[_hashName];
            DoDependsRef(abObj);
            return abObj;
        }
        else if (_readyABList.ContainsKey(_hashName)) //在准备加载中
        {
            abObj = _readyABList[_hashName];
            DoDependsRef(abObj);
            return abObj;
        }
        
        //....................
        //创建一个ab节点........
        //....................
        
        if (_loadingABList.Count < MAX_LOADING_COUNT) //正在加载的数量不能超过上限
        {
            DoLoad(abObj); //调用unity接口开始加载
    
            _loadingABList.Add(_hashName, abObj);
        }
        else _readyABList.Add(_hashName, abObj);
        
        return abObj;
    }
    
    private void UnloadAssetBundleAsync(string _hashName)
    {
        AssetBundleObject abObj = null;
        if (_loadedABList.ContainsKey(_hashName))
            abObj = _loadedABList[_hashName];
        else if (_loadingABList.ContainsKey(_hashName))
            abObj = _loadingABList[_hashName];
        else if (_readyABList.ContainsKey(_hashName))
            abObj = _readyABList[_hashName];
    
        abObj._refCount--;
    
        foreach (var dpObj in abObj._depends)
        {
            UnloadAssetBundleAsync(dpObj._hashName);
        }
    
        if (abObj._refCount == 0)
        {//这里只是加入销毁队列,并没有真正销毁,真正销毁要在Update里
            _unloadABList.Add(abObj._hashName, abObj);
        }
    }
     

    从这里,上文已经完成了整个异步加载的逻辑,已经实现创建到销毁的代码。但异步加载还有一个问题没有解决——判读ab节点加载完成。

    我们需要在ab节点及其依赖ab节点都加载完后,告诉上层调用逻辑,ab资源加载完了。简单地做法就是,在Update里逻辑判断一个节点及其子节点都加载完了。我们会有下面这样的代码结构

    注:圆角方形表示ab自身加载完成,箭头表示依赖关系

    图1-递归判定,如果需要知道A是否加载完,需要依次判定D,E,C,A四个节点,

    //不高效的逻辑判定方式
    bool IsAssetBundleLoaded(AssetBundleObject abObj)
    {
        if(abObj._dependLoadingCount == 0 && abObj._ab != null) return true;
        foreach (var dpObj in abObj._depends)
        {
            if(!IsAssetBundleLoaded(dpObj)) return false;
        }
        return true;
    }

    很明显的弊端,上述代码需要关心子依赖节点以及孙依赖节点,这样的代码不管是效率还是设计,都不是一种优秀的方式。

    那么有没有一种更好的方式呢,笔者提供一种解耦的方式——回调
    我们先用图示表示加载A和B到完成的整个过程

    注:圆角方形表示ab自身加载完成,箭头表示依赖关系
    上图,会按以下回调逻辑

    1.  同时加载A和B,标记引用计数
    2.  D自身加载完,会回调C;
        C自身没有加载完,然后C会记录子依赖加载情况
    3.  C自身加载完,但子依赖没加载完,不操作
    4.  B自身加载完,但子依赖没加载完,不操作
    5.  E自身加载完,会回调C;
        C的子依赖加载完了,C自己也加载完了,回调A和B;
        A自己没加载完,不操作;
        B自己已经加载完了,子依赖也加载完了,B完成加载
    6.  A自身加载完,子依赖已经加载完了,A完成加载

    按照上述逻辑,读者应该能够理解回调在解决的问题了吧。

    回调可以将父子孙的树形图结构,解耦成子父的边结构。关键代码如下

    private void DoLoadedCallFun(AssetBundleObject abObj)
    {
        //提取ab
        if (abObj._request != null)
        {
            abObj._ab = abObj._request.assetBundle; //如果没加载完,会异步转同步
            abObj._request = null;
            _loadingABList.Remove(abObj._hashName);
            _loadedABList.Add(abObj._hashName, abObj);
        }
    
        //运行回调
        foreach (var callback in abObj._callFunList)
        {
            callback(abObj._ab);
        }
        abObj._callFunList.Clear();
    }
    private AssetBundleObject LoadAssetBundleAsync(string _hashName, AssetBundleLoadCallBack _callFun)
    {//这里只是展示代码逻辑,代码非完整
        AssetBundleObject abObj = new AssetBundleObject();
        abObj._hashName = _hashName;
        abObj._refCount = 1;
        abObj._callFunList.Add(_callFun); //保存回调
    
        //加载依赖项
        string[] dependsData = _dependsDataList[_hashName];
        abObj._dependLoadingCount = dependsData.Length;
    
        foreach(var dpAssetName in dependsData)
        {
            var dpObj = LoadAssetBundleAsync(dpAssetName
                //这里是构造回调函数
                (AssetBundle _ab) =>
                {
                    abObj._dependLoadingCount--;
        
                    if (abObj._dependLoadingCount == 0 && abObj._request != null && abObj._request.isDone)
                    {//依赖加载完,自身也加载完,回调被依赖项
                        DoLoadedCallFun(abObj);
                    }
                }
               );
            abObj._depends.Add(dpObj);
        }
        return abObj;
    }
    private void UpdateLoad()
    {//每帧调用,用于触发加载完成
        if (_loadingABList.Count == 0) return;
        //检测加载完的
        tempLoadeds.Clear();
        foreach (var abObj in _loadingABList.Values)
        {
            if (abObj._dependLoadingCount == 0 && abObj._request != null && abObj._request.isDone)
            {//依赖加载完,自身也加载完,回调被依赖项
                tempLoadeds.Add(abObj);
            }
        }
        //回调中有可能对_loadingABList进行操作,提取后回调
        foreach (var abObj in tempLoadeds)
        {
            //加载完进行回调
            DoLoadedCallFun(abObj);
        }
    }

    到这里,超级复杂的依赖加载问题就解决啦,我们可以欢快地开始使用异步加载啦!!!

    我要异步加载和同步加载一起用
    异步加载已经很复杂了,如果还要在异步加载的基础上,使用同步加载,是不是感觉很头大!!!
    没关系,这边会给你提供整套解决方案。
    如果没有异步加载,同步加载是不是很开心地如下代码:
     

    private AssetBundleObject LoadAssetBundleSync(string _hashName)
    {
        AssetBundleObject abObj = null;
        if (_loadedABList.ContainsKey(_hashName)) //已经加载
        {
            abObj = _loadedABList[_hashName];
            DoDependsRef(abObj);
            return abObj;
        }
        //创建一个加载
        abObj = new AssetBundleObject();
        abObj._hashName = _hashName;
        abObj._refCount = 1;
    
        string path = GetAssetBundlePath(_hashName);
        abObj._ab = AssetBundle.LoadFromFile(path);
    
        //加载依赖项
        string[] dependsData = _dependsDataList[_hashName];
        abObj._dependLoadingCount = 0;
        foreach (var dpAssetName in dependsData)
        {
            var dpObj = LoadAssetBundleSync(dpAssetName);
            abObj._depends.Add(dpObj);
        }
    
        _loadedABList.Add(abObj._hashName, abObj);
        return abObj;
    }
     

    在异步请求一个AssetBundle的时候,会返回一个AssetBundleCreateRequest对象,Unity的官方文档上写
    AssetBundleCreateRequest.assetBundle的时候这样说:
    “
    Description Asset object being loaded (Read Only).
    “
    Note that accessing asset before isDone is true will stall the loading process.
    
    经测试,在isDone是false的时候,直接调用request.assetBundle,可以拿到同步加载的结果
     

    好啦,现在三个问题解决啦,看代码:

    private void DoLoadedCallFun(AssetBundleObject abObj)
    {
        //提取ab
        if (abObj._request != null)
        {
            abObj._ab = abObj._request.assetBundle; //如果没加载完,会异步转同步
            abObj._request = null;
            _loadingABList.Remove(abObj._hashName);
            _loadedABList.Add(abObj._hashName, abObj);
        }
    
        //运行回调
        foreach (var callback in abObj._callFunList)
        {
            callback(abObj._ab);
        }
        abObj._callFunList.Clear();
    }
    
    
        AssetBundleObject abObj = null;
        if (_loadedABList.ContainsKey(_hashName)) //已经加载
        {
            abObj = _loadedABList[_hashName];
            abObj._refCount++;
    
            foreach (var dpObj in abObj._depends)
            {
                LoadAssetBundleSync(dpObj._hashName); //递归依赖项,附加引用计数
            }
    
            return abObj;
        }
        else if (_loadingABList.ContainsKey(_hashName)) //在加载中,异步改同步
        {
            abObj = _loadingABList[_hashName];
            abObj._refCount++;
    
            foreach (var dpObj in abObj._depends)
            {
                LoadAssetBundleSync(dpObj._hashName); //递归依赖项,加载完
            }
    
            DoLoadedCallFun(abObj, false); //强制完成,回调
    
            return abObj;
        }
        else if (_readyABList.ContainsKey(_hashName)) //在准备加载中
        {
            abObj = _readyABList[_hashName];
            abObj._refCount++;
    
            foreach (var dpObj in abObj._depends)
            {
                LoadAssetBundleSync(dpObj._hashName); //递归依赖项,加载完
            }
    
            string path1 = GetAssetBundlePath(_hashName);
            abObj._ab = AssetBundle.LoadFromFile(path1);
    
            _readyABList.Remove(abObj._hashName);
            _loadedABList.Add(abObj._hashName, abObj);
    
            DoLoadedCallFun(abObj, false); //强制完成,回调
    
            return abObj;
        }
     

    好啦,到这里,同步加载也完美解决啦

    优化

    资源路径管理——字符串转hash

    下面的代码,是笔者使用的hash方式。

    private string GetHashName(string _assetName)
    {//读者可以自己定义hash方式,对内存有要求的话,可以hash成uint(或uint64)节省内存
        return _assetName.ToLower();
    }
    
    private string GetFileName(string _hashName)
    {//读者可以自己实现自己的对应关系
        return _hashName + ".ab";
    }
    
    // 获取一个资源的路径
    private string GetAssetBundlePath(string _hashName)
    {//读者可以自己实现的对应关系,笔者这里有多语言和文件版本的处理
        string lngHashName = GetHashName(LocalizationMgr.I.GetAssetPrefix() + _hashName);
        if (_dependsDataList.ContainsKey(lngHashName))
            _hashName = lngHashName;
    
        return FileVersionMgr.I.GetFilePath(GetFileName(_hashName));
    }
     

    资源管理,一定逃不开的路径管理,上面的三个函数,封装了必要的路径需求,读者有需求的话,可以使用针对项目的路径管理方案,这边笔者就当抛砖引玉啦。

    这边再提供一个内存优化方案,将_assetName Hash成uint值,这样可以没有大量字符串(依赖项配置和路径字符串)保存在内存中

    public static uint GetHashName(string _assetName)
    {
        if (string.IsNullOrEmpty(_assetName)) return 0;
    
        char[] bitarray = _assetName.ToCharArray();
        int count = bitarray.Length;
    
        uint hash = 0;
        while (count-- > 0)
        {
            hash = hash * seed + (bitarray[count]);
        }
    
        return hash;
    }
    private string GetFileName(uint _hashName)
    {//读者可以自己实现自己的对应关系
        return _hashName + ".ab";
    }
    private string GetAssetBundlePath(string _hashName)
    {
        return FileVersionMgr.I.GetFilePath(GetFileName(_hashName));
    }
     

    使用上述代码, 需要 LoadMainfest() 配合,还需要在AssetBundle打包导出时,将路径和依赖项路径Hash成uint,然后作为导出的文件名,具体实现参照这篇文章的导出根节点和依赖节点的GetAbName(ABNode abNode)函数。

    大招——资源管理器完整代码
    上文讲了那么多内容,开始放大招——资源管理器完整代码。
     

    using System;
    using System.Collections.Generic;
    using System.IO;
    using UnityEngine;
    
    public class AssetBundleLoadMgr
    {
        public delegate void AssetBundleLoadCallBack(AssetBundle ab);
    
        private class AssetBundleObject
        {
            public string _hashName;
    
            public int _refCount;
            public List<AssetBundleLoadCallBack> _callFunList = new List<AssetBundleLoadCallBack>();
    
            public AssetBundleCreateRequest _request;
            public AssetBundle _ab;
    
            public int _dependLoadingCount;
            public List<AssetBundleObject> _depends = new List<AssetBundleObject>();
        }
    
        private static AssetBundleLoadMgr _Instance = null;
    
        public static AssetBundleLoadMgr I
        {
            get {
                if (_Instance == null) _Instance = new AssetBundleLoadMgr();
                return _Instance;
            }
        }
    
        private const int MAX_LOADING_COUNT = 10; //同时加载的最大数量
    
        private List<AssetBundleObject> tempLoadeds = new List<AssetBundleObject>(); //创建临时存储变量,用于提升性能
    
        private Dictionary<string, string[]> _dependsDataList;
    
        private Dictionary<string, AssetBundleObject> _readyABList; //预备加载的列表
        private Dictionary<string, AssetBundleObject> _loadingABList; //正在加载的列表
        private Dictionary<string, AssetBundleObject> _loadedABList; //加载完成的列表
        private Dictionary<string, AssetBundleObject> _unloadABList; //准备卸载的列表
    
        private AssetBundleLoadMgr()
        {
            _dependsDataList = new Dictionary<string, string[]>();
    
            _readyABList = new Dictionary<string, AssetBundleObject>();
            _loadingABList = new Dictionary<string, AssetBundleObject>();
            _loadedABList = new Dictionary<string, AssetBundleObject>();
            _unloadABList = new Dictionary<string, AssetBundleObject>();
            
        }
    
        public void LoadMainfest()
        {
            string path = FileVersionMgr.I.GetFilePathByExist("Assets");
            if (string.IsNullOrEmpty(path)) return;
    
            _dependsDataList.Clear();
            AssetBundle ab = AssetBundle.LoadFromFile(path);
    
            if(ab == null)
            {
                string errormsg = string.Format("LoadMainfest ab NULL error !");
                Debug.LogError(errormsg);
                return;
            }
    
            AssetBundleManifest mainfest = ab.LoadAsset("AssetBundleManifest") as AssetBundleManifest;
            if (mainfest == null)
            {
                string errormsg = string.Format("LoadMainfest NULL error !");
                Debug.LogError(errormsg);
                return;
            }
    
            foreach(string assetName in mainfest.GetAllAssetBundles())
            {
                string hashName = assetName.Replace(".ab", "");
                string[] dps = mainfest.GetAllDependencies(assetName);
                for (int i = 0; i < dps.Length; i++)
                    dps[i] = dps[i].Replace(".ab", "");
                _dependsDataList.Add(hashName, dps);
            }
    
            ab.Unload(true);
            ab = null;
    
            Debug.Log("AssetBundleLoadMgr dependsCount=" + _dependsDataList.Count);
        }
    
        private string GetHashName(string _assetName)
        {//读者可以自己定义hash方式,对内存有要求的话,可以hash成uint(或uint64)节省内存
            return _assetName.ToLower();
        }
    
        private string GetFileName(string _hashName)
        {//读者可以自己实现自己的对应关系
            return _hashName + ".ab";
        }
        
        // 获取一个资源的路径
        private string GetAssetBundlePath(string _hashName)
        {//读者可以自己实现的对应关系,笔者这里有多语言和文件版本的处理
            string lngHashName = GetHashName(LocalizationMgr.I.GetAssetPrefix() + _hashName);
            if (_dependsDataList.ContainsKey(lngHashName))
                _hashName = lngHashName;
    
            return FileVersionMgr.I.GetFilePath(GetFileName(_hashName));
        }
    
        public bool IsABExist(string _assetName)
        {
            string hashName = GetHashName(_assetName);
            return _dependsDataList.ContainsKey(hashName);
        }
    
        //同步加载
        public AssetBundle LoadSync(string _assetName)
        {
            string hashName = GetHashName(_assetName);
            var abObj = LoadAssetBundleSync(hashName);
            return abObj._ab;
        }
    
        //异步加载(已经加载直接回调),每次加载引用计数+1
        public void LoadAsync(string _assetName, AssetBundleLoadCallBack callFun)
        {
            string hashName = GetHashName(_assetName);
            LoadAssetBundleAsync(hashName, callFun);
        }
        //卸载(异步),每次卸载引用计数-1
        public void Unload(string _assetName)
        {
            string hashName = GetHashName(_assetName);
            UnloadAssetBundleAsync(hashName);
        }
    
        private AssetBundleObject LoadAssetBundleSync(string _hashName)
        {
            AssetBundleObject abObj = null;
            if (_loadedABList.ContainsKey(_hashName)) //已经加载
            {
                abObj = _loadedABList[_hashName];
                abObj._refCount++;
    
                foreach (var dpObj in abObj._depends)
                {
                    LoadAssetBundleSync(dpObj._hashName); //递归依赖项,附加引用计数
                }
    
                return abObj;
            }
            else if (_loadingABList.ContainsKey(_hashName)) //在加载中,异步改同步
            {
                abObj = _loadingABList[_hashName];
                abObj._refCount++;
    
                foreach(var dpObj in abObj._depends)
                {
                    LoadAssetBundleSync(dpObj._hashName); //递归依赖项,加载完
                }
    
                DoLoadedCallFun(abObj, false); //强制完成,回调
    
                return abObj;
            }
            else if (_readyABList.ContainsKey(_hashName)) //在准备加载中
            {
                abObj = _readyABList[_hashName];
                abObj._refCount++;
    
                foreach (var dpObj in abObj._depends)
                {
                    LoadAssetBundleSync(dpObj._hashName); //递归依赖项,加载完
                }
    
                string path1 = GetAssetBundlePath(_hashName);
                abObj._ab = AssetBundle.LoadFromFile(path1);
    
                _readyABList.Remove(abObj._hashName);
                _loadedABList.Add(abObj._hashName, abObj);
    
                DoLoadedCallFun(abObj, false); //强制完成,回调
    
                return abObj;
            }
    
            //创建一个加载
            abObj = new AssetBundleObject();
            abObj._hashName = _hashName;
    
            abObj._refCount = 1;
    
            string path = GetAssetBundlePath(_hashName);
            abObj._ab = AssetBundle.LoadFromFile(path);
    
            if(abObj._ab == null)
            {
                try
                {
                    //同步下载解决
                    byte[] bytes = AssetsDownloadMgr.I.DownloadSync(GetFileName(abObj._hashName));
                    if (bytes != null && bytes.Length != 0)
                        abObj._ab = AssetBundle.LoadFromMemory(bytes);
                }
                catch (Exception ex)
                {
                    Debug.LogError("LoadAssetBundleSync DownloadSync" + ex.Message);
                }
            }
            
            //加载依赖项
            string[] dependsData = null;
            if (_dependsDataList.ContainsKey(_hashName))
            {
                dependsData = _dependsDataList[_hashName];
            }
    
            if (dependsData != null && dependsData.Length > 0)
            {
                abObj._dependLoadingCount = 0;
    
                foreach (var dpAssetName in dependsData)
                {
                    var dpObj = LoadAssetBundleSync(dpAssetName);
    
                    abObj._depends.Add(dpObj);
                }
    
            }
    
            _loadedABList.Add(abObj._hashName, abObj);
    
            return abObj;
        }
    
        private void UnloadAssetBundleAsync(string _hashName)
        {
            AssetBundleObject abObj = null;
            if (_loadedABList.ContainsKey(_hashName))
                abObj = _loadedABList[_hashName];
            else if (_loadingABList.ContainsKey(_hashName))
                abObj = _loadingABList[_hashName];
            else if (_readyABList.ContainsKey(_hashName))
                abObj = _readyABList[_hashName];
    
            if (abObj == null)
            {
                string errormsg = string.Format("UnLoadAssetbundle error ! assetName:{0}",_hashName);
                Debug.LogError(errormsg);
                return;
            }
    
            if (abObj._refCount == 0)
            {
                string errormsg = string.Format("UnLoadAssetbundle refCount error ! assetName:{0}", _hashName);
                Debug.LogError(errormsg);
                return;
            }
    
            abObj._refCount--;
    
            foreach (var dpObj in abObj._depends)
            {
                UnloadAssetBundleAsync(dpObj._hashName);
            }
    
            if (abObj._refCount == 0)
            {
                _unloadABList.Add(abObj._hashName, abObj);
            }
        }
    
    
        private AssetBundleObject LoadAssetBundleAsync(string _hashName, AssetBundleLoadCallBack _callFun)
        {
            AssetBundleObject abObj = null;
            if (_loadedABList.ContainsKey(_hashName)) //已经加载
            {
                abObj = _loadedABList[_hashName];
                DoDependsRef(abObj);
                _callFun(abObj._ab);
                return abObj;
            }
            else if(_loadingABList.ContainsKey(_hashName)) //在加载中
            {
                abObj = _loadingABList[_hashName];
                DoDependsRef(abObj);
                abObj._callFunList.Add(_callFun);
                return abObj;
            }
            else if (_readyABList.ContainsKey(_hashName)) //在准备加载中
            {
                abObj = _readyABList[_hashName];
                DoDependsRef(abObj);
                abObj._callFunList.Add(_callFun);
                return abObj;
            }
    
            //创建一个加载
            abObj = new AssetBundleObject();
            abObj._hashName = _hashName;
    
            abObj._refCount = 1;
            abObj._callFunList.Add(_callFun);
    
            //加载依赖项
            string[] dependsData = null;
            if (_dependsDataList.ContainsKey(_hashName))
            {
                dependsData = _dependsDataList[_hashName];
            }
    
            if (dependsData != null && dependsData.Length > 0)
            {
                abObj._dependLoadingCount = dependsData.Length;
    
                foreach(var dpAssetName in dependsData)
                {
                    var dpObj = LoadAssetBundleAsync(dpAssetName,
                        (AssetBundle _ab) =>
                        {
                            if(abObj._dependLoadingCount <= 0)
                            {
                                string errormsg = string.Format("LoadAssetbundle depend error ! assetName:{0}", _hashName);
                                Debug.LogError(errormsg);
                                return;
                            }
    
                            abObj._dependLoadingCount--;
                            
                            //依赖加载完
                            if (abObj._dependLoadingCount == 0 && abObj._request != null && abObj._request.isDone)
                            {
                                DoLoadedCallFun(abObj);
                            }
                        }
                    );
    
                    abObj._depends.Add(dpObj);
                }
    
            }
    
            if (_loadingABList.Count < MAX_LOADING_COUNT) //正在加载的数量不能超过上限
            {
                DoLoad(abObj);
    
                _loadingABList.Add(_hashName, abObj);
            }
            else _readyABList.Add(_hashName, abObj);
    
            return abObj;
        }
    
        private void DoDependsRef(AssetBundleObject abObj)
        {
            abObj._refCount++;
    
            if (abObj._depends.Count == 0) return;
            foreach (var dpObj in abObj._depends)
            {
                DoDependsRef(dpObj); //递归依赖项,加载完
            }
        }
    
        private void DoLoad(AssetBundleObject abObj)
        {
            if (AssetsDownloadMgr.I.IsNeedDownload(GetFileName(abObj._hashName)))
            {//这里是关联下载逻辑,可以实现异步下载再异步加载
                AssetsDownloadMgr.I.DownloadAsync(GetFileName(abObj._hashName), 
                    () =>
                    {
                        string path = GetAssetBundlePath(abObj._hashName);
                        abObj._request = AssetBundle.LoadFromFileAsync(path);
    
                        if (abObj._request == null)
                        {
                            string errormsg = string.Format("LoadAssetbundle path error ! assetName:{0}", abObj._hashName);
                            Debug.LogError(errormsg);
                        }
                    }
                );
            }
            else
            {
                string path = GetAssetBundlePath(abObj._hashName);
                abObj._request = AssetBundle.LoadFromFileAsync(path);
    
                if (abObj._request == null)
                {
                    string errormsg = string.Format("LoadAssetbundle path error ! assetName:{0}", abObj._hashName);
                    Debug.LogError(errormsg);
                }
            }
    
        }
    
        private void DoLoadedCallFun(AssetBundleObject abObj, bool isAsync = true)
        {
            //提取ab
            if(abObj._request != null)
            {
                abObj._ab = abObj._request.assetBundle; //如果没加载完,会异步转同步
                abObj._request = null;
                _loadingABList.Remove(abObj._hashName);
                _loadedABList.Add(abObj._hashName, abObj);
            }
    
            if (abObj._ab == null)
            {
                string errormsg = string.Format("LoadAssetbundle _ab null error ! assetName:{0}", abObj._hashName);
                string path = GetAssetBundlePath(abObj._hashName);
                errormsg += "
     File " + File.Exists(path) + " Exists " + path;
    
                try
                {//尝试读取二进制解决
                    if(File.Exists(path))
                    {
                        byte[] bytes = File.ReadAllBytes(path);
                        if (bytes != null && bytes.Length != 0)
                            abObj._ab = AssetBundle.LoadFromMemory(bytes);
                    }
                }
                catch (Exception ex)
                {
                    Debug.LogError("LoadAssetbundle ReadAllBytes Error " + ex.Message);
                }
    
                if (abObj._ab == null)
                {
                    //同步下载解决
                    byte[] bytes = AssetsDownloadMgr.I.DownloadSync(GetFileName(abObj._hashName));
                    if (bytes != null && bytes.Length != 0)
                        abObj._ab = AssetBundle.LoadFromMemory(bytes);
    
                    if (abObj._ab == null)
                    {//同步下载还不能解决,移除
                        if (_loadedABList.ContainsKey(abObj._hashName)) _loadedABList.Remove(abObj._hashName);
                        else if (_loadingABList.ContainsKey(abObj._hashName)) _loadingABList.Remove(abObj._hashName);
    
                        Debug.LogError(errormsg);
    
                        if (isAsync)
                        {//异步下载解决
                            AssetsDownloadMgr.I.AddDownloadSetFlag(GetFileName(abObj._hashName));
                        }
                    }
                }
            }
    
            //运行回调
            foreach (var callback in abObj._callFunList)
            {
                callback(abObj._ab);
            }
            abObj._callFunList.Clear();
        }
    
        private void UpdateLoad()
        {
            if (_loadingABList.Count == 0) return;
            //检测加载完的
            tempLoadeds.Clear();
            foreach (var abObj in _loadingABList.Values)
            {
                if (abObj._dependLoadingCount == 0 && abObj._request != null && abObj._request.isDone)
                {
                    tempLoadeds.Add(abObj);
                }
            }
            //回调中有可能对_loadingABList进行操作,提取后回调
            foreach (var abObj in tempLoadeds)
            {
                //加载完进行回调
                DoLoadedCallFun(abObj);
            }
            
        }
    
        private void DoUnload(AssetBundleObject abObj)
        {
            //这里用true,卸载Asset内存,实现指定卸载
            if(abObj._ab == null)
            {
                string errormsg = string.Format("LoadAssetbundle DoUnload error ! assetName:{0}", abObj._hashName);
                Debug.LogError(errormsg);
                return;
            }
    
            abObj._ab.Unload(true);
            abObj._ab = null;
        }
    
        private void UpdateUnLoad()
        {
            if (_unloadABList.Count == 0) return;
    
            tempLoadeds.Clear();
            foreach (var abObj in _unloadABList.Values)
            {
                if (abObj._refCount == 0 && abObj._ab != null)
                {//引用计数为0并且已经加载完,没加载完等加载完销毁
                    DoUnload(abObj);
                    _loadedABList.Remove(abObj._hashName);
    
                    tempLoadeds.Add(abObj);
                }
    
                if (abObj._refCount > 0)
                {//引用计数加回来(销毁又瞬间重新加载,不销毁,从销毁列表移除)
                    tempLoadeds.Add(abObj);
                }
            }
    
            foreach(var abObj in tempLoadeds)
            {
                _unloadABList.Remove(abObj._hashName);
            }
        }
    
        private void UpdateReady()
        {
            if (_readyABList.Count == 0) return;
            if (_loadingABList.Count >= MAX_LOADING_COUNT) return;
            
            tempLoadeds.Clear();
            foreach (var abObj in _readyABList.Values)
            {
                DoLoad(abObj);
    
                tempLoadeds.Add(abObj);
                _loadingABList.Add(abObj._hashName, abObj);
    
                if (_loadingABList.Count >= MAX_LOADING_COUNT) break;
            }
    
            foreach (var abObj in tempLoadeds)
            {
                _readyABList.Remove(abObj._hashName);
            }
        }
    
        public void Update()
        {
            UpdateLoad();
            UpdateReady();
            UpdateUnLoad();
        }
    }
     

    整篇文章到这里就结束啦!!!如果对上述的逻辑不是很理解的话,没有关系,上述代码可以无缝嵌入任何一个Unity游戏——就是这么666。

    00


     

  • 相关阅读:
    从尾到头打印链表
    剑指offer
    Codeforces Round #345
    算法入门系列之字符串
    【codenet】代码相似度计算框架调研 -- 把内容与形式分开
    【学习笔记--数据结构】合法的出栈序列与栈混洗
    我的博客即将入驻“云栖社区”,诚邀技术同仁一同入驻。
    【PAT L2-001】最短路计数
    【CF689D Friends and Subsequences】二分搜索,区间查询
    【编译原理】语法分析LL(1)分析法的FIRST和FOLLOW集
  • 原文地址:https://www.cnblogs.com/rollingyouandme/p/14563807.html
Copyright © 2020-2023  润新知