本系列文章由抗痘洁面泥 出品,转载请注明出处。
Assetbundle管理与加载
最近在做项目优化的时候发现公司的项目用的还是老式的WWW去加载assetbundle资源的形式,而且是通过在两个Update里面分开加载AB和Asset的,这样虽然避免了协程的的使用,但是把一件事分开成了两件事,而且是需要每一帧都要在Update里面去检测,这样会加重Update里面的逻辑负担,所以我自己就重新用协程去写了一套资源加载。
1 对比WWW与LoadFromFile
首先WWW是一种以数据流的形式把AB加载到内存里面,他会在初始化的时候去构造网络连接对象,一种类似socket的东西,而且他会在每次初始化的时候去创建WWW对象,如果不做一个对象池去管理的话会造成对象过多而出发GC,严重的话会导致游戏卡顿,而且Unity官网上也推荐用LoadFromFIle的形式去加载AB,我去测了下,WWW与LoadFromFIle在速度上的差别,LoadFromFIle可以节省约1/3的时间,所以果断舍弃掉WWW。
2 使用协程还是使用Update去加载
使用Update去加载的话相当于是把加载Asset与AB分开来,这样我需要在不同的脚本去处理这两件事,会显得逻辑上很复杂,而且每一帧都要在Update检测,这样让逻辑很难被剥离出来。而用协程去实现的话,协程本身会存在一定的开销,而且协程开启过多也会造成GC,但是资源加载本身就不应该一次性加载过多的资源,所以协程的数量是可以控制的,那么协程的开销也可以忽略不计了,其次是协程把加载Asset与AB结合到了一起,变成了一件事,这样让逻辑更清晰,我只需要去关心这个协程是如何加载AB与Asset就行了,而且也很容易剥离开来。
3 AssetBundle的依赖以及引用计数
我们都知道在打包出来的AB中会有两个文件,一个是AB包,一个是manifest文件,而manifest文件中就记录了这个AB包所有的资源的名字等相关信息,以及AB包所引用的资源,这样的话我们就可以做一个工具用来检测所有AB包中是否存在相同的资源,我们就可以把这些重复的资源单独出来打成一个AB包,而运行时加载依赖的话,需要把 BuildPipeline.BuildAssetBundles设置的输出目录下的AB中的资源加载出来,转换为AssetBundleManifest。而AB的引用是会在加载AB的时候用到,主要是为了我们卸载资源,因为如果我们在加载完AB包之后没有做引用计数的话,如果A依赖了B,C也依赖了B,A与C同时加载到场景中,如果没有做引用计数,卸载A的时候会把B也卸载掉,那么C就会出现资源丢失的情况;此外还会出现内存泄露的情况,所以我们需要去维护一个引用计数,来保证正常的卸载。
4 设计思路
首先我们需要明白我们加载AB包需要写什么东西,需要一个AB名,一个Asset名,一个回调函数,一个引用计数,一个依赖数组,一个AB的加载状态,一个Asset加载状态,如下图所示
5 AssetBundleManager
Assetbundle不能一次性加载过多,否在会造成异步加载时被Lock,用profiler工具可以看到,所以我设置了一个队列每次从队列中取5个来加载,不同的项目可以根据自己的需要去设置最大加载数量,然后5个加载完了再去检查队列中还有Request没,如果有则等这帧完了再加载。在释放资源的时候提供立即释放与延迟释放的选择。然后剩下是加载场景,因为场景的加载接口是单独的,但是加载AB部分是一样的,所以就留给大家自己去完成了,最后附上代码。
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public class AssetBundleManager : MonoBehaviour { private string mMainManifestPath { get { return string.Format("{0}/{1}", Application.streamingAssetsPath, "AssetBundlDir/AssetBundlDir"); } } private static AssetBundleManager _instance; public static AssetBundleManager Instance { get { return _instance; } } private Dictionary<string, AssetReqBaseInfo> mAssetBundleInfoDic = new Dictionary<string, AssetReqBaseInfo>(); private Queue<AssetReq> mAssetRequestQueue = new Queue<AssetReq>(); private List<AssetReq> mLoadingAssetReq = new List<AssetReq>(); private List<bool> mLoadingAssetFlag = new List<bool>(mMaxLoadCount); private List<string> mDelayReleaseAssets = new List<string>(); private const int mMaxLoadCount = 5; private const int mMaxReleaseCount = 20; private const int mReleaseCountPerFrame = 5; private WaitForEndOfFrame mWaitFrameEnd = new WaitForEndOfFrame(); private float mTime = 0; private float mTimeInterval = 50; public delegate void AssetReqCallBack(UnityEngine.Object rOriginalRes, string rABName, string rResName); public class AssetReq { public string mABName; public string mResName; public AssetReqCallBack mCallBack; public bool mDelay; public AssetReq(string rABName, string rResName, AssetReqCallBack rCallBack, bool rDelay) { mABName = rABName; mResName = rResName; mCallBack = rCallBack; mDelay = rDelay; } } public class AssetReqBaseInfo { public string mABName; public AssetBundle mAssetBundle; public string[] mDependenceAB; public int mRefCount; public int mVersion; public ABLoadState mABState; public Dictionary<string, AssetInfo> mAsset; public AssetReqBaseInfo(string rABName, string[] rDepABName, int rVersion) { mABName = rABName; mDependenceAB = rDepABName; mRefCount = 0; mAssetBundle = null; mVersion = rVersion; mAsset = new Dictionary<string, AssetInfo>(); mABState = ABLoadState.None; } } public class AssetInfo { public UnityEngine.Object mAsset; public AssetLodState mState; public AssetInfo(UnityEngine.Object rAsset, AssetLodState rAssetState) { mAsset = rAsset; mState = rAssetState; } } public enum AssetLodState { LoadFailed, Loading, Loaded, } public enum ABLoadState { None, Loading, Loaded, Release, } private void Awake() { _instance = this; for (int i = 0; i < mMaxLoadCount; i++) { mLoadingAssetFlag.Add(false); } } private void Start() { StartCoroutine(LoadAssetBaseInfo(1)); } private void Update() { if (mTime > mTimeInterval) DelayRelease(); else mTime += Time.deltaTime; } /// <summary> /// release assetbundle which refcount is zero /// </summary> /// <param name="rABName"></param> /// <param name="rDelay"></param> /// <param name="rCallBack"></param> public void Release(string rABName, bool rDelay, AssetReqCallBack rCallBack) { AssetReqBaseInfo rBaseInfo = mAssetBundleInfoDic[rABName]; rBaseInfo.mRefCount--; if (rDelay && rBaseInfo.mRefCount == 0 && rBaseInfo.mABState == ABLoadState.Loaded && rBaseInfo.mAssetBundle != null) { if(!mDelayReleaseAssets.Contains(rABName)) mDelayReleaseAssets.Add(rABName); } else if (rBaseInfo.mABState == ABLoadState.Loaded && rBaseInfo.mAssetBundle != null && rBaseInfo.mRefCount == 0) UnloadAssetbundle(rABName); if (rCallBack != null) rCallBack(null, rABName, null); } private void DelayRelease() { if (mDelayReleaseAssets.Count > mMaxReleaseCount) { int rRemoveIndex = 0; for (int i = 0; i < mReleaseCountPerFrame; i++) { if (mDelayReleaseAssets.Count > 0) { string rReleaseABName = mDelayReleaseAssets[i]; UnloadAssetbundle(rReleaseABName); rRemoveIndex++; } } mDelayReleaseAssets.RemoveRange(0, rRemoveIndex); } } private void UnloadAssetbundle(string rABName) { AssetReqBaseInfo rBaseInfo = mAssetBundleInfoDic[rABName]; rBaseInfo.mAsset.Clear(); rBaseInfo.mAssetBundle.Unload(true); rBaseInfo.mAssetBundle = null; rBaseInfo.mABState = ABLoadState.None; } public void Load(string rABName, string rResName, AssetReqCallBack rCallBack) { AssetReq rReq = new global::AssetBundleManager.AssetReq(rABName, rResName, rCallBack, false); mAssetRequestQueue.Enqueue(rReq); AddToLoadList(); LoopLoadAsset(); } private void AddToLoadList() { if (mAssetRequestQueue.Count > 0 && !mLoadingAssetFlag.Contains(true)) { mLoadingAssetReq.Clear(); for (int count = 0; count < mMaxLoadCount; count++) { if (mAssetRequestQueue.Count <= 0) break; AssetReq rAssetRequest = mAssetRequestQueue.Dequeue(); mLoadingAssetReq.Add(rAssetRequest); } } } private void LoopLoadAsset() { for (int req_index = 0; req_index < mLoadingAssetReq.Count; req_index++) { StartCoroutine(LoadAssetBundleFromFile(mLoadingAssetReq[req_index], null, req_index)); } } private bool CheckABName(string rABName) { if (mAssetBundleInfoDic.ContainsKey(rABName)) return true; return false; } private IEnumerator LoadAssetBundleFromFile(AssetReq rAssetReq, string rDependABName, int rCurIndex) { if (rAssetReq == null) { if (!CheckABName(rDependABName)) { Debug.LogError("not find assetbundle of " + rDependABName); yield break; } AssetReqBaseInfo rAssetReqInfo = mAssetBundleInfoDic[rDependABName]; if (rAssetReqInfo.mABState == ABLoadState.Loaded && rAssetReqInfo.mAsset != null) yield break; while (rAssetReqInfo.mABState == ABLoadState.Loading) { //wait dependency assetbundle load finish; yield return null; } rAssetReqInfo.mABState = ABLoadState.Loading; AssetBundleCreateRequest rABCreateRequest = AssetBundle.LoadFromFileAsync(Application.streamingAssetsPath + "/AssetBundlDir/" + rAssetReqInfo.mABName); yield return rABCreateRequest; if (!rABCreateRequest.isDone) { Debug.LogError(rAssetReqInfo.mABName + " assetbundle load faid "); yield break; } rAssetReqInfo.mABState = ABLoadState.Loaded; rAssetReqInfo.mRefCount++; } else { //load assetbundle and asset if (!CheckABName(rAssetReq.mABName)) { Debug.LogError("not find assetbundle of " + rAssetReq.mABName); yield break; } mLoadingAssetFlag[rCurIndex] = true; AssetReqBaseInfo rAssetReqInfo = mAssetBundleInfoDic[rAssetReq.mABName]; while (rAssetReqInfo.mABState == ABLoadState.Loading) { //wait dependency assetbundle load finish; yield return null; } if (rAssetReqInfo.mABState == ABLoadState.None) { //load dependency assetbundle rAssetReqInfo.mABState = ABLoadState.Loading; mDelayReleaseAssets.Remove(rAssetReq.mABName); for (int dep_index = 0; dep_index < rAssetReqInfo.mDependenceAB.Length; dep_index++) { yield return StartCoroutine(LoadAssetBundleFromFile(null, rAssetReqInfo.mDependenceAB[dep_index], -1)); } AssetBundleCreateRequest rABCreateRequest = AssetBundle.LoadFromFileAsync(Application.streamingAssetsPath + "/AssetBundlDir/"+rAssetReqInfo.mABName); yield return rABCreateRequest; if (!rABCreateRequest.isDone) { rAssetReqInfo.mABState = ABLoadState.None; Debug.LogError(rAssetReqInfo.mABName + " assetbundle load faid "); yield break; } else { rAssetReqInfo.mABState = ABLoadState.Loaded; rAssetReqInfo.mAssetBundle = rABCreateRequest.assetBundle; } } if(rAssetReqInfo.mABState == ABLoadState.Loaded) { if (!rAssetReqInfo.mAsset.ContainsKey(rAssetReq.mResName)) { AssetInfo rAssetInfo = new AssetInfo(null, AssetLodState.Loading); rAssetReqInfo.mAsset.Add(rAssetReq.mResName, rAssetInfo); AssetBundleRequest rABResReq = rAssetReqInfo.mAssetBundle.LoadAssetAsync(rAssetReq.mResName); yield return rABResReq; if (rABResReq.isDone) rAssetInfo.mAsset = rABResReq.asset; else { Debug.LogError("fail load " + rAssetReq.mResName + " from " + rAssetReq.mABName); rAssetInfo.mState = AssetLodState.LoadFailed; yield break; } } else { while (rAssetReqInfo.mAsset[rAssetReq.mResName].mState == AssetLodState.Loading) { yield return null; } if (rAssetReqInfo.mAsset[rAssetReq.mResName].mState == AssetLodState.LoadFailed) yield break; } rAssetReq.mCallBack(rAssetReqInfo.mAsset[rAssetReq.mResName].mAsset, rAssetReqInfo.mABName, rAssetReq.mResName); } rAssetReqInfo.mRefCount++; mLoadingAssetFlag[rCurIndex] = false; yield return mWaitFrameEnd; //loop load assetrequest if (!mLoadingAssetFlag.Contains(true)) { if (mAssetRequestQueue.Count > 0) { AddToLoadList(); LoopLoadAsset(); } } } } /// <summary> /// Load MainManifest File /// </summary> /// <param name="rVersion"></param> /// <returns></returns> IEnumerator LoadAssetBaseInfo(int rVersion) { AssetBundleCreateRequest rRequest = AssetBundle.LoadFromFileAsync(mMainManifestPath); yield return rRequest; if (!rRequest.isDone) { Debug.LogError("Fail load Mainmanifest file at " + mMainManifestPath); yield break; } else { if (rRequest.assetBundle != null) { AssetBundleRequest rABReq = rRequest.assetBundle.LoadAllAssetsAsync(); yield return rABReq; if (rABReq.isDone) { AssetBundleManifest rManifest = rABReq.asset as AssetBundleManifest; string[] rAllAssetNames = rManifest.GetAllAssetBundles(); for (int asset_index = 0; asset_index < rAllAssetNames.Length; asset_index++) { string[] rDependencsName = rManifest.GetAllDependencies(rAllAssetNames[asset_index]); for (int i = 0; i < rDependencsName.Length; i++) { Debug.LogError(rDependencsName[i]); } AssetReqBaseInfo rBaseInfo = new AssetReqBaseInfo(rAllAssetNames[asset_index], rDependencsName, rVersion); mAssetBundleInfoDic.Add(rAllAssetNames[asset_index], rBaseInfo); } } } else { Debug.LogError("Fail load Mainmanifest's all assets at " + mMainManifestPath); yield break; } } } }