最近在做项目优化的时候发现公司的项目用的还是老式的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;
}
}
}
}