在游戏中会存在大量的ab包资源,如果这些都加载在内存中,不卸载那么很有可能会造成游戏崩溃。就像一个容器只往里添加而不往外拿,那么再大的容器也会被撑满。所以ab包什么时候卸载,卸载哪些包,就会是一个大问题。本文使用引用计数的方法解决ab包资源的管理问题。
首先需要明确需要管理哪些引用,就从UI面板的打开生命周期来看,一个UI会绑定一个预制体,加载预制体的ab包再实例化该游戏物体,那么这里会存在游戏物体与这个ab包之间的引用;其次在ab包加载的过程中,ab包又会与它依赖的包之间存在包与包的引用,引用关系如下:
对ab包来说,在更新自身引用计数的同时,也需要更新对依赖包的引用计数。最后当ab包的引用为0时,即可卸载这个ab包。对游戏物体来说,只需要保存对单个ab包的引用计数即可,不需要对包的依赖也进行计数。所以可以在代码中创建一个单独的类,用来保存ab包与游戏对象之间的引用计数。
一个ab包可以被多个游戏物体引用,一个游戏物体也可以引用多个游戏物体,是一种多对多的关系,游戏物体的生命周期才是ab包引用更新的关键。
public class AssetReferenceBind : MonoBehaviour
{
//单个包引用绑定结构
private class ReferenceBind
{
//已加载的ab包
public LoadedAssetBundle bundle;
//引用该包的游戏物体list
public List<GameObject> referenceGos = new List<GameObject>();
}
//ab包名与包引用绑定关系字典
private static Dictionary<string,ReferenceBind> distReferenceBind = new Dictionary<string, ReferenceBind> ();
}
在资源加载完毕后,在UI基类代码中,调用绑定函数,这样不用每个UI类都需要写绑定方法,也不容易出错。
//游戏物体与ab包之间引用绑定
public static AssetBundle Check(GameObject go, string assetBundleName, LoadedAssetBundle loadedAssetBundle)
{
ReferenceBind reference;
if(!distReferenceBind.TryGetValue(assetBundleName, out reference))
{
reference = new ReferenceBind();
distReferenceBind[assetBundleName] = reference;
}
if(loadedAssetBundle != null)
{
//先移除旧的绑定资源
if (reference.bundle != null)
UIManager.Instance.unLoadAssetBundle(assetBundleName);
reference.bundle = loadedAssetBundle;
}
if (reference.bundle == null)
return null;
if(!reference.referenceGos.Contains(go))
reference.referenceGos.Add(go);
return reference.bundle.assetBundle;
}
//在ui基类中调用绑定
AssetReferenceBind.Check(ui.gameObject, UIManager.Instance.getUIAssetBundleName(uiname), assetBundle);
至此已完成游戏物体与ab包之间的引用绑定操作,加上前面文章ab包的加载中已完成ab包之间的引用计数,绑定相关的引用计数已OK。接下来就是卸载,首先卸载游戏物体与ab包之间的引用。
public static void unLoadUnuseAssets()
{
foreach(var referenceBind in distReferenceBind)
{
for (var i = referenceBind.Value.referenceGos.Count - 1; i>=0; i--)
{
if (!referenceBind.Value.referenceGos[i])//游戏物体为null,从引用list中移除
referenceBind.Value.referenceGos.RemoveAt(i);
}
//没有游戏对象引用时,清除包的引用
if (referenceBind.Value.referenceGos.Count == 0 && referenceBind.Value.bundle != null)
{
UIManager.Instance.unLoadAssetBundle(referenceBind.Key);
}
}
}
卸载包与包之间的依赖引用。
/**
卸载ab包资源及其依赖包资源引用
*/
public void unLoadAssetBundle(string assetBundleName)
{
unLoadSingleBundle(assetBundleName);
string[] dependencies = null;
string relativePath = assetBundleName;
dependencies = mainBundleManifest.GetAllDependencies(relativePath);
foreach (string dependency in dependencies)
{
unLoadSingleBundle(dependency);
}
}
/**
卸载单个ab包资源引用计数
*/
public void unLoadSingleBundle(string assetBundleName)
{
LoadedAssetBundle assetBundle = getLoadedAssetBundle(assetBundleName);
if (assetBundle == null)
return;
if (assetBundle.referencedCount <= 0)
Debug.LogWarning("unLoadSingleBundle {0} referencedCount has already zero");
else
assetBundle.referencedCount--;
}
至此ab包的引用,随着游戏物体的销毁,引用计数更新。但此时还只是更新了引用,并未在内存中真实的卸载ab包资源。接下来就是真正卸载ab包资源的操作。
/**
清除引用计数为0的ab包资源
*/
public void unLoadUnuseAssets()
{
//先更新游戏物体与包之间的引用
AssetReferenceBind.unLoadUnuseAssets();
LoadedAssetBundle loadedAssetBundle;
string key;
for(var i= assetBundleNames.Count-1; i>=0; i--)
{
key = assetBundleNames[i];
loadedAssetBundle = loadedAssetBundles[key];
//再卸载计数为0的包资源
if (loadedAssetBundle.referencedCount <= 0)
{
loadedAssetBundles.Remove(key);
assetBundleNames.RemoveAt(i);
loadedAssetBundle.unLoad();
}
}
}
当然ab包的卸载也不需要实时操作,频繁的操作内存本身也是一直消耗,所以一般会预设一个内存峰值,当游戏内内存达到该值时,再去卸载一些无用的ab包,才是相对合理的操作。