zoukankan      html  css  js  c++  java
  • Unity开发(三) AssetBundle同步异步引用计数资源加载管理器

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
    本文链接:https://blog.csdn.net/wowo1gt/article/details/100561236
    文章目录
    前言
    AssetBundle加载技术选型
    加载去协程化
    Update才是王道
    外部接口
    加载依赖关系配置
    加载节点数据结构
    依赖加载——递归&引用计数&队列&回调
    我要异步加载和同步加载一起用
    资源路径管理——字符串转hash
    大招——资源管理器完整代码
    前言
    这篇文章内容巨多,逻辑也复杂,花了4天写出来。(写博客还是费时间啊)
    很多设计和逻辑,在脑子中是很清晰的,但用文字表述就会显得很复杂,没有图文对照就更难理解了。

    AssetBundle加载技术选型
    AssetBundle加载有三套接口,WWW,UnityWebRequest和AssetBundle,大部分文章都推荐AssetBundle,本人也推荐。

    关于AssetBundle的加载原理和用法之类的基础知识读者自己百度学习,这边就不进行大量描述了

    前两者都要经历将整个文件的二进制流下载或读取到内存中,然后对这段内存文件进行ab资源的读取解析操作,而AssetBundle可以只读取存储于本地的ab文件的头部部分,在需要的情况下,读取ab中的数据段部分(Asset资源)。

    所以AssetBundle相对的优势是

    不进行下载(不占用下载缓存区内存)
    不读取整个文件到内存(不占用原始文件二进制内存)
    读取非压缩或LZ4的ab,只读取ab的文件头(约5kb/个)
    同步异步加载并行可用
    所以,从内存和效率方面,AssetBundle会是目前最优解,而使用非压缩或LZ4读者自己评断(推荐LZ4)

    AssetBundle加载方式最重要的接口(接口用法读者自己百度学习)
    AssetBundle.LoadFromFile 从本地文件同步加载ab
    AssetBundle.LoadFromFileAsync 从本地文件异步加载ab
    AssetBundle.Unload 卸载,注意true和false区别
    AssetBundle.LoadAsset 从ab同步加载Asset
    AssetBundle.LoadAssetAsync 从ab异步加载Asset

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

    大量依赖ab等待加载,逻辑复杂
    ab加载状态切换的复杂化
    协程顺序的不确定性,增加难度
    ab卸载和加载同时进行处理难
    ab同步和异步同时进行处理难
    协程在某些情况确实可以让开发简单化,但在耦合高的代码中非常容易导致逻辑复杂化。
    这里笔者提供一种使用Update去协程化的方案。
    我们都知道,使用协程的地方,大部分都是需要等待线程返回逻辑的,而这样的等待逻辑可以使用Update每帧访问的方式,确定线程逻辑是否结束

    AssetBundleCreateRequest request = AssetBundle.LoadFromFileAsync(path);

    IEnumerator LoadAssetBundle()
    {
    yield return request;
    //do something
    }

    转变为

    void Update()
    {
    if(request.isDone)
    {
    //do something
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    其实协程本质,就是保留现场的回调函数,内部机制也是update的每帧遍历(具体参见IEnumerator原理)。

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

    UpdateReady
    UpdateLoad
    UpdateUnLoad
    准备队列
    加载队列
    完成队列
    销毁队列
    代码如下

    private Dictionary<string, AssetBundleObject> _readyABList; //预备加载的列表
    private Dictionary<string, AssetBundleObject> _loadingABList; //正在加载的列表
    private Dictionary<string, AssetBundleObject> _loadedABList; //加载完成的列表
    private Dictionary<string, AssetBundleObject> _unloadABList; //准备卸载的列表
    1
    2
    3
    4
    队列之间,队列成员的转移需要一个触发点,而这样的触发点如果都写在加载和销毁逻辑里,耦合度过高,而且逻辑复杂还容易出错。

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

    一般资源加载,都是默认资源是存在的
    资源如果不存在,一定是策划没有把资源放进去(嗯,一定是这样)
    设计上是加载了总依赖关系的Mainfest,是对文件存在性可以进行判断的
    从性能的角度,通过File.exists()来判断文件存在性,是效率低下的方式
    代码中对异常是有处理的,会有重复加载,下载和修复完整性的逻辑
    笔者很喜欢的一种设计,就是通过Update来降低耦合度,这种方式代码清晰,逻辑简单,但缺点也很明显,丢失原始现场。

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

    Yes
    Yes
    Yes
    Update
    UpdateLoad
    UpdateReady
    UpdateUnLoad
    遍历正在加载的ab是否加载完成
    正在加载的ab总数是否低于上限
    遍历引用计数为0的ab是否销毁
    运行回调函数
    创建新的加载
    销毁ab
    TIP:为什么Update里三个函数的运行顺序跟队列转移顺序不一样?

    UpdateReady在UpdateLoad后面,可以实现当前帧就创建新的加载,否则要等到下一帧
    UpdateUnLoad放最后,是因为正在加载的资源要等到加载完才能卸载
    外部接口
    根据上面的逻辑,很容易设计下面的接口逻辑

    外部接口
    加载依赖关系
    异步
    同步
    卸载
    刷新
    每帧调用
    LoadMainfest
    LoadAsync
    LoadSync
    Unload
    Update
    加载管理器
    主线程
    加载依赖关系配置
    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;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    这部分,大部分游戏都大同小异,就是将配置转化成类结构。注意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>(); //依赖项
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    加载节点的数据结构不复杂,看代码就很容易理解。

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

    难点为一下几点:

    加载时,root节点和depend节点引用计数的正确增加
    卸载时,root节点和depend节点引用计数的正确减少
    还未加载,准备加载,正在加载,已经加载节点关系处理
    节点加载完成,回调逻辑的高效和正确性
    我们来一一分解
    首先,看一下ab节点的引用计数要实现的逻辑

    1图-初始
    2图-加载A
    3图-加载E
    A+0
    B+0
    C+0
    D+0
    E+0
    A+1
    B+0
    C+1
    D+1
    E+0
    A+1
    B+0
    C+1
    D+2
    E+1
    4图-卸载A
    5图-加载B
    6图-卸载E
    A+0
    B+0
    C+0
    D+1
    E+1
    A+0
    B+1
    C+1
    D+2
    E+1
    A+0
    B+1
    C+1
    D+1
    E+0
    注: 上图显示加载和销毁都需要递归标记依赖节点的依赖节点
    TIP:为什么引用计数一定要递归标记所有子节点?

    我们需要确定一个节点是否需要销毁,是通过引用计数是否为零来判断的,很多语言使用的内存回收机制就是引用计数。
    如果只标记当前节点和其一层依赖项,当其依赖项也作为主加载节点,我就没办法判断二层依赖节点是否需要销毁了。
    例如按上述逻辑,

    加载A,标记A+1,C+1
    加载C,标记A+1,C+2,D+1
    卸载C,标记A+1,C+1,D+0
    这里就会卸载D,而实际上,D仍然是需要保留的,不能卸载
    所以,带依赖关系的引用计数,需要递归标记所有子节点,才能确认任意一个节点是否需要卸载。
    每次加载,都要递归标记,会不会有效率问题?
    很幸运,在绝大多数情况,依赖节点关系不会超过三层,依赖节点总数量不超过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
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    上述代码构造了引用计数,递归,加入队列。理解起来其实不难,难在写出符合设想的逻辑代码。

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

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

    当一个节点引用计数由0变为1时,需要创建ab节点,加入准备队列或加载队列。
    当一个节点加载完ab,将其加入完成队列
    当一个节点引用计数由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);
    }
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    从这里,上文已经完成了整个异步加载的逻辑,已经实现创建到销毁的代码。但异步加载还有一个问题没有解决——判读ab节点加载完成。

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

    图1-递归判定
    判定
    判定
    判定
    A+1
    B+0
    C+1
    D+1
    E+1
    注:圆角方形表示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;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    很明显的弊端,上述代码需要关心子依赖节点以及孙依赖节点,这样的代码不管是效率还是设计,都不是一种优秀的方式。

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

    图1-同时加载A和B
    图2-D加载完
    图3-C加载完
    回调
    A+1
    B+1
    C+2
    D+2
    E+2
    A+1
    B+1
    C+2
    D+2
    E+2
    A+1
    B+1
    C+2
    D+2
    E+2
    图4-B加载完
    图5-E加载完
    图6-A加载完
    回调
    回调
    回调
    A+1
    B+1
    C+2
    D+2
    E+2
    A+1
    B+1
    C+2
    D+2
    E+2
    A+1
    B+1
    C+2
    D+2
    E+2
    注:圆角方形表示ab自身加载完成,箭头表示依赖关系
    上图,会按以下回调逻辑

    同时加载A和B,标记引用计数
    D自身加载完,会回调C;
    C自身没有加载完,然后C会记录子依赖加载情况
    C自身加载完,但子依赖没加载完,不操作
    B自身加载完,但子依赖没加载完,不操作
    E自身加载完,会回调C;
    C的子依赖加载完了,C自己也加载完了,回调A和B;
    A自己没加载完,不操作;
    B自己已经加载完了,子依赖也加载完了,B完成加载
    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);
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    到这里,超级复杂的依赖加载问题就解决啦,我们可以欢快地开始使用异步加载啦!!!

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

    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;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    写出同步加载代码后,你会发现难点就一个——正在加载的节点如何强制加载完。
    我们这里有四个队列,准备队列、加载队列、完成队列和销毁队列。

    销毁队列不用管,是一个标记队列,用于延迟卸载,不影响加载逻辑
    完成队列也很简单,只用增加引用计数就可以了
    准备队列还没开始加载,只需要解决引用计数和依赖关系回调
    加载队列正在加载中,除了解决引用计数和依赖关系回调,还要解决ab异步转同步的问题
    总结一下,就是三个问题——引用计数、依赖关系回调和ab异步转同步

    引用计数可以很简单啦,递归一下所有依赖节点,都+1就解决了。
    注意:同步加载和异步加载会导致引用计数是2次,需要调用2次Unload才会卸载

    依赖关系回调需要强制手动运行被依赖项的回调函数,然后改变队列

    ab异步转同步,很幸运的,Unity提供了同步转异步的方式

    在异步请求一个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;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    好啦,到这里,同步加载也完美解决啦

    资源路径管理——字符串转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));
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    资源管理,一定逃不开的路径管理,上面的三个函数,封装了必要的路径需求,读者有需求的话,可以使用针对项目的路径管理方案,这边笔者就当抛砖引玉啦。

    这边再提供一个内存优化方案,将_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));
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    使用上述代码, 需要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();
    }
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418
    419
    420
    421
    422
    423
    424
    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    438
    439
    440
    441
    442
    443
    444
    445
    446
    447
    448
    449
    450
    451
    452
    453
    454
    455
    456
    457
    458
    459
    460
    461
    462
    463
    464
    465
    466
    467
    468
    469
    470
    471
    472
    473
    474
    475
    476
    477
    478
    479
    480
    481
    482
    483
    484
    485
    486
    487
    488
    489
    490
    491
    492
    493
    494
    495
    496
    497
    498
    499
    500
    501
    502
    503
    504
    505
    506
    507
    508
    509
    510
    511
    512
    513
    514
    515
    516
    517
    518
    519
    520
    521
    522
    523
    524
    525
    526
    527
    528
    529
    530
    531
    532
    533
    534
    535
    536
    537
    538
    539
    540
    541
    542
    543
    544
    545
    546
    547
    548
    549
    550
    551
    552
    553
    554
    整篇文章到这里就结束啦!!!如果对上述的逻辑不是很理解的话,没有关系,上述代码可以无缝嵌入任何一个Unity游戏——就是这么666。
    ————————————————
    版权声明:本文为CSDN博主「无为战士」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/wowo1gt/article/details/100561236

  • 相关阅读:
    noip模拟赛 梦想
    noip模拟赛 水题
    noip模拟赛 猜数字
    Java基础知识强化64:基本类型包装类的引入
    Java基础知识强化63:Arrays工具类之方法源码解析
    Java基础知识强化62:Arrays工具类之概述和使用
    Java基础知识强化61:经典查找之 常见查找算法小结
    Java基础知识强化60:经典查找之二分查找
    Java基础知识强化59:String(字符串)和其他类型的相互转化
    Java基础知识强化58:经典排序之二叉树排序(BinaryTreeSort)
  • 原文地址:https://www.cnblogs.com/nafio/p/11653939.html
Copyright © 2011-2022 走看看