【原创】我所理解的资源加载方式
最近转战unity3d,接的第一个任务就是关于资源管理的部分。
由于项目是web和standalone(微端)并存的,所以希望保证业务逻辑尽量保持一致,跟之前ios,android的执行流程略有不同,比如在web模式下,FILE类是被禁用的,所以指望通过写文件来操作相关功能参数的方法是不可行的。下面,是我对这方面的一些理解。本文中如果没有特殊说明,下载的资源都是AssetBundle
web程序的运行流程大致是
首先加载web.html,这是整个程序的入口
他会加载相关的unity3d文件(本例中的web.unity3d)和unity3d平台所需要的文件(各种js文件)
基本资源加载完成后,进入界面。
其他使用时才下载的资源全部以AssetBundle的形式存放在远程目录,通过LoadFromCacheOrDownload函数传入版本号进行下载。
注意:unity为web版本的程序启用了50M的cache空间,所有下载的文件都存放在C:\Users\你的用户名\AppData\LocalLow\Unity\WebPlayer\Cache\Shared目录下
经过验证,web程序对于文件下载执行如下步骤
请求文件:http://xxxx/test/a.ab
1.代码中AB池里是否存在
2.unity cache里是否存在
3.浏览器缓存中是否存在
如果都不存在,则启动下载
下载完成后,根据浏览器的policy存入浏览器缓存,然后根据unity的policy存入unity的cache,最后代码中写入AB池以备后续使用。
standalone程序的运行流程跟web不太一样
1.exe文件包含打包进去的unity3d文件(scene文件)
2.目录,文件操作是启用的,比如Directory,File等
注意:
使用LoadFromCacheOrDownload方式下载的文件都是存放在C:\Users\你的用户名\AppData\LocalLow\Unity\WebPlayer\Cache目录下,web版会放在Shard和Temp目录下,而standalone版则会放在单独的子目录(一个默认工程的名字大致是DefaultCompany_xxx)
通过以上流程,可以看出,standalone和web版本的加载方式不同,但是我们可以通过合理的处理达到实现不同但是使用一致的结果。
先放一张整图,让大家有个整体的概念
整个流程描述大致是
启动游戏的时候,通过HTTP or WWW的方式下载version文件,里面保存【文件名, 修改时间, 依赖表】
当需要加载某个资源的时候,首先通过文件名获取文件的依赖关系表,然后生成下载文件列表
比如你想下载a.ab,但是a.ab文件依赖于c1.ab, c4.ab,那么最终生成的下载表就是[c1.ab, c4.ab, a.ab],制作之初我有个误区,以为有依赖关系的文件必须先下载依赖关系文件再下载目标文件,还特意写了个有向图的算法来计算关联关系及执行顺序,实际上经过测试,不需要。只要保证下载列表完全下载完成再使用ab就ok了。
然后判断一下在ABPool里(我们自己实现的AssetBundle池)是否已经存在这个ab,如果存在就直接返回ab的句柄
如果没有,则执行分平台下载流程
关于下载,上面已经解释过,standalone和web版的加载方式是不一样的,所以这里对不同平台做了专门的处理
standalone:
首先判断本地是否有这个文件,如果有,则判断本地这个文件的版本号是否是线上最新的版本号,如果不是,则通过HTTP方式下载文件,并保存到本地,然后通过WWW的方式将文件load到内存中,写入ABPool以备后续使用
web:
传入下载地址和版本号,unity自己会判断是否需要下载文件。
经过研究,LoadFromCacheOrDownload后面的version参数其实相当于一个key,并没有顺序的概念,当你通过5下载某个文件成功之后,底层会将这个文件的版本号改写成5,下次再调用的时候,就不会启动下载而是从cache里直接获取。如果你使用4作为版本号调用LoadFromCacheOrDownload的时候,底层会发现本地cache里并没有这个版本号的这个文件,然后重新下载。所有,只要版本号是变动的(唯一的,比如用文件修改时间作为版本号)
最终返回List表示已经加载结束
好了,大致流程就是这样,下面开始详细讲解每个功能
1.版本文件生成
我们这里的版本文件存放几个数据【文件名,verison,依赖表】
完整代码是
1 public class VersionList 2 { 3 protected class Data { //元数据,存放文件名,修改时间和依赖表 4 public Data(string _key, int _modifyTime, List<string> _depends) { 5 key = _key; 6 depends = _depends; 7 modifyTime = _modifyTime; 8 } 9 10 public string key { get; set; } 11 public int modifyTime { get; set; } 12 public List<string> depends { get; set; } 13 } 14 15 static string s_bundleDir = "Res"; //导出目录 16 static string s_versionFile = s_bundleDir + "/version.txt"; //资源文件 17 static Dictionary<string, Data> s_files = new Dictionary<string, Data>(); //文件信息字典 18 19 //计算时间戳并转换成小时数(注意:这里使用的是小时数作为版本号,开发期间可能一个小时会打包多次,但是对于线上这是不被允许的<如果出现这种情况,说明开发和测试的工作没做好>) 20 public static int dtToHours (DateTime dateTime) 21 { 22 var start = new DateTime(2016, 1, 1, 0, 0, 0, dateTime.Kind); 23 return Convert.ToInt32((dateTime - start). TotalHours); 24 } 25 26 //遍历目录,获取相关信息 27 static void GetObjectInfoToArray(string path) 28 { 29 string[] fs = Directory.GetFiles (path); 30 foreach (string f in fs) { 31 if(f.Contains(".meta")) //忽略meta文件 32 continue; 33 FileInfo fi = new FileInfo(f); 34 if (!fi.Exists) 35 { 36 continue; 37 } 38 39 string _f = f.Replace( "\\", "/" ); //统一目录分隔符 40 41 List<string> _depends = new List<string>(); 42 string []paths = AssetDatabase.GetDependencies(new string[]{_f}); //获取当前文件的所依赖的文件列表 43 foreach(string p in paths){ 44 string _p = p.Replace("\\", "/"); 45 if (_p != _f) { 46 _depends.Add(_p); 47 } 48 } 49 50 //将文件名,更新时间,依赖列表写入字典 51 s_files.Add(_f, new Data(_f, dtToHours (fi.LastWriteTime), _depends)); 52 } 53 54 try { 55 //遍历子目录 56 string[] ds = Directory.GetDirectories(path); 57 foreach(string d in ds) { 58 GetObjectInfoToArray (d); 59 } 60 } catch (System.IO.DirectoryNotFoundException) { 61 Debug.Log ("The path encapsulated in the " + path + "Directory object does not exist."); 62 } 63 } 64 65 [MenuItem( "VersionList/Generator" )] 66 static void Generator() 67 { 68 Debug.Log ("Begin Generator VersionList"); 69 s_files.Clear (); 70 71 //创建res目录 72 if (!Directory.Exists(s_bundleDir)) 73 { 74 Directory.CreateDirectory(s_bundleDir); 75 } 76 77 //删除老的version文件 78 if (File.Exists (s_versionFile)) { 79 File.Delete(s_versionFile); 80 } 81 82 //遍历生成Assets下所有文件的版本信息 83 GetObjectInfoToArray ("Assets"); 84 if (s_files.Keys.Count == 0) { 85 return; 86 } 87 88 //自定义格式写入文件 89 FileInfo vf = new FileInfo (s_versionFile); 90 StreamWriter sw = vf.CreateText (); 91 92 foreach (KeyValuePair<string, Data> kv in s_files) { 93 string tmp = kv.Key+";"+kv.Value.modifyTime; 94 if (kv.Value.depends != null && kv.Value.depends.Count > 0) { 95 tmp += ","; 96 for (int i=0; i<kv.Value.depends.Count-1; ++i) { 97 tmp += kv.Value.depends[i]; 98 tmp += ":"; 99 } 100 101 tmp += kv.Value.depends[kv.Value.depends.Count - 1]; 102 } 103 sw.WriteLine(tmp); 104 } 105 106 sw.Close(); 107 sw.Dispose(); 108 109 Debug.Log ("End Generator VersionList"); 110 } 111 }
加载version文件
1 IEnumerator LoadVersionFile() 2 { 3 Debug.Log("Begin Read VersionList"); 4 5 s_files.Clear(); 6 WWW www = new WWW("http://ldr123.mobi/webAU/test/version.ab"); 7 yield return www; 8 9 string result = www.assetBundle.mainAsset.ToString(); 10 string[] r = result.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries); 11 foreach (string line in r) 12 { 13 string[] res = line.Split(';'); 14 if (res.Length == 2) 15 { 16 string key = res[0]; 17 string value = res[1]; 18 string[] values = value.Split(','); 19 int modifyTime = int.Parse(values[0]); 20 List<string> depends = null; 21 if (values.Length == 2) 22 { 23 string d = values[1]; 24 string[] ds = d.Split(':'); 25 if (ds.Length > 0) 26 { 27 depends = new List<string>(); 28 foreach (string x in ds) 29 { 30 depends.Add(x); 31 } 32 } 33 } 34 35 s_files.Add(key, new Data(key, modifyTime, depends)); 36 } 37 } 38 39 Debug.Log("End Read VersionList"); 40 }
AB池的代码,以文件名为key存放所有已经加载的AssetBundle句柄
1 public class AssetBundlePool { 2 private class Data 3 { 4 public Data(AssetBundle _ab, int _version) 5 { 6 ab = _ab; 7 version = _version; 8 } 9 10 public AssetBundle ab { get; set; } 11 public int version { get; set; } 12 } 13 14 private Dictionary<string, Data> abContent = null; 15 16 public static AssetBundlePool instance = null; 17 public static AssetBundlePool Instance() 18 { 19 if (instance == null) 20 { 21 instance = new AssetBundlePool(); 22 } 23 24 return instance; 25 } 26 27 public AssetBundlePool() 28 { 29 abContent = new Dictionary<string, Data>(); 30 } 31 32 public AssetBundle getAssetBundle(string name, int version = -1) 33 { 34 if (abContent.ContainsKey(name)) 35 { 36 Data d = abContent[name]; 37 if (version == -1 || d.version == version) 38 { 39 return d.ab; 40 } 41 } 42 43 return null; 44 } 45 46 public void setAssetBundle(string name, AssetBundle ab, int version) 47 { 48 if (getAssetBundle(name, version) == null) 49 { 50 abContent.Add(name, new Data(ab, version)); 51 } 52 } 53 54 //todo:unload all 55 //todo:unload single 56 }
AssetLoader的代码
1 using UnityEngine; 2 using System.Collections; 3 using System.Collections.Generic; 4 5 #if UNITY_EDITOR || UNITY_STANDALONE 6 using System.IO; 7 #endif 8 9 public class AssetLoader 10 { 11 protected class Data 12 { 13 public Data(string _key, int _version, List<string> _depends) 14 { 15 key = _key; 16 depends = _depends; 17 version = _version; 18 } 19 20 public string key { get; set; } 21 public int version { get; set; } 22 public List<string> depends { get; set; } 23 } 24 25 private string m_strWWWAssetPath = "http://ldr123.mobi/webAU/test/"; 26 private string m_strStandaloneAssetPath = "Res/"; 27 private delegate void wwwCallback (string name); 28 private delegate void downCallback (); 29 30 private List<List<string>> m_downloadingRes = null; 31 private List<string> m_processing = null; 32 private MonoBehaviour m_mbHelper = null; 33 public bool m_bReady = false; 34 35 static Dictionary<string, Data> s_remoteFiles = new Dictionary<string, Data>(); 36 static Dictionary<string, Data> s_localFiles = new Dictionary<string, Data>(); 37 38 public AssetLoader (MonoBehaviour mb) 39 { 40 m_mbHelper = mb; 41 } 42 43 #if UNITY_WEBPLAYER 44 private IEnumerator webdownload(string filename, int version, wwwCallback callback) 45 { 46 while (!Caching.ready) 47 yield return null; 48 49 50 string assetPath = m_strWWWAssetPath + filename; 51 WWW www = WWW.LoadFromCacheOrDownload(assetPath, version); 52 yield return www; 53 54 if (!string.IsNullOrEmpty(www.error)) 55 { 56 yield return null; 57 } 58 else 59 { 60 AssetBundlePool.Instance().setAssetBundle(filename, www.assetBundle, version); 61 www.Dispose(); 62 www = null; 63 } 64 65 if (callback != null) 66 { 67 callback(filename); 68 } 69 } 70 #endif 71 72 #if UNITY_EDITOR || UNITY_STANDALONE 73 private IEnumerator standalonedownload(string filename, int version, wwwCallback callback) 74 { 75 string assetPath = m_strStandaloneAssetPath + filename; 76 bool needDownload = true; 77 FileInfo fi = new FileInfo(assetPath); 78 if (fi.Exists) 79 { 80 if (s_localFiles.ContainsKey(filename)) 81 { 82 if (s_localFiles[filename].version == version) 83 { 84 needDownload = false; 85 } 86 } 87 } 88 89 if (needDownload) 90 { 91 string assetWWWPath = m_strWWWAssetPath + filename; 92 WWW www = new WWW(assetWWWPath); 93 yield return www; 94 95 if (!string.IsNullOrEmpty(www.error)) 96 { 97 yield return null; 98 } 99 else 100 { 101 if (File.Exists(assetPath)) 102 { 103 File.Delete(assetPath); 104 } 105 106 using (FileStream fsWrite = new FileStream(assetPath, FileMode.Create, FileAccess.Write)) 107 { 108 using (BinaryWriter bw = new BinaryWriter(fsWrite)) 109 { 110 bw.Write(www.bytes); 111 } 112 } 113 114 if (s_localFiles.ContainsKey(filename)) 115 { 116 s_localFiles[filename].version = version; 117 } 118 else 119 { 120 Data d = s_remoteFiles[filename]; 121 s_localFiles.Add(filename, new Data(d.key, version, d.depends)); 122 } 123 124 AssetBundlePool.Instance().setAssetBundle(filename, www.assetBundle, version); 125 www.Dispose(); 126 www = null; 127 } 128 } 129 130 if (callback != null) 131 { 132 callback(filename); 133 } 134 } 135 #endif 136 137 private void download(string filename, wwwCallback callback) 138 { 139 if (!s_remoteFiles.ContainsKey(filename)) 140 { 141 return; 142 } 143 144 int version = s_remoteFiles[filename].version; 145 if (version == 0) 146 { 147 return; 148 } 149 150 if (AssetBundlePool.Instance().getAssetBundle(filename, version) != null) 151 { 152 if (callback != null) 153 { 154 callback(filename); 155 } 156 } 157 else 158 { 159 #if UNITY_WEBPLAYER 160 m_mbHelper.StartCoroutine(webdownload(filename, version, callback)); 161 #elif UNITY_EDITOR || UNITY_STANDALONE 162 m_mbHelper.StartCoroutine(standalonedownload(filename, version, callback)); 163 #endif 164 } 165 } 166 167 private void www_callback (string name) 168 { 169 m_processing.Remove (name); 170 171 //use ab 172 // AssetBundle ab = AssetBundlePool.Instance().getAssetBundle(name); 173 } 174 175 private IEnumerator startSubDownload (downCallback callback) 176 { 177 if (m_processing.Count > 0) { 178 foreach (string x in m_processing) { 179 download (x, www_callback); 180 } 181 } 182 183 while (m_processing.Count != 0) { 184 yield return null; 185 } 186 187 m_downloadingRes.RemoveAt (0); 188 189 if (callback != null) { 190 callback (); 191 } 192 } 193 194 private void _startDownload () 195 { 196 if (m_downloadingRes.Count == 0) { 197 return; 198 } 199 200 m_processing = m_downloadingRes[0]; 201 m_mbHelper.StartCoroutine (startSubDownload (delegate() { 202 _startDownload (); 203 })); 204 } 205 206 public IEnumerator startDownload (List<List<string>> lst) 207 { 208 m_bReady = false; 209 m_downloadingRes = lst; 210 if (m_downloadingRes.Count > 0) { 211 _startDownload (); 212 213 while (m_downloadingRes.Count != 0) { 214 yield return null; 215 } 216 } 217 218 m_bReady = true; 219 220 } 221 }
测试代码
1 using UnityEngine; 2 using UnityEngine.UI; 3 using System.Collections; 4 using System.Collections.Generic; 5 6 public class load : MonoBehaviour 7 { 8 public Button click; 9 10 private AssetLoader dh = null; 11 IEnumerator loadRes (List<List<string>> lst) 12 { 13 log("begin loadRes"); 14 StartCoroutine (dh.startDownload (lst)); 15 while (!dh.m_bReady) { 16 yield return null; 17 } 18 19 click.gameObject.SetActive(true); 20 log("end loadRes"); 21 } 22 23 void Start () 24 { 25 dh = new AssetLoader(this); 26 click.GetComponent<Button> ().onClick.AddListener (delegate() { 27 this.Click (); 28 }); 29 } 30 31 void Click () 32 { 33 click.gameObject.SetActive(false); 34 StartCoroutine(loadRes(new List<List<string>>() 35 { 36 new List<string> (){"a.ab", "b.ab", "c.ab"}, 37 new List<string> (){"00.ab"} 38 })); 39 } 40 }