AssetBundleSystem
Ps:稍微抽点时间写一下这个,这个小项目完成度80%左右,在年初2月份的时候就完成的差不多了,因为各种原因处于半搁浅的阶段,现在官方出了Addressable Assets System ,遂和大伙谈谈这个资源加载。
真的是懒到极致了。
起个头:
项目中代码只是占了体积的一小部分,资源是项目中体积的大头,同时也时刻影响着加载和内存。所以合理的使用和管理资源成为了一个项目中比较重要的部分,在此重要分为两个部分 Editor 和代码部分。
代码部分:
众所周知,Unity5之后除了Assetbundle,这个看似方便的东西,实际上如果不对它进行一个合理的管控,对于包体和内存都是一个不好的发展。它会通过相同的assetbundlename将所有引用的(未打成ab)资源的都打到一个ab包中,冗余就变得不可避免;但是如果做到0冗余,则又会增加IO的开销(小文件太多)。所以就得由代码或者工具进行一些合适的管控。(以assetbundle为例子)
加载部分:
初步设想需求就是这样:
需要梳理清楚这个资源是否有对其他资源的依赖,否则可能导致加载出来的对象关键数据丢失或者整个加载失败。良好的事件通知机制:当加载失败或者加载成功的能及时正确的通知到调用方。良好的异常/失败处理机制:当一个对象加载过程中其中某个步骤失败了,那么之前可能会有先一步加载的资源(比如依赖资源)可能会无法正确的释放,导致泄漏。良好的日志信息:方便开发者准确的把握流程和加载过程中的情况,方便维护相关内容。同时根据功能分多个system。
说到运行时的加载的资源会有2种:Resources的资源,通过Resources.Load<T>(xxxx)的方式进行同步加载。AssetBundle的资源,通过AssetBundle.LoadFromFile /LoadFromMemory等同步和Async系列的异步接口进行加载。Resouces下的资源Unity自动维护引用关系,直接Load足以。而对于细分依赖关系的Assetbundle资源,则需要自行维护资源加载。
1.通过AssetDatabase.GetAssetBundleDependencies 或EditorUtility.CollectDependencies来进行资源依赖的收集。 2.加载过程,在异步情况下如果有正在进行的同目标Task,则把委托事件同等的传递给Task方,先行加载依赖(如果内存中已经存在相关的资源则不需要加载),并记录资源引用。 3.当依赖加载完成时进行目标资源的加载,并记录引用,当真正的主体资源被成功加载的时候则对其依赖进行引用计数的维护(这样即便在加载流程中发生异常也能保证依赖引用计数的正确性,如果提前在依赖加载阶段进行计数,则可能会导致在异常发生的时候的泄漏问题出现)。 4.事件通知,完成或者失败通知到调用方,如果是Prefab因为在unity需要真正使用需要Instaniate所以在Instaniate层使用WeakReference或者挂接AliveTrakcer(随便取名的监听生命周期的脚本函数)来维护实例引用。如果是其他比如Font,Text,Texture资源,则在调用方进行SelfReference等非实例化对象的记录。
举例:摘出一段代码:
判断任务类型(任务尽量单一,不要涉及太多,不容易维护,所以这里基本就是一个task对一个资源),然后进行加载,先加载依赖,当依赖完成则进行目标资源的加载
if (task.IsLoad())
{
LoadDependency(ref context, ref task, ref result);
LoadSubTask(ref context, ref task, ref result);
int finishcnt;
int totalcnt;
if (task.IsSubAllDone(ref context, out finishcnt, out totalcnt) )//
{
GameAssetBundle gameAssetBundle;
if (context.Cache.GetAssetBundle(task.AssetBundleName, out gameAssetBundle))
{
if (task.Result.ProgresCallback != null)
{
ProgressArgs progressArgs = new ProgressArgs((float)finishcnt / (totalcnt + 1), 0, task.AssetPath, false);
task.Result.ProgresCallback(ref progressArgs);
}
Load(ref context, ref task, ref gameAssetBundle, ref result, true);
context.Cache.UpdateAssetBundle(task.AssetBundleName, ref gameAssetBundle);
}
else
{
Debug.LogErrorFormat("not found gameassetbundle :{0}", task.AssetBundleName);
AddException(ref context, task.AssetPath);
}
}
else
{
if (task.Result.ProgresCallback != null)
{
ProgressArgs progressArgs = new ProgressArgs((float)finishcnt / (totalcnt + 1), 0, task.AssetPath, false);
task.Result.ProgresCallback(ref progressArgs);
}
}
}
gameobject因为会涉及到实例化,所以需要维护实例化的引用, 像普通的asset资源或者场景资源,则通过依赖引用和自引用的方式进行引用维护,最后根据不同的策略进行管理。
内存管理:
当一些资源已经使用完毕或者完全看不见的时候,会需要即时的对资源进行一个卸载操作,或者要加载一些资源则可以直接使用缓存中的资源。常规一般就是通过已经加载对象的引用计数和卸载策略(比如卸载周期,对未来即将到来的加载调用做一个等待周期结束再实际卸载的操作)来判断资源是否需要卸载,卸载策略主要看你关注的重心是什么,如果你的重心是内存,则即时快速的卸载或者对内存阀值的控制是好的方案;如果你关心的重心是帧率,则异步的加载和较长时间的资源内存驻留是个合适的方案。一些老资源则可以直接使用缓存中的资源。对于assetbundle,通过AssetBundle.UnLoad(true/false)进行卸载,true需要把加载出来的Assets也进行一个统一的卸载,因为true会完整的卸全部资源,false则单纯的卸载Assetbundle资源,通过之前维护的引用计数进行合理的管理。这里默认采用unload true,因为对全部的引用都做了管理,清理的更干净,可以用这种,不放心,可以unload false。
策略制定方面:
基本就是采用上面说的:简单/内存/fps
举例:摘出一段代码:
public enum UnLoadStrategy
{
Default,
Memory,
FramesPerSecond,
}
public void Run(ref AssetBundleContext context, AssetEvent eEvent)
{
bool force = eEvent == AssetEvent.Destroy;
bool inLoading = context.Tasks == null ? false : context.Tasks.Count > 0;
if (!inLoading || force)
{
var result = Record(ref context, ref force, context.Cache.GetAllAssetBundle());
if (result != null && result.Count > 0)
{
UnLoad(result, ref context);
}
}
}
极懒模式之编辑器部分:
基本制作了几个简单的部分:
Asset:修改assetbundlename(拖拽 / ×(设置空名字)),罗列信息,罗列冗余打包的资源,正/反向查资源引用。
Profiler:简单罗列task和ab的情况
Build:显示了参数选项和含义,记得打依赖资源(dep)!
Sprite:极简(各种sort,set偷懒了,233),主要就是用于观察图集和引用。
很晚了,有空再补充。晚安!!下一章预计(有空可以交流一波,下一个模块基本形式已经完成,持续优化中):
资源地址:AssetBundleSystem(有空则维护一波,不过等官方那个出来后,可能就不用这个了。)