当前位置:   article > 正文

Unity开发(三) AssetBundle同步异步引用计数资源加载管理器_assetbundle 引用计数

assetbundle 引用计数

前言

这篇文章内容巨多,逻辑也复杂,花了4天写出来。(写博客还是费时间啊)
很多设计和逻辑,在脑子中是很清晰的,但用文字表述就会显得很复杂,没有图文对照就更难理解了。

Unity资源加载

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

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

针对AssetBundle 的加载,本文会作讲解,并提供整套方案和代码,
针对Asset 的加载,读者可以参阅Asset同步异步引用计数资源加载管理器
针对GameObject的加载,读者可以参阅Prefab加载自动化管理引用计数管理器

框架

AssetBundle加载技术选型

AssetBundle加载有三套接口,WWWUnityWebRequestAssetBundle,大部分文章都推荐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
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

其实协程本质,就是保留现场的回调函数,内部机制也是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:为什么没有设计异常队列

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

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

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

Yes
Yes
Yes
Update
UpdateLoad
UpdateReady
UpdateUnLoad
遍历正在加载的ab是否加载完成
正在加载的ab总数是否低于上限
遍历引用计数为0的ab是否销毁
运行回调函数
创建新的加载
销毁ab

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

  1. UpdateReady在UpdateLoad后面,可以实现当前帧就创建新的加载,否则要等到下一帧
  2. 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
  • 16

这部分,大部分游戏都大同小异,就是将配置转化成类结构。注意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
  • 16

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

依赖加载——递归&引用计数&队列&回调

依赖加载,是ab加载逻辑里最难最复杂最容易出bug的地方,也是本文的难点。

难点为一下几点:

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

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

1图-初始
2图-加载A
3图-加载E
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/341581
推荐阅读
相关标签
  

闽ICP备14008679号