Unity资源加载
Unity资源类型,按加载流程顺序,有三种
AssetBundle
资源以压缩包文件存在(Resources目录下资源打成包体后也是以ab格式存在)Asset
资源在内存中的存在格式GameObject
针对Prefab导出的Asset,可实例化
针对AssetBundle
的加载,本文会作讲解,并提供整套方案和代码,
针对Asset
的加载,写在 ( 资源管理器 01 )Asset 同步异步引用计数资源加载管理器 里;
针对GameObject
的加载,写在( 资源管理器 03 )prefab 加载自动化管理引用计数器 里;
框架
AssetBundle加载技术选型
AssetBundle加载有三套接口,WWW
,UnityWebRequest
和 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