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

本文详细介绍了Unity中AssetBundle的加载管理,包括AssetBundle加载技术选型,推荐使用,避免协程化加载,利用Update进行资源加载与卸载。文章提出了一种依赖加载的解决方案,涉及递归引用计数、队列和回调管理,确保异步和同步加载的高效协同。同时,文章讨论了资源路径管理和优化策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

这篇文章内容巨多,逻辑也复杂,花了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
	}
}

其实协程本质,就是保留现场的回调函数,内部机制也是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; //准备卸载的列表

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

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;

这部分,大部分游戏都大同小异,就是将配置转化成类结构。注意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节点的引用计数要实现的逻辑

1图-初始
2图-加载A
3图-加载E
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值