Unity资源内存管理--webstream控制
一 使用前提
1,需要使用资源热更新
2,使用Assetbundle资源热更(AssetBundle是产生webstream的元凶)
二 为什么要用AssetBundle
AssetBundle本质上就是一个压缩算法,只不过比起zip等一些压缩多了一些信息,比如平台信息(Ios,android),依赖信息等,既然是压缩,那就很好理解了,AssetBundle就是为了减少包体的大小的。Assets/Resources目录其实也是一样的
三 怎么生成AssetBundle
using UnityEditor; using UnityEngine; using System.Collections.Generic; using System.Linq; public class PushAndPop { [MenuItem("Build/test")] static void Test() { string path1 ="Assets/Resources/UI1.prefab"; List<string> deps1 = GetDepends(path1); Execute(path1, deps1); string path2 = "Assets/Resources/UI2.prefab"; List<string> deps2 = GetDepends(path2); Execute(path2, deps2); } //获取path的依赖资源路径 static List<string> GetDepends(string path) { List<string> deps = AssetDatabase.GetDependencies(new string[] { path }).ToList<string>(); return deps; } //打包资源path成AssetBundle static void Execute(string path, List<string> deps) { string SavePath = Application.streamingAssetsPath + "/"; BuildAssetBundleOptions buildOp = BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets | BuildAssetBundleOptions.DeterministicAssetBundle; //先打包依赖 BuildPipeline.PushAssetDependencies(); string bundleName = ""; foreach (var dep in deps) { Debug.Log(dep); string ext = System.IO.Path.GetExtension(dep); if (ext == ".png" || ext==".shader" || ext==".FBX" || ext==".ttf") { Object sharedAsset = AssetDatabase.LoadMainAssetAtPath(dep); bundleName = sharedAsset.name.Replace('/', '_') + ext.Replace('.','_'); BuildPipeline.BuildAssetBundle(sharedAsset, null, SavePath + bundleName + ".assetbundle", buildOp, BuildTarget.StandaloneWindows); } } //再打包主资源 BuildPipeline.PushAssetDependencies(); Object mainAsset = AssetDatabase.LoadMainAssetAtPath(path); bundleName = mainAsset.name.Replace('/', '_'); BuildPipeline.BuildAssetBundle(mainAsset, null, SavePath + mainAsset.name + ".assetbundle", buildOp, BuildTarget.StandaloneWindows); BuildPipeline.PopAssetDependencies(); BuildPipeline.PopAssetDependencies(); AssetDatabase.Refresh(); } }
实例代码把Resources目录下的UI1和UI2打成了AssetBundle,并把他们放到了StreamingAssets目录,如下
其中myAtlas_png和Unlit_Transparent Colored_shader是UI1和UI2的依赖资源
四 加载AssetBundle并且实例化资源
加载要先依赖再主资源
using UnityEngine; using System.Collections; using System.Collections.Generic; public class AssetLoad : MonoBehaviour { void OnGUI() { //依赖加载按钮 if (GUI.Button(new Rect(0f, 30f, 100f, 20f), "Load Share Res")) { StartCoroutine(Load(@"file://" + Application.streamingAssetsPath + "/myAtlas_png.assetbundle")); StartCoroutine(Load(@"file://" + Application.streamingAssetsPath + "/Unlit_Transparent Colored_shader.assetbundle")); } if (GUI.Button(new Rect(0f, 60f, 100f, 20f), "Load UI1")) { StartCoroutine(LoadAndInstantiate(@"file://"+Application.streamingAssetsPath + "/UI1.assetbundle")); } if (GUI.Button(new Rect(0f, 90f, 100f, 20f), "Load UI2")) { StartCoroutine(LoadAndInstantiate(@"file://" + Application.streamingAssetsPath + "/UI2.assetbundle")); } } // 加载 IEnumerator Load(string url) { WWW www = new WWW(url); yield return www; AssetBundle ab = www.assetBundle; //Object obj = ab.mainAsset; ab.LoadAll(); Debug.Log("load" + url); } // 加载并实例化 IEnumerator LoadAndInstantiate(string url) { WWW www = new WWW(url); yield return www; if (!System.String.IsNullOrEmpty(www.error)) { Debug.Log(www.error); } else { Object main = www.assetBundle.mainAsset; GameObject.Instantiate(main); } } }
实例用www从StreamingAssets目录加载资源,加载完成后
五 AssetBundle的内存占用
我们先看看加进来的AssetBudle都使用了哪些内存,点击unity的菜单栏Window->Profiler,打开如下窗口
点击到Memory那一栏,在点击Simple旁边的下三角,选择Detailed
点击other,看到下面有一栏WebStream
这就是我们加进来的4个AssetBundle的占用的内存,后面带有各自占用内存
但是还没完,我们点开Assets这一栏
我们看到Texture2D下面有一张我们用到的图片,Shader下面也有
那么问题来了,为什么加进来的AssetBundle占有两处内存(WebStream和Asset),其实也很好理解,我们上面知道AssetBundle和压缩一样,这就好比我们在网站下载了一个zip的压缩文件,如果我们不解压是看不了里面的内容的,这就相当于WebStream下面的内存,Unity是识别不了的。这时候你用压缩软件把zip文件解压了,并且把文件解压在另外的目录,这个解压的操作和unity的ab.loadAll()操作是一样一样的,有木有,有木有,加载出来的Asset资源,Unity就能识别了。
六 AssetBundle内存释放
上面我们说过AssetBundle的内存占用分为WebStream和Asset,那么他们分别是怎么释放的,在什么时候释放,这是个问题
首先,我们释放WebStream的内存,调用ab.Unload(false)函数,可以释放AssetBundle的WebStream的内存
修改Load函数
// 加载 List<AssetBundle> ablist = new List<AssetBundle>(); IEnumerator Load(string url) { WWW www = new WWW(url); yield return www; AssetBundle ab = www.assetBundle; //Object obj = ab.mainAsset; ab.LoadAll(); //缓存ab ablist.Add(ab); Debug.Log("load" + url); } // 加载并实例化 IEnumerator LoadAndInstantiate(string url) { WWW www = new WWW(url); yield return www; if (!System.String.IsNullOrEmpty(www.error)) { Debug.Log(www.error); } else { Object main = www.assetBundle.mainAsset; GameObject.Instantiate(main); } //释放WebStream foreach(var ab in ablist) { if(ab != null) { ab.Unload(false); } } }
运行我们在看看
只剩下两个ui主资源的Webstream了,把他俩的ab加入释放列表,也是可以释放的。
unload(false)释放了Webstream的内存,我们再看看怎么释放Asset的内存,就是类似Texture2D下的图片内存等
释放Asset的内存使用Resources.UnloadAsset(obj) obj就是LoadAll加载出来的资源
我们再次修改我们的加载函数
// 加载 List<AssetBundle> ablist = new List<AssetBundle>(); List<Object> objlist = new List<Object>(); IEnumerator Load(string url) { WWW www = new WWW(url); yield return www; AssetBundle ab = www.assetBundle; //Object obj = ab.mainAsset; Object[] objs = ab.LoadAll(); //缓存ab ablist.Add(ab); objlist.AddRange(objs.ToList<Object>()); Debug.Log("load" + url); }
添加一个按钮作为释放的操作
if(GUI.Button(new Rect(0f, 120f, 100f, 20f), "Unload")) { foreach (var obj in objlist) { Resources.UnloadAsset(obj); } }
当点击Unload的时候释放了png和shader的Asset内存,我们发现UI也不可见了,所以这个释放操作是在所有引用到这个依赖的GameObject都Destroy的时候调用的,不然会出现资源丢失的情况
七 WebStream的释放时机
我们知道Webstream是用unlaod(false)释放的,但是我们发现,如果UI1和UI2都引用了png,当你实例化UI1后释放png的Webstream,在实例化UI2就不成功了,因为UI2依赖png,所以Webstream和Asset内存一样,也要在所有引用到这个依赖的GameObject都Destroy的时候才能释放。
最后修改的代码如下
using UnityEngine; using System.Collections; using System.Collections.Generic; using System.Linq; public class AssetLoad : MonoBehaviour { void OnGUI() { //依赖加载按钮 if (GUI.Button(new Rect(0f, 30f, 100f, 20f), "Load Share Res")) { StartCoroutine(Load(@"file://" + Application.streamingAssetsPath + "/myAtlas_png.assetbundle")); StartCoroutine(Load(@"file://" + Application.streamingAssetsPath + "/Unlit_Transparent Colored_shader.assetbundle")); } if (GUI.Button(new Rect(0f, 60f, 100f, 20f), "Load UI1")) { StartCoroutine(LoadAndInstantiate(@"file://"+Application.streamingAssetsPath + "/UI1.assetbundle")); } if (GUI.Button(new Rect(0f, 90f, 100f, 20f), "Load UI2")) { StartCoroutine(LoadAndInstantiate(@"file://" + Application.streamingAssetsPath + "/UI2.assetbundle")); } if (GUI.Button(new Rect(0f, 120f, 100f, 20f), "Detroy")) { GameObject go = GameObject.Find("UI1(Clone)"); GameObject.Destroy(go); go = GameObject.Find("UI2(Clone)"); GameObject.Destroy(go); } if (GUI.Button(new Rect(0f, 150f, 100f, 20f), "Unload")) { //释放WebStream foreach (var ab in ablist) { if (ab != null) { ab.Unload(false); } } //释放Asset foreach (var obj in objlist) { Resources.UnloadAsset(obj); } } } // 加载 List<AssetBundle> ablist = new List<AssetBundle>(); List<Object> objlist = new List<Object>(); IEnumerator Load(string url) { WWW www = new WWW(url); yield return www; AssetBundle ab = www.assetBundle; //Object obj = ab.mainAsset; Object[] objs = ab.LoadAll(); //缓存ab ablist.Add(ab); objlist.AddRange(objs.ToList<Object>()); Debug.Log("load" + url); } // 加载并实例化 IEnumerator LoadAndInstantiate(string url) { WWW www = new WWW(url); yield return www; if (!System.String.IsNullOrEmpty(www.error)) { Debug.Log(www.error); } else { AssetBundle ab = www.assetBundle; ablist.Add(ab); Object main = ab.mainAsset; GameObject.Instantiate(main); } } }
八 游戏项目中的内存解决方案
一般在游戏中,游戏资源很多,依赖关系很更复杂,有可能一个png有很多个GameObject引用,如果不及时的释放没有引用的资源,游戏很可能因为内存不足变得卡顿,甚至闪退,
一个很好的办法就是关联主资源和所有实例化的GameObject,并且给主资源引用的依赖计数,每当有一个主资源引用到依赖,依赖引用计数就+1,每当实例化一个GameObject,主资源的计数+1,当删除一个GamObject的时候,主资源计数-1,当主资源计数为0,主资源依赖计数分别-1,如果依赖的计数为0,释放这个依赖的Webstream和Asset内存。