赞
踩
unity资源的计数型AssetBundle加载管理
那内存管理有我们动态申请的程序运行或者计算使用的内存,还有一部分就是动态加载进来的资源的内存。
那这篇说的是关于资源内存管理的。
说的好像很高端,其实是个很简单的东西
在MVC的结构下,资源内存管理和AB加载是一个独立的模块
资源内存管理引用加载器,形成一个单独的模块我把它叫做Loader(加载器)
首先第一点,为什么要做资源内存管理
很明显,不用的资源可以被正确卸载。不至于我们的项目在运行的时候闪退崩溃。
目前最好用,最普遍的的一套内存管理的概念就是计数引用。其实和unity自身的内存管理或者C#,java的垃圾回收机制类似。
当一个object没有引用的时候就去释放它。
那Unity自带的 Resources.UnloadUnusedAssets 之类的方法,去检查资源是否还有引用的,没有引用就卸载。这种方法存在很大缺点,就是会遍历所有资源倒是顿卡。所以我们需要一套自己的资源管理方法。
相关的资料网上有很多,不需要重复解释了。
那做计数管理的好处就是可以知道什么该卸载资源了,而不是重复使用同一个AB之后不知道什么时候该卸载
那接下来直接说内存内存管理的一个原理
一个Unity游戏的资源加载流程一般分为两种 Sync 和 Async,也就是同步和异步的加载,
不管在现有的unity的哪个里 加载就是 AssetBundle.Load 或者 AssetBundle.LoadAsync只是写法不同。
那我使用的加载都是Async的,也就是异步,可有有效的避免 Sync的IO阻塞导致的卡顿。
Load的流程一般来说
界面请求资源->加载->得到AB->提取资源->使用资源
LoadAsync的流程是
界面请求资源->等待加载器回调
加载器加载资源->提取资源->回调
界面收到回调->使用资源
使用计数管理很重要的一点是需要外部的逻辑 加载卸载对齐
加载必须有卸载,创建必须有销毁。在确保外部逻辑合理的情况下,计数才有意义。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; /// <summary> /// 单个Asset加载的状态; /// 因为使用异步加载,加载是有个过程的. /// 在过程中可能需要外部获取的信息或者其他操作; /// </summary> public enum AssetState : byte { None, Loading, Finish, //... } /// <summary> /// 一个资源的信息; /// </summary> public class AssetPack { public AssetPack(string strName, string strLoadPath) { m_strLoadPath = strLoadPath; m_strName = strName; } //一些必须资料; private string m_strName = null; // 资源名; private string m_strLoadPath = null; // 加载路径; public System.Action<string> m_CallBack = null; // 回调; //运行时内容; public int m_nRef = 0; // 资源的引用计数; public AssetState m_AssetState = AssetState.None; // 加载状态; //加载结果内容 public AssetBundle m_AssetBundle = null; // 资源本体; public IEnumerator LoadAsync() { //TODO 资源的具体加载逻辑; m_AssetState = AssetState.Loading; // 开始加载 yield return null; // 等待加载完成 yield return null; // 加载完成 // 回调说明完成; m_AssetState = AssetState.Finish; if (m_CallBack != null) { m_CallBack(m_strName); } } } /// <summary> /// 给外部使用的加载器; /// Sync 和 Async 在这里是对于外部逻辑来说的同步或者异步. /// 不是IO方面的Async. /// 因为外部有时候需要利用Coroutine做一些流程化的加载步骤. 需要Sync /// 或者是界面的图片加载不在乎时序,加载即显示的内容.需要Async /// </summary> public class AssetLoader { private Dictionary<string, AssetPack> m_dicAllAssets = new Dictionary<string, AssetPack>(); public IEnumerator LoadSync(string strName, string strPath/*, 其他需要的参数*/) { /*因为是Sync的,所以在这套流程里应该是不需要回调的参数 * 因为Coroutine之后一定会有结果,加载成功 或者 资源不存在. * 回调本来就是用来通知加载完成的,所以这里不需要回调. */ // 直接调用下面的对外接口. // 因为这里是概念和逻辑上的Async和Sync区分,不是IO,所以组合逻辑; LoadAsync(strName, strPath, null/*, 其他需要的参数*/); while (!ChechLoadFinish(strName)) { yield return null; } } /// <summary> /// 加载逻辑 /// </summary> public void LoadAsync(string strName, string strPath, System.Action<string> cb/*, 其他需要的参数*/) { /*加载本身是一套线性流程,没什么问题,按照官方Demo就可以了. * 那我们要做的是避免重复加载资源. * 所以: */ if (m_dicAllAssets.ContainsKey(strName)) { // 如果这个资源已经申请下载过了,或者还没有被卸载,那么m_dicAllAssets一定有这个key存在. AssetPack ap = m_dicAllAssets[strName]; ap.m_nRef++; // 因为这个加载的请求,所以计数+1; ap.m_CallBack += cb; // 这里判断资源是否已经加载完成; if (ap.m_AssetState == AssetState.Finish) { //如果资源加载完成,直接回调; /*因为在申请加载这个资源的时候有几种情况 * 1.这个资源已经被加载过,并且在使用 * 2.这个资源不存在与内存,也就是说 没有加载过,或者已经被卸载 * 3.这个资源正在加载中. * * 所以在1的情况下,我们不需要重新加载这个资源,直接可以去到引用,外部直接获取就可以. */ cb(strName); } } else { //如果Dictionary没有这个key,那么: AssetPack ap = new AssetPack("", ""); ap.m_CallBack = cb; ap.m_nRef = 1; // 添加一个计数; /*启动Coroutine*/ap.LoadAsync(); // 这里我使用的是 管理空脚本的GameObject来启停Coroutine,这部分可以再说. /*在这个启动Coroutine加载完成后,由内部调用CallBack,在加载过程中,在上面的if里会增加callback,会一起调用。 * 这样就满足了 上面的if中说到的3种情况. */ m_dicAllAssets.Add(strName, ap); } } /// <summary> /// 对外卸载用的接口; /// </summary> public void Unload(string strName, System.Action<string> cb) { // 卸载的逻辑就不具体写了 /*判断是否存在 * 如果存在扣除计数, 减掉cb * 如果计数为0, Dictionary.Remove */ /*需要注意的是 在确定Remove的时候要停止Coroutine,因为可能UnLoad的时候这个资源正在加载.比如开了界面,马上关闭*/ } /// <summary> /// 检查加载是否完成; /// </summary> public bool ChechLoadFinish(string strName) { if (m_dicAllAssets.ContainsKey(strName)) { AssetPack ap = m_dicAllAssets[strName]; // 这里检查 return ap.m_AssetState == AssetState.Finish; } return false; } public AssetBundle GetAssetBundle(string strName) { if (m_dicAllAssets.ContainsKey(strName)) { // 获取内容return; return m_dicAllAssets[strName].m_AssetBundle; } return null; } } public class DemoAsync : MonoBehaviour { [SerializeField] Image image = null; private AssetLoader al = new AssetLoader(); // AssetLoader是作为单例存在,还是作为Logic的成员存在,其实是无所谓的.看怎么合理怎么来; void Start () { al.LoadAsync("img_000.ab","路径", OnCallBackLoadFinish); } private void OnCallBackLoadFinish(string strName) { //这个函数的参数返回strName可以判断加载是否完成.批量预加载某些资源的时候会有用; // 这里直接名字获取就可以; AssetBundle ab = al.GetAssetBundle("img_000.ab"); //之后可以获取资源进行操作了 // 要说明一下,这里只是简单的写了,ab应该由AssetPack确定资源类型后,在AssetPack加载完后,直接提取内容,外部直接使用具体资源; //image.sprite = ***; } void OnDestory() { al.Unload("img_000.ab", OnCallBackLoadFinish); } } public class DemoSync : MonoBehaviour { private AssetLoader al = new AssetLoader(); public void LoadBattleScene() { StartCoroutine("LoadBattleSceneAsync"); } private int m_nProgress = 0; // 切换场景加载进度; private IEnumerator LoadBattleSceneAsync() { m_nProgress = 0; yield return al.LoadSync("battlescene.ab", "路径"); //SceneManage.LoadScene m_nProgress = 30; yield return al.LoadSync("player.ab", "路径"); //PlayerLogic.Create m_nProgress = 60; yield return al.LoadSync("music.ab", "路径"); //music.init m_nProgress = 100; //等等类似这样的流程逻辑,可以用同步方法 } }
程序学无止尽。
欢迎大家沟通,有啥不明确的,或者不对的,也可以和我私聊
我的QQ 334524067 神一般的狄狄
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。