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

mac2024-03-10  48

版权声明:本文为博主原创文章,遵循 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 从本地文件同步加载abAssetBundle.LoadFromFileAsync 从本地文件异步加载abAssetBundle.Unload 卸载,注意true和false区别AssetBundle.LoadAsset 从ab同步加载AssetAssetBundle.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 }}1234567891011121314151617其实协程本质,就是保留现场的回调函数,内部机制也是update的每帧遍历(具体参见IEnumerator原理)。

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

UpdateReadyUpdateLoadUpdateUnLoad准备队列加载队列完成队列销毁队列代码如下

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

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

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

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

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

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

外部接口加载依赖关系异步同步卸载刷新每帧调用LoadMainfestLoadAsyncLoadSyncUnloadUpdate加载管理器主线程加载依赖关系配置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;123456789101112131415这部分,大部分游戏都大同小异,就是将配置转化成类结构。注意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>(); //依赖项}123456789101112131415加载节点的数据结构不复杂,看代码就很容易理解。

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

难点为一下几点:

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

1图-初始2图-加载A3图-加载EA+0B+0C+0D+0E+0A+1B+0C+1D+1E+0A+1B+0C+1D+2E+14图-卸载A5图-加载B6图-卸载EA+0B+0C+0D+1E+1A+0B+1C+1D+2E+1A+0B+1C+1D+1E+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;}1234567891011121314151617181920212223242526272829303132333435363738上述代码构造了引用计数,递归,加入队列。理解起来其实不难,难在写出符合设想的逻辑代码。

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

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

当一个节点引用计数由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); }}

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

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

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

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

图1-同时加载A和B图2-D加载完图3-C加载完回调A+1B+1C+2D+2E+2A+1B+1C+2D+2E+2A+1B+1C+2D+2E+2图4-B加载完图5-E加载完图6-A加载完回调回调回调A+1B+1C+2D+2E+2A+1B+1C+2D+2E+2A+1B+1C+2D+2E+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); }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566到这里,超级复杂的依赖加载问题就解决啦,我们可以欢快地开始使用异步加载啦!!!

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

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;}1234567891011121314151617181920212223242526272829写出同步加载代码后,你会发现难点就一个——正在加载的节点如何强制加载完。我们这里有四个队列,准备队列、加载队列、完成队列和销毁队列。

销毁队列不用管,是一个标记队列,用于延迟卸载,不影响加载逻辑完成队列也很简单,只用增加引用计数就可以了准备队列还没开始加载,只需要解决引用计数和依赖关系回调加载队列正在加载中,除了解决引用计数和依赖关系回调,还要解决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; }12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667好啦,到这里,同步加载也完美解决啦

资源路径管理——字符串转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));}12345678910111213141516171819资源管理,一定逃不开的路径管理,上面的三个函数,封装了必要的路径需求,读者有需求的话,可以使用针对项目的路径管理方案,这边笔者就当抛砖引玉啦。

这边再提供一个内存优化方案,将_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));}1234567891011121314151617181920212223使用上述代码, 需要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 += "\n 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(); }}

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554整篇文章到这里就结束啦!!!如果对上述的逻辑不是很理解的话,没有关系,上述代码可以无缝嵌入任何一个Unity游戏——就是这么666。————————————————版权声明:本文为博主「无为战士」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/wowo1gt/article/details/100561236

最新回复(0)