前言 本文主要用于总结CatAsset 在Runtime部分的设计思路
代码结构如下:
CatJson:包含用于进行Json序列化与反序列化的代码
Database:包含清单信息与运行时信息的数据结构
Extensions:包含扩展代码
Misc:包含各类杂项代码
Pool:包含游戏对象池与引用池代码
TaskSystem:包含支持CatAsset Runtime核心功能运转的任务系统代码
Updatable:包含用于可更新模式下的版本检查器与资源组更新器代码
资源清单与运行时信息 资源清单 资源清单(CatAssetManifest) 记录了在Editor 下构建出的所有资源包(BundleManifest
)及其中资源(AssetManifest
)的相关信息
对于内置资源而言只有通过CatAssetManifest
能够读取到相关信息的,才是可被CatAsset管理的
运行时信息 对应BundleManifest
和AssetManifest
,有BundleRuntimeInfo
和AssetRuntimeInfo
,用于保存在游戏运行中产生的相关行为的信息,如资源实例、引用计数 等
任务系统 CatAsset的Runtime中大部分核心功能都是基于任务系统(TaskSystem) 实现的,此系统主要用于解决下列异步运行相关需求:
异步运行间的依赖等待
异步运行到一半需要取消
目标相同的异步运行的合并
异步运行频率的限制
异步运行的优先级
诸如加载、卸载、更新 等操作都封装为了对应的任务(Task) ,被放置于任务组(TaskGroup) 中,由任务运行器(TaskRunner) 根据优先级 对TaskGroup 进行管理
任务 任务(Task) 是TaskSystem 中实际的逻辑运行者,从抽象基类BaseTask
、接口ITask
派生
接口ITask
定义如下:
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 public interface ITask : IReference { TaskRunner Owner { get ; } int GUID { get ; } string Name { get ; } TaskState State { get ; set ; } float Progress { get ; } public int MergedTaskCount { get ; } void MergeTask (ITask task ) ; void Run ( ) ; void Update ( ) ; void Cancel ( ) ; }
任务状态 任务状态(TaskState) 表示此Task
的内部运行情况,其定义如下:
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 public enum TaskState { Free, Waiting, Running, Finished, }
通常来说
未开始运行的Task
为Free 状态
需要等待其他Task
运行才能开始运行的Task
为Waiting 状态
正在运行的Task
为Running 状态
运行结束的Task
为Finished 状态
任务合并 对于同名的Task
,TaskSystem 会将其放入到已有Task
的MergedTaskList
中,已有Task
运行结束后会将运行结果回调给MergedTaskList
中的所有Task
任务取消 Task
被创建后会获得一个全局唯一的GUID
,对于支持取消操作的Task
,可通过此GUID
进行取消,被取消的Task
即便运行结束了也不会回调给使用者运行结果
任务运行器与任务组 任务运行器 任务运行器(TaskRunner) 是所有Task
运行的起点
TaskRunner
在初始化时会按照预定义的任务优先级(TaskPriority) 去创建对应优先级的任务组(TaskGroup) ,并按照优先级去轮询TaskGroup
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 public enum TaskPriority { VeryLow = 0 , Low = 1 , Middle = 2 , Height = 3 , VeryHeight = 4 , }
1 2 3 4 5 6 7 8 9 10 11 public TaskRunner ( ) { int priorityNum = Enum.GetNames(typeof (TaskPriority)).Length; for (int i = 0 ; i < priorityNum; i++) { taskGroups.Add(new TaskGroup((TaskPriority)i)); } }
任务组 任务组(TaskGroup) 中保存了以此TaskGroup
对应优先级来运行的Task
对象,会在每次Task
运行后,根据Task
的State
来决定是否增加当前运行中的任务计数
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 public bool Run ( ) { int index = nextRunningTaskIndex; nextRunningTaskIndex++; ITask task = runningTasks[index]; try { if (task.State == TaskState.Free) { task.Run(); } task.Update(); } catch (Exception e) { task.State = TaskState.Finished; throw ; } finally { switch (task.State) { case TaskState.Finished: waitRemoveTasks.Add(index); break ; }; } switch (task.State) { case TaskState.Running: case TaskState.Finished: return true ; } return false ; }
频率限制 TaskRunner
中定义了单帧最大任务运行数量,在每次Update
时会根据TaskGroup
返回结果来统计当前帧运行中的任务数量,如果达到了限制数量就会停止对TaskGroup
的运行
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 public void Update ( ) { int curRanCount = 0 ; for (int i = taskGroups.Count - 1 ; i >= 0 ; i--) { TaskGroup group = taskGroups[i]; group .PreRun(); while (curRanCount < MaxRunCount && group .CanRun) { if (group .Run()) { curRanCount++; } } group .PostRun(); } }
资源加载 资源类别的判断 在CatAsset开发总结:Editor篇 中提及过3种支持的资源类别:
内置资源包资源
内置原生资源
外置原生资源
CatAsset的LoadAsse<T>
接口统一了3种类别的资源加载,使用者在加载资源时无需关心其所加载的资源的具体类别
内部判断资源类别的代码如下
编辑器资源模式 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 public static AssetCategory GetAssetCategoryWithEditorMode (string assetName, Type assetType ) { if (assetName.StartsWith("Assets/" )) { if (typeof (UnityEngine.Object).IsAssignableFrom(assetType) || assetType == typeof (object )) { return AssetCategory.InternalBundleAsset; } else { return AssetCategory.InternalRawAsset; } } else { return AssetCategory.ExternalRawAsset; } }
(注意:因为编辑器资源模式下无法准确判断以Assets/开头的路径加载资源时,是要加载 内置资源包资源 还是加载内置原生资源 ,所以只能规定当加载类型为UnityEngine.Object
及其派生类型或object
类型时视为内置资源包资源加载,不过这并不影响最终加载结果)
非编辑器资源模式 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 public static AssetCategory GetAssetCategory (string assetName ) { if (!assetName.StartsWith("Assets/" ) && !assetName.StartsWith("Packages/" )) { return AssetCategory.ExternalRawAsset; } AssetRuntimeInfo assetRuntimeInfo = CatAssetDatabase.GetAssetRuntimeInfo(assetName); if (assetRuntimeInfo == null ) { Debug.LogError($"GetAssetCategory调用失败,资源{assetName} 的AssetRuntimeInfo为空" ); return default ; } if (assetRuntimeInfo.BundleManifest.IsRaw) { return AssetCategory.InternalRawAsset; } return AssetCategory.InternalBundleAsset; }
资源的加载 在得到资源类别后就可以进行后续的加载行为了
编辑器资源模式 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 AssetCategory category; #if UNITY_EDITOR if (IsEditorMode) { category = Util.GetAssetCategoryWithEditorMode(assetName, typeof (T)); object asset; try { if (category == AssetCategory.InternalBundleAsset) { Type assetType = typeof (T); if (assetType == typeof (object )) { assetType = typeof (Object); } asset = UnityEditor.AssetDatabase.LoadAssetAtPath(assetName,assetType); } else { if (category == AssetCategory.ExternalRawAsset) { assetName = Util.GetReadWritePath(assetName); } asset = File.ReadAllBytes(assetName); } } catch (Exception e) { callback?.Invoke(false , default ,default , userdata); throw ; } LoadAssetResult result = new LoadAssetResult(asset, category); callback?.Invoke(true , result.GetAsset<T>(),result, userdata); return default ; } #endif
无论加载何种类别资源,最终都被封装进了资源加载结果(LoadAssetResult) 中,并通过result.GetAsset<T>
接口将其回调给使用者
资源加载结果 资源加载结果(LoadAssetResult) 是CatAsset对3种类别资源的统一封装,其代码如下:
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 public struct LoadAssetResult { private object asset; public AssetCategory Category { get ; } public LoadAssetResult (object asset, AssetCategory category ) { this .asset = asset; Category = category; } public object GetAsset ( ) { return asset; } public T GetAsset <T >( ) { if (asset == null ) { return default ; } Type type = typeof (T); if (type == typeof (object )) { return (T)asset; } switch (Category) { case AssetCategory.InternalBundleAsset: if (typeof (UnityEngine.Object).IsAssignableFrom(type)) { return (T) asset; } else { Debug.LogError($"LoadAssetResult.GetAsset<T>调用失败,资源类别为{Category} ,但是T为{type} " ); return default ; } case AssetCategory.InternalRawAsset: case AssetCategory.ExternalRawAsset: if (type == typeof (byte [])) { return (T)asset; } CustomRawAssetConverter converter = CatAssetManager.GetCustomRawAssetConverter(type); if (converter == null ) { Debug.LogError($"LoadAssetResult.GetAsset<T>调用失败,没有注册类型{type} 的CustomRawAssetConverter" ); return default ; } object convertedAsset = converter((byte [])asset); return (T) convertedAsset; } return default ; } }
LoadAssetResult
的主要功能就是在调用GetAsset<T>
时根据资源类别和指定的类型进行不同处理:
如果指定类型为object
,直接返回原始资源实例
如果资源类别为内置资源包资源 ,并且指定类型为UnityEngine.Object
及其派生类型,则按指定类型返回原始资源实例,否则报错(因为资源包资源只能以UnityEngine.Object
及其派生类型加载)
如果资源类别为内置/外置原生资源 ,并且指定类型为byte[]
,直接返回原始资源实例(因为原生资源都是按照byte[]
加载的),否则使用已注册的自定义原生资源转换器(CustomRawAssetConverter) 将byte[]
转换为指定类型并返回
自定义原生资源转换器 想要统一对3种类别资源的使用,重点就在于统一资源包资源与原生资源的使用 ,在加载调用代码保持不变的情况下,即使所加载的资源从资源包资源变成了内置/外置原生资源,也能保证后续逻辑的正常运行
比如:
1 2 string assetName = ???;CatAssetManager.LoadAsset<Sprite>(assetName, null , callback);
CatAsset所保证的即是无论上述代码中的assetName
表示一个内置资源包资源,还是表示一个内置/外置原生资源,callback
中的逻辑都无需关心这件事,而只需要处理加载得到的Sprite
对象
想要做到这点就需要使用自定义原生资源转换器(CustomRawAssetConverter) ,其是一个委托类型,将byte[]
转换为object
,定义如下:
1 2 3 4 public delegate object CustomRawAssetConverter (byte [] bytes ) ;
CatAsset默认提供了对Texture2D
、Sprite
、TextAsset
的转换器:
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 static CatAssetManager ( ){ RegisterCustomRawAssetConverter(typeof (Texture2D),(bytes => { Texture2D texture2D = new Texture2D(0 , 0 ); texture2D.LoadImage(bytes); return texture2D; })); RegisterCustomRawAssetConverter(typeof (Sprite),(bytes => { Texture2D texture2D = new Texture2D(0 , 0 ); texture2D.LoadImage(bytes); Sprite sp = Sprite.Create(texture2D, new Rect(0 , 0 , texture2D.width, texture2D.height), Vector2.zero); return sp; })); RegisterCustomRawAssetConverter(typeof (TextAsset),(bytes => { string text = Encoding.UTF8.GetString(bytes); TextAsset textAsset = new TextAsset(text); return textAsset; })); }
非编辑器资源模式 在非编辑器资源模式下会根据资源类别使用不同的Task 处理加载任务
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 category = Util.GetAssetCategory(assetName); if (category == AssetCategory.ExternalRawAsset){ CatAssetDatabase.TryCreateExternalRawAssetRuntimeInfo(assetName); } switch (category){ case AssetCategory.None: callback?.Invoke(false , default ,default , userdata); return default ; case AssetCategory.InternalBundleAsset: LoadBundleAssetTask<T> loadBundleAssetTask = LoadBundleAssetTask<T>.Create(loadTaskRunner, assetName, userdata, callback); loadTaskRunner.AddTask(loadBundleAssetTask, priority); return loadBundleAssetTask.GUID; case AssetCategory.InternalRawAsset: case AssetCategory.ExternalRawAsset: LoadRawAssetTask<T> loadRawAssetTask = LoadRawAssetTask<T>.Create(loadTaskRunner,assetName,category,userdata,callback); loadTaskRunner.AddTask(loadRawAssetTask, priority); return loadRawAssetTask.GUID; }
在加载外置原生资源 时,会尝试为此外置原生资源创建对应的清单信息和运行时信息,以进行统一管理
资源包资源加载任务 **资源包资源加载任务(LoadBundleAssetTask)**是所有Task
中最为复杂的,其将整个加载过程分为了6个阶段进行处理:
BundleLoading(资源包加载中)
BundleLoaded(资源包加载结束)
DependenciesLoading(依赖资源加载中)
DependenciesLoaded(依赖资源加载结束)
AssetLoading(资源加载中)
AssetLoaded(资源加载结束)
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 public override void Update ( ){ switch (loadBundleAssetState) { case LoadBundleAssetState.BundleLoading: CheckStateWithBundleLoading(); break ; case LoadBundleAssetState.BundleLoaded: CheckStateWithBundleLoaded(); break ; case LoadBundleAssetState.DependenciesLoading: CheckStateWithDependenciesLoading(); break ; case LoadBundleAssetState.DependenciesLoaded: CheckStateWithDependenciesLoaded(); break ; case LoadBundleAssetState.AssetLoading: CheckStateWithAssetLoading(); break ; case LoadBundleAssetState.AssetLoaded: CheckStateWithAssetLoaded(); break ; } }
原生资源加载任务 无论是内置还是外置的原生资源,都统一通过原生资源加载任务(LoadRawAssetTask) 进行处理
由于原生资源无需处理资源包与依赖资源的加载,所以实现也比较简单,只被划分为了2个阶段:
Loading(资源加载中)
Loaded(资源加载结束)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public override void Update ( ){ switch (loadRawAssetState) { case LoadRawAssetState.Loading: CheckStateWithLoading(); break ; case LoadRawAssetState.Loaded: CheckStateWithLoaded(); break ; } }
资源卸载 资源的卸载需要调用UnloadAsset
接口传入原始资源实例,并且卸载要与加载成对的调用 ,资源才能被正确卸载
引用计数规则 CatAsset通过引用计数来管理资源的卸载,其计数规则如下:
每次调用LoadAsset
或UnloadAsset
时,将目标资源的引用计数+1或-1
当1个资源的引用计数从0变为1或从1变为0时,其直接依赖的资源的引用计数+1或-1
举例来说,假设有资源A依赖B,B依赖C ,那么调用1次LoadAsset(A)
后的引用计数状态为:
再调用2次LoadAsset(A)
最后调用1次LoadAsset(B)
现在开始按照加载调用的次数来调用卸载
调用3次UnloadAsset(A)
由于A的引用计数变为了0,导致B的引用计数被-1
再调用1次UnloadAsset(B)
由于B的引用计数变为了0,导致C的引用计数被-1,此时A、B、C的引用计数都正确归0了
为什么不在每次加载或卸载资源时,都增加或减少目标资源所有依赖资源的引用计数? 这样做也是可以的
目前的计数规则方案采取了【 主资源通过依赖加载,只对其直接依赖资源最多贡献1个引用计数】 的原则
目的在于可以通过将1个资源的引用计数减去它被依赖加载的次数,得到它被主动加载的次数,从而方便查出一些因为使用者没有成对调用加载/卸载接口导致的资源无法被卸载的问题
用上面的例子来说,B的引用计数为2,因为被依赖加载的次数为1,两者相减就知道了B被主动加载的次数为1
资源包资源的卸载 每当资源包资源的引用计数从0变为1或从1变为0时,就会从此资源所在资源包的使用中资源记录(UsedAssets) 添加或删除
而当资源包的UsedAssets
为空时,就会通过资源包卸载任务(UnloadBundleTask) 开始卸载倒计时
在倒计时过程中,如果UsedAssets
不为空,会马上结束Task
的运行,反之则会在倒计时结束后,通过AssetBundle.Unload(true)
来将资源包及其中已加载的资源真正的从内存中删除
原生资源的卸载 对于原生资源而言,由于加载的只是byte[]
对象,所以卸载也只是将缓存的byte[]
对象引用置空而已
资源更新 资源版本检查 要进行资源的更新,需要先通过读取资源清单文件以进行版本检查
CatAsset通过检查只读区、读写区、远端 三方的资源清单进行版本对比
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public static void CheckVersion (OnVersionChecked callback ){ if (isChecking) { return ; } isChecking = true ; onVersionChecked = callback; string readOnlyManifestPath = Util.GetReadOnlyPath(Util.ManifestFileName); string readWriteManifestPath = Util.GetReadWritePath(Util.ManifestFileName); string remoteManifestPath = Util.GetRemotePath(Util.ManifestFileName); CatAssetManager.CheckUpdatableManifest(readOnlyManifestPath,CheckReadOnlyManifest); CatAssetManager.CheckUpdatableManifest(readWriteManifestPath,CheckReadWriteManifest); CatAssetManager.CheckUpdatableManifest(remoteManifestPath, CheckRemoteManifest); }
在三方资源清单都读取到后,会为资源清单里记录的每一条资源清单信息建立对应的检查信息(CheckInfo) ,并将资源清单信息赋值到此CheckInfo
中
以检查只读区资源清单的回调方法为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private static void CheckReadOnlyManifest (bool success, UnityWebRequest uwr, object userdata ){ if (!success) { isReadOnlyLoaded = true ; RefreshCheckInfos(); return ; } CatAssetManifest manifest = JsonParser.ParseJson<CatAssetManifest>(uwr.downloadHandler.text); foreach (BundleManifestInfo item in manifest.Bundles) { CheckInfo checkInfo = GetOrAddCheckInfo(item.RelativePath); checkInfo.ReadOnlyInfo = item; } isReadOnlyLoaded = true ; RefreshCheckInfos(); }
而在三方资源清单都读取完毕后,就会开始刷新CheckInfo
的版本检查状态(CheckState) ,然后根据CheckState
进行后续处理
版本检查状态 CatAsset中定义了4种版本检查状态:
NeedUpdate(需要更新)
InReadWrite(最新版本存在于读写区)
InReadOnly(最新版本存在于只读区)
Disuse(已废弃)
其计算规则如下:
如果此资源包不存在远端信息,则State
为Disuse
,并需要删除读写区那份
如果此资源包存在只读区信息,且和远端信息一致,则State
为InReadOnly
,并需要删除读写区那份
如果此资源包存在读写区信息,且和远端信息一致,则State
为InReadWrite
如果此资源包存在远端信息,但本地不存在,或本地信息与远端不一致,则State
为NeedUpdate
,并需要删除读写区那份
具体代码如下:
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 public class CheckInfo { public string Name; public CheckState State; public bool NeedRemove; public BundleManifestInfo ReadOnlyInfo; public BundleManifestInfo ReadWriteInfo; public BundleManifestInfo RemoteInfo; public CheckInfo (string name ) { Name = name; } public void RefreshState ( ) { if (RemoteInfo == null ) { State = CheckState.Disuse; NeedRemove = ReadWriteInfo != null ; return ; } if (ReadOnlyInfo != null && ReadOnlyInfo.Equals(RemoteInfo)) { State = CheckState.InReadOnly; NeedRemove = ReadWriteInfo != null ; return ; } if (ReadWriteInfo != null && ReadWriteInfo.Equals(RemoteInfo)) { State = CheckState.InReadWrite; NeedRemove = false ; return ; } State = CheckState.NeedUpdate; NeedRemove = ReadWriteInfo != null ; } }
刷新资源组与更新器信息 在计算过CheckInfo的CheckState后,会根据CheckState刷新此资源的资源组信息(GroupInfo)及此资源的资源组更新器(GroupUpdater)
规则如下:
对于State
不为Disuse
的资源,会添加到资源组的远端资源包信息中
如果State
为NeedUpdate
,会添加到对应的资源组更新器中
如果State
为InReadWrite
或InReadOnly
,会添加到资源组的本地资源包信息中
如果此资源需要删除,则会从读写区删除,并重新生成读写区资源清单文件
具体代码如下:
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 private static void RefreshCheckInfos ( ){ if (!isReadOnlyLoaded || !isReadWriteLoaded || !isRemoteLoaded) { return ; } int totalCount = 0 ; long totalLength = 0 ; bool needGenerateReadWriteManifest = false ; foreach (KeyValuePair<string ,CheckInfo> pair in checkInfoDict) { CheckInfo checkInfo = pair.Value; checkInfo.RefreshState(); if (checkInfo.State != CheckState.Disuse) { GroupInfo groupInfo = CatAssetDatabase.GetOrAddGroupInfo(checkInfo.RemoteInfo.Group); groupInfo.AddRemoteBundle(checkInfo.RemoteInfo.RelativePath); groupInfo.RemoteCount++; groupInfo.RemoteLength += checkInfo.RemoteInfo.Length; } switch (checkInfo.State) { case CheckState.NeedUpdate: totalCount++; totalLength += checkInfo.RemoteInfo.Length; GroupUpdater groupUpdater = CatAssetUpdater.GetOrAddGroupUpdater(checkInfo.RemoteInfo.Group); groupUpdater.AddUpdateBundle(checkInfo.RemoteInfo); groupUpdater.TotalCount++; groupUpdater.TotalLength += checkInfo.RemoteInfo.Length; break ; case CheckState.InReadWrite: GroupInfo groupInfo = CatAssetDatabase.GetOrAddGroupInfo(checkInfo.ReadWriteInfo.Group); groupInfo.AddLocalBundle(checkInfo.ReadWriteInfo.RelativePath); groupInfo.LocalCount++; groupInfo.LocalLength += checkInfo.ReadWriteInfo.Length; CatAssetDatabase.InitRuntimeInfo(checkInfo.ReadWriteInfo,true ); CatAssetUpdater.AddReadWriteManifestInfo(checkInfo.ReadWriteInfo); break ; case CheckState.InReadOnly: groupInfo = CatAssetDatabase.GetOrAddGroupInfo(checkInfo.ReadOnlyInfo.Group); groupInfo.AddLocalBundle(checkInfo.ReadOnlyInfo.RelativePath); groupInfo.LocalCount++; groupInfo.LocalLength += checkInfo.ReadOnlyInfo.Length; CatAssetDatabase.InitRuntimeInfo(checkInfo.ReadOnlyInfo,false ); break ; } if (checkInfo.NeedRemove) { Debug.Log($"删除读写区资源:{checkInfo.Name} " ); string path = Util.GetReadWritePath(checkInfo.Name); File.Delete(path); needGenerateReadWriteManifest = true ; } } if (needGenerateReadWriteManifest) { CatAssetUpdater.GenerateReadWriteManifest(); } VersionCheckResult result = new VersionCheckResult(string .Empty,totalCount, totalLength); onVersionChecked?.Invoke(result); Clear(); }
资源的更新 资源更新是以资源组为单位进行的,在启动资源组更新器(GroupUpdater) 后,会根据此资源组需要更新的资源创建资源包下载任务(DownloadBundleTask)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 internal void UpdateGroup (OnBundleUpdated callback ){ if (State != GroupUpdaterState.Free) { return ; } State = GroupUpdaterState.Running; foreach (BundleManifestInfo info in updateBundles) { string localFilePath = Util.GetReadWritePath(info.RelativePath); string downloadUri = Path.Combine(CatAssetUpdater.UpdateUriPrefix, info.RelativePath); CatAssetManager.DownloadBundle(this ,info,downloadUri,localFilePath,onBundleDownloaded); } onBundleUpdated = callback; }
1 2 3 4 5 6 7 8 9 internal static void DownloadBundle (GroupUpdater groupUpdater, BundleManifestInfo info,string downloadUri,string localFilePath,DownloadBundleCallback callback ){ DownloadBundleTask task = DownloadBundleTask.Create(downloadTaskRunner, downloadUri, info, groupUpdater, downloadUri, localFilePath, callback); downloadTaskRunner.AddTask(task,TaskPriority.Height); }
资源包下载任务 资源包下载任务(DownloadBundleTask) 通过UnityWebRequest
和DownloadHandlerFile
实现了低GC的文件下载,并且在启动时会通过检查本地已下载文件的字节长度进行断点续传操作
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 public override void Run ( ){ if (groupUpdater.State == GroupUpdaterState.Paused) { return ; } int startLength = 0 ; if (File.Exists(localTempFilePath)) { using (FileStream fs = File.OpenWrite(localTempFilePath)) { fs.Seek(0 , SeekOrigin.End); startLength = (int )fs.Length; } } UnityWebRequest uwr = new UnityWebRequest(downloadUri); if (startLength > 0 ) { uwr.SetRequestHeader("Range" , $"bytes={{{startLength} }}-" ); } uwr.downloadHandler = new DownloadHandlerFile(localTempFilePath, startLength > 0 ); op = uwr.SendWebRequest(); }
重试与校验 在下载失败后会尝试重新下载
如果下载成功了会先校验文件长度,若长度相同再校验MD5值,校验失败则会删除下载文件并尝试重新下载
校验都通过后会认为下载成功,并回调结果给使用者
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 public override void Update ( ){ if (op == null ) { State = TaskState.Free; return ; } if (!op.webRequest.isDone) { State = TaskState.Running; return ; } if (op.webRequest.result == UnityWebRequest.Result.ConnectionError || op.webRequest.result == UnityWebRequest.Result.ProtocolError) { if (RetryDownload()) { Debug.LogError($"下载失败准备重试:{Name} ,错误信息:{op.webRequest.error} ,当前重试次数:{retriedCount} " ); } else { Debug.LogError($"重试次数达到上限:{Name} ,错误信息:{op.webRequest.error} ,当前重试次数:{retriedCount} " ); State = TaskState.Finished; onFinished?.Invoke(false ,bundleManifestInfo); } return ; } FileInfo fi = new FileInfo(localTempFilePath); bool isVerify = fi.Length == bundleManifestInfo.Length; if (isVerify) { string md5 = Util.GetFileMD5(localTempFilePath); isVerify = md5 == bundleManifestInfo.MD5; } if (!isVerify) { File.Delete(localTempFilePath); if (RetryDownload()) { Debug.LogError($"校验失败准备重试:{Name} ,当前重试次数:{retriedCount} " ); } else { Debug.LogError($"重试次数达到上限:{Name} ,当前重试次数:{retriedCount} " ); State = TaskState.Finished; onFinished?.Invoke(false ,bundleManifestInfo); } return ; } State = TaskState.Finished; if (File.Exists(localFilePath)) { File.Delete(localFilePath); } File.Move(localTempFilePath, localFilePath); onFinished?.Invoke(true ,bundleManifestInfo); }
重新生成读写区资源清单 在GroupUpdater
的OnBundleDownloaded
回调被调用时,会刷新自身保存的已下载资源信息,并且在所有资源下载完毕或已下载字节数达到要求后,重新生成读写区资源清单
而如果都成功更新完了,就会将自身移除掉,否则将会等待下一次启动更新器
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 private void OnBundleDownloaded (bool success, BundleManifestInfo info ){ callbackCount++; if (callbackCount == TotalCount) { State = GroupUpdaterState.Free; } BundleUpdateResult result; if (!success) { Debug.LogError($"更新{info.RelativePath} 失败" ); result = new BundleUpdateResult(false ,info.RelativePath,this ); onBundleUpdated?.Invoke(result); return ; } updateBundles.Remove(info); UpdatedCount++; UpdatedLength += info.Length; deltaUpdatedLength += info.Length; CatAssetDatabase.InitRuntimeInfo(info, true ); CatAssetUpdater.AddReadWriteManifestInfo(info); GroupInfo groupInfo = CatAssetDatabase.GetOrAddGroupInfo(info.Group); groupInfo.AddLocalBundle(info.RelativePath); groupInfo.LocalCount++; groupInfo.LocalLength += info.Length; bool allDownloaded = UpdatedCount >= TotalCount; if (allDownloaded || deltaUpdatedLength >= generateManifestLength) { deltaUpdatedLength = 0 ; CatAssetUpdater.GenerateReadWriteManifest(); } if (allDownloaded) { CatAssetUpdater.RemoveGroupUpdater(GroupName); } result = new BundleUpdateResult(true ,info.RelativePath,this ); onBundleUpdated?.Invoke(result); }