http://blog.csdn.net/qq_19399235/article/details/51702964

1:Unity5 资源管理架构设计(2017.4.22版本)

2:Android 热更新(不考虑IOS)根据C#反射实现的代码全更新方案(网上一大坨,我重新整理一下)。



一:Unity资源管理架构设计

注意:我配置的Bundle资源文件都放在Assets/ResourceABs文件夹下,并且此文件夹下每个文件夹都对应一个Bundle文件,最终这些文件都打包到StreamingAssets流文件夹下。

1:设计一个资源信息管理类,能够反映Assets/ResourceABs文件夹下的全部的资源信息。
生成工具放在Editor文件下, 代码如下:
[csharp] view
plain
 copy

  1. using UnityEngine;
  2. using System.Collections;
  3. using System.IO;
  4. using UnityEditor;
  5. using xk_System.AssetPackage;
  6. public class ExportAssetInfoEditor : MonoBehaviour
  7. {
  8. static string extention = AssetBundlePath.ABExtention;
  9. static string BuildAssetPath = "Assets/ResourceABs";
  10. static string CsOutPath = "Assets/Scripts/auto";
  11. //~~~~~~~~~~~~~~~~~~~~~~~~~~~~创建AB文件所有的信息~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  12. [MenuItem("UnityEditor/GenerationPackage/Generation AssetInfo Cs File")]
  13. public static void GenericAssetCSInfo()
  14. {
  15. Debug.Log("Start Generation AssetInfo Cs Info");
  16. CreateABCSFile();
  17. Debug.Log("Finish Generation AssetInfo Cs Info");
  18. }
  19. private static void CreateABCSFile()
  20. {
  21. string m = "";
  22. m += "namespace xk_System.AssetPackage\n{\n";
  23. DirectoryInfo mDir = new DirectoryInfo(BuildAssetPath);
  24. m += "\tpublic class " + mDir.Name + "Folder : Singleton<" + mDir.Name + "Folder>\n\t{\n";
  25. string s = "";
  26. foreach (var v in mDir.GetDirectories())
  27. {
  28. FileInfo[] mFileInfos1 = v.GetFiles();
  29. int mFilesLength1 = 0;
  30. foreach (var v1 in mFileInfos1)
  31. {
  32. if (v1.Extension != ".meta")
  33. {
  34. mFilesLength1++;
  35. break;
  36. }
  37. }
  38. if (mFilesLength1 > 0 || v.GetDirectories().Length > 0)
  39. {
  40. string fieldName = v.Name + "Folder";
  41. m += "\t\t public " + fieldName + " " + v.Name + "=new " + fieldName + "();\n";
  42. // s += CreateDirClass(v, v.Name.ToLower());
  43. }
  44. }
  45. foreach (var v in mDir.GetDirectories())
  46. {
  47. m += CreateDirClass(v, v.Name.ToLower());
  48. }
  49. m += "\t}\n";
  50. // m += s;
  51. m += "}\n";
  52. string fileName = CsOutPath + "/" + mDir.Name + ".cs";
  53. StreamWriter mSw = new StreamWriter(fileName, false);
  54. mSw.Write(m);
  55. mSw.Close();
  56. }
  57. private static string CreateDirClass(DirectoryInfo mDir, string bundleName)
  58. {
  59. string tStr = GetTStr(mDir);
  60. string m = "";
  61. string s = "";
  62. FileInfo[] mFileInfos = mDir.GetFiles();
  63. int mFilesLength = 0;
  64. foreach (var v in mFileInfos)
  65. {
  66. if (v.Extension != ".meta")
  67. {
  68. mFilesLength++;
  69. break;
  70. }
  71. }
  72. if (mFilesLength > 0)
  73. {
  74. string bundleName1 = bundleName+ extention;
  75. m = tStr+"public class " + mDir.Name + "Folder\n"+tStr+"{\n";
  76. foreach (var v in mFileInfos)
  77. {
  78. if (v.Extension != ".meta")
  79. {
  80. string assetPath = GetAssetPath(v.FullName);
  81. string fileName = v.Name.Substring(0, v.Name.LastIndexOf(v.Extension));
  82. m += tStr+"\t public AssetInfo m" + fileName + "=new AssetInfo(\""+assetPath+"\",\"" + bundleName1 + "\",\"" + v.Name + "\");\n";
  83. }
  84. }
  85. m += tStr+"}\n";
  86. }
  87. else
  88. {
  89. if (mDir.GetDirectories().Length > 0)
  90. {
  91. m = tStr+"public class " + mDir.Name + "Folder\n"+tStr+"{\n";
  92. foreach (var v in mDir.GetDirectories())
  93. {
  94. FileInfo[] mFileInfos1 = v.GetFiles();
  95. int mFilesLength1 = 0;
  96. foreach (var v1 in mFileInfos1)
  97. {
  98. if (v1.Extension != ".meta")
  99. {
  100. mFilesLength1++;
  101. break;
  102. }
  103. }
  104. if (mFilesLength1 > 0 || v.GetDirectories().Length > 0)
  105. {
  106. string fieldName = v.Name + "Folder";
  107. m += tStr+"\t public " + fieldName + " " + v.Name + "=new " + fieldName + "();\n";
  108. }
  109. }
  110. foreach (var v in mDir.GetDirectories())
  111. {
  112. m += CreateDirClass(v, bundleName + "_" + v.Name.ToLower());
  113. }
  114. m += tStr+"}\n";
  115. // m += s;
  116. }
  117. }
  118. return m;
  119. }
  120. public static string GetTStr(DirectoryInfo mDir)
  121. {
  122. int coutT = 0;
  123. int index = mDir.FullName.IndexOf(@"ResourceABs\");
  124. if (index >= 0)
  125. {
  126. for(int j=0;j<mDir.FullName.Length;j++)
  127. {
  128. if (j > index)
  129. {
  130. var v = mDir.FullName[j];
  131. if (v.Equals('\\'))
  132. {
  133. coutT++;
  134. }
  135. }
  136. }
  137. }
  138. coutT++;
  139. string tStr = "";
  140. int i = 0;
  141. while(i<coutT)
  142. {
  143. tStr += "\t";
  144. i++;
  145. }
  146. return tStr;
  147. }
  148. public static string GetAssetPath(string filePath)
  149. {
  150. string assetPath = "";
  151. int index = filePath.IndexOf(@"Assets\");
  152. if (index >= 0)
  153. {
  154. assetPath = filePath.Remove(0, index);
  155. assetPath = assetPath.Replace(@"\","/");
  156. }
  157. return assetPath;
  158. }
  159. }
到这里,这个资源基本信息管理类就处理好了。

2:我们就正式开始写资源管理架构类了

我们首先写一个AssetBundleManager类,这个类的目的专门用来更新完毕后,充当资源加载管理器。代码如下
[csharp] view
plain
 copy

  1. using UnityEngine;
  2. using System.Collections;
  3. using xk_System.Debug;
  4. using System.Collections.Generic;
  5. using System.Xml;
  6. namespace xk_System.AssetPackage
  7. {
  8. /// <summary>
  9. /// 此类的目的就是加载本地的Bundle进行资源读取操作的
  10. /// </summary>
  11. public class AssetBundleManager : SingleTonMonoBehaviour<AssetBundleManager>
  12. {
  13. private ResourcesABManager mResourcesABManager = new ResourcesABManager();
  14. private Dictionary<string, AssetBundle> mBundleDic = new Dictionary<string, AssetBundle>();
  15. private Dictionary<string, Dictionary<string, UnityEngine.Object>> mAssetDic = new Dictionary<string, Dictionary<string, UnityEngine.Object>>();
  16. private List<string> mBundleLockList = new List<string>();
  17. /// <summary>
  18. /// 加载Assetbundle方案1:初始化时,全部加载
  19. /// </summary>
  20. /// <returns></returns>
  21. public IEnumerator InitLoadAllBundleFromLocal()
  22. {
  23. yield return mResourcesABManager.InitLoadMainifestFile();
  24. List<AssetBundleInfo> bundleList = mResourcesABManager.mNeedLoadBundleList;
  25. List<AssetBundleInfo>.Enumerator mIter = bundleList.GetEnumerator();
  26. while (mIter.MoveNext())
  27. {
  28. yield return AsyncLoadFromLoaclSingleBundle(mIter.Current);
  29. }
  30. }
  31. public IEnumerator InitAssetBundleManager()
  32. {
  33. yield return mResourcesABManager.InitLoadMainifestFile();
  34. }
  35. private IEnumerator CheckBundleDependentBundle(AssetBundleInfo mBundle)
  36. {
  37. if (mBundle != null)
  38. {
  39. string[] mdependentBundles = mBundle.mDependentBundleList;
  40. foreach (string s in mdependentBundles)
  41. {
  42. AssetBundleInfo mBundleInfo = mResourcesABManager.GetBundleInfo(s);
  43. if (mBundleInfo != null)
  44. {
  45. AssetBundle mAB = null;
  46. if (!mBundleDic.TryGetValue(mBundleInfo.bundleName, out mAB))
  47. {
  48. yield return AsyncLoadFromLoaclSingleBundle(mBundleInfo);
  49. }
  50. else
  51. {
  52. if (mAB == null)
  53. {
  54. yield return AsyncLoadFromLoaclSingleBundle(mBundleInfo);
  55. }
  56. }
  57. }
  58. }
  59. }
  60. }
  61. /// <summary>
  62. /// 从本地外部存储位置加载单个Bundle资源,全部加载
  63. /// </summary>
  64. /// <param name="BaseBundleInfo"></param>
  65. /// <returns></returns>
  66. private IEnumerator AsyncLoadFromLoaclSingleBundle1(AssetBundleInfo BaseBundleInfo)
  67. {
  68. if(mBundleLockList.Contains(BaseBundleInfo.bundleName))
  69. {
  70. while(mBundleLockList.Contains(BaseBundleInfo.bundleName))
  71. {
  72. yield return null;
  73. }
  74. yield break;
  75. }
  76. mBundleLockList.Add(BaseBundleInfo.bundleName);
  77. yield return CheckBundleDependentBundle(BaseBundleInfo);
  78. string path = AssetBundlePath.Instance.ExternalStorePathUrl;
  79. string url = path + "/" + BaseBundleInfo.bundleName;
  80. WWW www = new WWW(url);
  81. yield return www;
  82. if (www.isDone)
  83. {
  84. if (!string.IsNullOrEmpty(www.error))
  85. {
  86. DebugSystem.LogError("www Load Error:" + www.error);
  87. www.Dispose();
  88. mBundleLockList.Remove(BaseBundleInfo.bundleName);
  89. yield break;
  90. }
  91. }
  92. AssetBundle asset = www.assetBundle;
  93. SaveBundleToDic(BaseBundleInfo.bundleName, asset);
  94. mBundleLockList.Remove(BaseBundleInfo.bundleName);
  95. www.Dispose();
  96. }
  97. /// <summary>
  98. /// 从本地外部存储位置加载单个Bundle资源,全部加载
  99. /// </summary>
  100. /// <param name="BaseBundleInfo"></param>
  101. /// <returns></returns>
  102. private IEnumerator AsyncLoadFromLoaclSingleBundle(AssetBundleInfo BaseBundleInfo)
  103. {
  104. if (mBundleLockList.Contains(BaseBundleInfo.bundleName))
  105. {
  106. while (mBundleLockList.Contains(BaseBundleInfo.bundleName))
  107. {
  108. yield return null;
  109. }
  110. yield break;
  111. }
  112. mBundleLockList.Add(BaseBundleInfo.bundleName);
  113. yield return CheckBundleDependentBundle(BaseBundleInfo);
  114. string path = AssetBundlePath.Instance.ExternalStorePath+"/"+BaseBundleInfo.bundleName;
  115. AssetBundleCreateRequest www= AssetBundle.LoadFromFileAsync(path);
  116. www.allowSceneActivation = true;
  117. yield return www;
  118. AssetBundle asset = www.assetBundle;
  119. SaveBundleToDic(BaseBundleInfo.bundleName, asset);
  120. mBundleLockList.Remove(BaseBundleInfo.bundleName);
  121. }
  122. /// <summary>
  123. /// 异步从本地外部存储加载单个Asset文件,只加载Bundle中的单个资源
  124. /// </summary>
  125. /// <param name="bundle"></param>
  126. /// <returns></returns>
  127. private IEnumerator AsyncLoadFromLocalSingleAsset(AssetBundleInfo bundle, string assetName)
  128. {
  129. if (bundle != null)
  130. {
  131. yield return AsyncLoadFromLoaclSingleBundle(bundle);
  132. UnityEngine.Object Obj = mBundleDic[bundle.bundleName].LoadAsset(assetName);
  133. if (Obj != null)
  134. {
  135. DebugSystem.Log("Async Load Asset Success:" + Obj.name);
  136. SaveAssetToDic(bundle.bundleName, assetName, Obj);
  137. }
  138. }
  139. }
  140. /// <summary>
  141. /// 同步从本地外部存储加载单个Bundle文件
  142. /// </summary>
  143. /// <param name="BaseBundleInfo"></param>
  144. /// <param name="assetName"></param>
  145. /// <returns></returns>
  146. private void SyncLoadFromLocalSingleBundle(string bundleName)
  147. {
  148. if (!JudegeOrExistBundle(bundleName))
  149. {
  150. string path = AssetBundlePath.Instance.ExternalStorePath + "/" + bundleName;
  151. AssetBundle asset = AssetBundle.LoadFromFile(path);
  152. SaveBundleToDic(bundleName, asset);
  153. }else
  154. {
  155. DebugSystem.LogError("Bundle 已存在:"+bundleName);
  156. }
  157. }
  158. /// <summary>
  159. /// 同步从本地外部存储加载单个资源文件
  160. /// </summary>
  161. /// <param name="BaseBundleInfo"></param>
  162. /// <param name="assetName"></param>
  163. /// <returns></returns>
  164. public UnityEngine.Object SyncLoadFromLocalSingleAsset(AssetInfo mAssetInfo)
  165. {
  166. if (!JudgeOrExistAsset(mAssetInfo.bundleName, mAssetInfo.assetName))
  167. {
  168. string path = AssetBundlePath.Instance.ExternalStorePath+"/"+mAssetInfo.bundleName;
  169. AssetBundle asset = AssetBundle.LoadFromFile(path);
  170. SaveBundleToDic(mAssetInfo.bundleName,asset);
  171. }
  172. return GetAssetFromDic(mAssetInfo.bundleName,mAssetInfo.assetName);
  173. }
  174. private void SaveBundleToDic(string bundleName, AssetBundle bundle)
  175. {
  176. if (bundle == null)
  177. {
  178. DebugSystem.LogError("未保存的Bundle为空:"+bundleName);
  179. return;
  180. }
  181. if (!mBundleDic.ContainsKey(bundleName))
  182. {
  183. mBundleDic[bundleName] = bundle;
  184. }else
  185. {
  186. DebugSystem.LogError("Bundle资源 重复:"+bundleName);
  187. }
  188. }
  189. private void SaveAssetToDic(string bundleName, string assetName, UnityEngine.Object asset)
  190. {
  191. if (asset == null)
  192. {
  193. DebugSystem.LogError("未保存的资源为空:"+assetName);
  194. return;
  195. }
  196. if(asset is GameObject)
  197. {
  198. GameObject obj = asset as GameObject;
  199. obj.SetActive(false);
  200. }
  201. if (!mAssetDic.ContainsKey(bundleName))
  202. {
  203. Dictionary<string, UnityEngine.Object> mDic = new Dictionary<string, UnityEngine.Object>();
  204. mAssetDic.Add(bundleName, mDic);
  205. }
  206. mAssetDic[bundleName][assetName] = asset;
  207. }
  208. private bool JudgeOrBundelIsLoading(string bundleName)
  209. {
  210. if (mBundleLockList.Contains(bundleName))
  211. {
  212. return true;
  213. }else
  214. {
  215. return false;
  216. }
  217. }
  218. private bool JudegeOrExistBundle(string bundleName)
  219. {
  220. if (mBundleDic.ContainsKey(bundleName) && mBundleDic[bundleName] != null)
  221. {
  222. return true;
  223. }
  224. else
  225. {
  226. return false;
  227. }
  228. }
  229. private bool JudgeOrExistAsset(string bundleName, string asstName)
  230. {
  231. if (JudegeOrExistBundle(bundleName))
  232. {
  233. if (!mAssetDic.ContainsKey(bundleName) || mAssetDic[bundleName] == null || !mAssetDic[bundleName].ContainsKey(asstName) || mAssetDic[bundleName][asstName] == null)
  234. {
  235. UnityEngine.Object mm = mBundleDic[bundleName].LoadAsset(asstName);
  236. if (mm != null)
  237. {
  238. SaveAssetToDic(bundleName, asstName, mm);
  239. return true;
  240. }
  241. else
  242. {
  243. return false;
  244. }
  245. }
  246. else
  247. {
  248. return true;
  249. }
  250. }
  251. else
  252. {
  253. return false;
  254. }
  255. }
  256. private UnityEngine.Object GetAssetFromDic(string bundleName, string asstName)
  257. {
  258. if (JudgeOrExistAsset(bundleName, asstName))
  259. {
  260. UnityEngine.Object mAsset1 = mAssetDic[bundleName][asstName];
  261. if (mAsset1 is GameObject)
  262. {
  263. GameObject obj = Instantiate(mAsset1) as GameObject;
  264. return obj;
  265. }
  266. else
  267. {
  268. return mAsset1;
  269. }
  270. }
  271. else
  272. {
  273. DebugSystem.LogError("Asset is NUll:" + asstName);
  274. }
  275. return null;
  276. }
  277. #if UNITY_EDITOR
  278. private Dictionary<string, UnityEngine.Object> mEditorAssetDic = new Dictionary<string, UnityEngine.Object>();
  279. private UnityEngine.Object GetAssetFromEditorDic(string assetPath)
  280. {
  281. if (string.IsNullOrEmpty(assetPath))
  282. {
  283. DebugSystem.LogError("Editor AssetPath is Empty");
  284. return null;
  285. }
  286. UnityEngine.Object asset = null;
  287. if (!mEditorAssetDic.TryGetValue(assetPath, out asset))
  288. {
  289. asset = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(assetPath);
  290. if (asset != null)
  291. {
  292. if (asset is GameObject)
  293. {
  294. GameObject obj = asset as GameObject;
  295. obj.SetActive(false);
  296. }
  297. mEditorAssetDic.Add(assetPath, asset);
  298. }
  299. else
  300. {
  301. DebugSystem.LogError("找不到资源:" + assetPath);
  302. }
  303. }
  304. if (asset is GameObject)
  305. {
  306. GameObject obj = Instantiate(asset) as GameObject;
  307. return obj;
  308. }
  309. else
  310. {
  311. return asset;
  312. }
  313. }
  314. #endif
  315. public IEnumerator AsyncLoadBundle(string bundleName)
  316. {
  317. if (!JudegeOrExistBundle(bundleName))
  318. {
  319. string path = AssetBundlePath.Instance.ExternalStorePath + "/" + bundleName;
  320. AssetBundle asset = AssetBundle.LoadFromFile(path);
  321. SaveBundleToDic(bundleName, asset);
  322. yield return null;
  323. }
  324. }
  325. /// <summary>
  326. /// 这个东西用来在顶层使用
  327. /// </summary>
  328. /// <param name="type"></param>
  329. /// <param name="assetName"></param>
  330. /// <returns></returns>
  331. public UnityEngine.Object LoadAsset(AssetInfo mAssetInfo)
  332. {
  333. if (GameConfig.Instance.orUseAssetBundle)
  334. {
  335. return GetAssetFromDic(mAssetInfo.bundleName, mAssetInfo.assetName);
  336. }
  337. else
  338. {
  339. return GetAssetFromEditorDic(mAssetInfo.assetPath);
  340. }
  341. }
  342. /// <summary>
  343. /// 这个东西用来在专门的管理器中使用(底层封装一下),禁止在顶层使用
  344. /// </summary>
  345. /// <param name="type"></param>
  346. /// <param name="assetName"></param>
  347. /// <returns></returns>
  348. public IEnumerator AsyncLoadAsset(AssetInfo mAssetInfo)
  349. {
  350. if (GameConfig.Instance.orUseAssetBundle)
  351. {
  352. string bundleName = mAssetInfo.bundleName;
  353. string asstName = mAssetInfo.assetPath;
  354. if (!JudgeOrExistAsset(bundleName, asstName))
  355. {
  356. yield return AsyncLoadFromLocalSingleAsset(mResourcesABManager.GetBundleInfo(bundleName), asstName);
  357. }
  358. }
  359. }
  360. }
  361. public class ResourcesABManager
  362. {
  363. public int VersionId = -1;
  364. public List<AssetBundleInfo> mNeedLoadBundleList = new List<AssetBundleInfo>();
  365. public AssetBundleInfo GetBundleInfo(string bundleName)
  366. {
  367. AssetBundleInfo mBundleInfo = mNeedLoadBundleList.Find((x) =>
  368. {
  369. return x.bundleName == bundleName;
  370. });
  371. return mBundleInfo;
  372. }
  373. public IEnumerator InitLoadMainifestFile()
  374. {
  375. if (mNeedLoadBundleList.Count == 0)
  376. {
  377. string path = AssetBundlePath.Instance.ExternalStorePathUrl;
  378. string url = path + "/" + AssetBundlePath.AssetDependentFileBundleName;
  379. WWW www = new WWW(url);
  380. yield return www;
  381. if (www.isDone)
  382. {
  383. if (!string.IsNullOrEmpty(www.error))
  384. {
  385. DebugSystem.LogError("初始化 MainifestFile 失败:" + www.error);
  386. www.Dispose();
  387. yield break;
  388. }
  389. }
  390. AssetBundle asset = www.assetBundle;
  391. www.Dispose();
  392. if (asset == null)
  393. {
  394. DebugSystem.LogError("MainifestFile Bundle is Null");
  395. www.Dispose();
  396. yield break;
  397. }
  398. AssetBundleManifest mAllBundleMainifest = asset.LoadAsset<AssetBundleManifest>(AssetBundlePath.AssetDependentFileAssetName);
  399. if (mAllBundleMainifest == null)
  400. {
  401. DebugSystem.LogError("Mainifest is Null");
  402. www.Dispose();
  403. yield break;
  404. }
  405. string[] mAssetNames = mAllBundleMainifest.GetAllAssetBundles();
  406. if (mAssetNames != null)
  407. {
  408. foreach (var v in mAssetNames)
  409. {
  410. string bundleName = v;
  411. string[] bundleDependentList = mAllBundleMainifest.GetAllDependencies(v);
  412. Hash128 mHash = mAllBundleMainifest.GetAssetBundleHash(v);
  413. AssetBundleInfo mABInfo = new AssetBundleInfo(bundleName, mHash, bundleDependentList);
  414. mNeedLoadBundleList.Add(mABInfo);
  415. }
  416. }
  417. else
  418. {
  419. DebugSystem.Log("初始化资源依赖文件: Null");
  420. }
  421. asset.Unload(false);
  422. DebugSystem.Log("初始化资源管理器全局Bundle信息成功");
  423. www.Dispose();
  424. yield return InitLoadExternalStoreVersionConfig();
  425. }
  426. }
  427. private IEnumerator InitLoadExternalStoreVersionConfig()
  428. {
  429. string url = AssetBundlePath.Instance.ExternalStorePathUrl + "/" + AssetBundlePath.versionConfigBundleName;
  430. WWW www = new WWW(url);
  431. yield return www;
  432. if (www.isDone)
  433. {
  434. if (!string.IsNullOrEmpty(www.error))
  435. {
  436. DebugSystem.LogError("www Load Error:" + www.error);
  437. www.Dispose();
  438. yield break;
  439. }
  440. }
  441. AssetBundle mConfigBundle = www.assetBundle;
  442. TextAsset mVersionConfig = mConfigBundle.LoadAsset<TextAsset>(AssetBundlePath.versionConfigAssetName);
  443. VersionId = GetVersionIdByParseXML(mVersionConfig);
  444. DebugSystem.Log("当前版本号:"+VersionId);
  445. mConfigBundle.Unload(false);
  446. www.Dispose();
  447. }
  448. private int GetVersionIdByParseXML(TextAsset mTextAsset)
  449. {
  450. XmlDocument mdoc = new XmlDocument();
  451. mdoc.LoadXml(mTextAsset.text);
  452. foreach (XmlNode v in mdoc.ChildNodes)
  453. {
  454. if (v.Name == "root")
  455. {
  456. foreach (XmlNode x in v.ChildNodes)
  457. {
  458. if (x.Name.Contains("versionId"))
  459. {
  460. return int.Parse(x.InnerText);
  461. }
  462. }
  463. }
  464. }
  465. return 0;
  466. }
  467. }
  468. public class AssetBundleInfo
  469. {
  470. public string bundleName;
  471. public Hash128 mHash;
  472. public string[] mDependentBundleList;
  473. public AssetBundleInfo(string bundleName, Hash128 mHash128, string[] mDependentBundleList)
  474. {
  475. this.bundleName = bundleName;
  476. this.mHash = mHash128;
  477. this.mDependentBundleList = mDependentBundleList;
  478. }
  479. }
  480. public class AssetInfo
  481. {
  482. public string bundleName;
  483. public string assetName;
  484. public string assetPath;
  485. public AssetInfo(string assetPath,string bundleName, string assetName)
  486. {
  487. this.assetPath = assetPath;
  488. this.bundleName = bundleName;
  489. this.assetName = assetName;
  490. }
  491. public AssetInfo(string bundleName, string assetName)
  492. {
  493. this.bundleName = bundleName;
  494. this.assetName = assetName;
  495. }
  496. }
  497. public class AssetBundlePath : Singleton<AssetBundlePath>
  498. {
  499. public const string versionConfigBundleName = "version.xk_unity3d";
  500. public const string versionConfigAssetName = "version.xml";
  501. public const string AssetDependentFileBundleName = "StreamingAssets";
  502. public const string AssetDependentFileAssetName = "AssetBundleManifest";
  503. public const string ABExtention = ".xk_unity3d";
  504. public readonly string StreamingAssetPathUrl;
  505. public readonly string ExternalStorePathUrl;
  506. public readonly string WebServerPathUrl;
  507. public readonly string ExternalStorePath;
  508. public AssetBundlePath()
  509. {
  510. if (Application.platform == RuntimePlatform.WindowsEditor)
  511. {
  512. WebServerPathUrl = "file:///F:/WebServer";
  513. StreamingAssetPathUrl = "file:///" + Application.streamingAssetsPath;
  514. ExternalStorePathUrl = "file:///" + Application.persistentDataPath;
  515. ExternalStorePath = Application.persistentDataPath;
  516. } else if (Application.platform == RuntimePlatform.WindowsPlayer)
  517. {
  518. WebServerPathUrl = "file:///F:/WebServer";
  519. StreamingAssetPathUrl = "file:///" + Application.streamingAssetsPath;
  520. ExternalStorePathUrl = "file:///" + Application.persistentDataPath;
  521. ExternalStorePath = Application.persistentDataPath;
  522. }else if(Application.platform == RuntimePlatform.Android)
  523. {
  524. WebServerPathUrl = "file:///F:/WebServer";
  525. StreamingAssetPathUrl = "jar:file://" + Application.dataPath + "!/assets";
  526. ExternalStorePathUrl = "file://" + Application.persistentDataPath;
  527. ExternalStorePath = Application.persistentDataPath;
  528. }
  529. DebugSystem.LogError("www server path: " + WebServerPathUrl);
  530. DebugSystem.LogError("www local Stream Path: " + StreamingAssetPathUrl);
  531. DebugSystem.LogError("www local external Path: " + ExternalStorePathUrl);
  532. }
  533. }
  534. }

3:加载完本地的Bundle文件,那么现在我们开始下载Web服务器上的Bundle文件:

注意:本来我是自己定义一个md5配置文件专门用来比对资源,后来发现,Unity已经帮我们实现了这个功能。这个关键点就在于AssetBundleManifest类,具体请参考这篇文章末尾所讲的资源依赖配置文件:http://liweizhaolili.blog.163.com/blog/static/16230744201541410275298/

下载Web服务器资源代码如下:

[csharp] view
plain
 copy

  1. using UnityEngine;
  2. using System.Collections;
  3. using xk_System.Debug;
  4. using System.Collections.Generic;
  5. using xk_System.AssetPackage;
  6. using System.IO;
  7. using System.Xml;
  8. namespace xk_System.HotUpdate
  9. {
  10. public class AssetBundleHotUpdateManager : Singleton<AssetBundleHotUpdateManager>
  11. {
  12. private int mStreamFolderVersionId=-1;
  13. private int mExternalStoreVersionId=-1;
  14. private int mWebServerVersionId=-1;
  15. private List<AssetBundleInfo> mExternalStoreABInfoList = new List<AssetBundleInfo>();
  16. private List<AssetBundleInfo> mWebServerABInfoList = new List<AssetBundleInfo>();
  17. private List<AssetBundleInfo> mStreamFolderABInfoList = new List<AssetBundleInfo>();
  18. private DownLoadAssetInfo mDownLoadAssetInfo = new DownLoadAssetInfo();
  19. public LoadProgressInfo mTask = new LoadProgressInfo();
  20. public IEnumerator CheckUpdate()
  21. {
  22. mTask.progress = 0;
  23. mTask.Des = "正在检查资源";
  24. yield return CheckVersionConfig();
  25. if (mDownLoadAssetInfo.mAssetNameList.Count > 0)
  26. {
  27. mTask.progress += 10;
  28. mTask.Des = "正在下载资源";
  29. yield return DownLoadAllNeedUpdateBundle();
  30. }
  31. else
  32. {
  33. mTask.progress = 100;
  34. }
  35. }
  36. /// <summary>
  37. /// 检查版本配置文件
  38. /// </summary>
  39. /// <returns></returns>
  40. private IEnumerator CheckVersionConfig()
  41. {
  42. yield return InitLoadExternalStoreVersionConfig();
  43. yield return InitLoadStreamFolderVersionConfig();
  44. yield return InitLoadWebServerVersionConfig();
  45. DebugSystem.Log("本地版本号:" + mExternalStoreVersionId);
  46. DebugSystem.Log("WebServer版本号:" + mWebServerVersionId);
  47. DebugSystem.Log("StreamFolder版本号:" + mStreamFolderVersionId);
  48. if (mWebServerVersionId > mExternalStoreVersionId)
  49. {
  50. yield return InitLoadExternalStoreABConfig();
  51. if (mWebServerVersionId > mStreamFolderVersionId)
  52. {
  53. yield return InitLoadWebServerABConfig();
  54. CheckAssetInfo(AssetBundlePath.Instance.WebServerPathUrl, mWebServerABInfoList);
  55. }
  56. else
  57. {
  58. yield return InitLoadStreamFolderABConfig();
  59. CheckAssetInfo(AssetBundlePath.Instance.StreamingAssetPathUrl, mStreamFolderABInfoList);
  60. }
  61. }
  62. else if (mStreamFolderVersionId > mExternalStoreVersionId)
  63. {
  64. yield return InitLoadExternalStoreABConfig();
  65. yield return InitLoadStreamFolderABConfig();
  66. CheckAssetInfo(AssetBundlePath.Instance.StreamingAssetPathUrl, mStreamFolderABInfoList);
  67. }
  68. }
  69. /// <summary>
  70. /// 检查资源配置文件
  71. /// </summary>
  72. /// <returns></returns>
  73. private void CheckAssetInfo(string url, List<AssetBundleInfo> mUpdateABInfoList)
  74. {
  75. mDownLoadAssetInfo.url = url;
  76. foreach (AssetBundleInfo k in mUpdateABInfoList)
  77. {
  78. AssetBundleInfo mBundleInfo = mExternalStoreABInfoList.Find((x) =>
  79. {
  80. if (x.mHash.isValid && k.mHash.isValid)
  81. {
  82. return x.mHash.Equals(k.mHash);
  83. }
  84. else
  85. {
  86. DebugSystem.LogError("Hash is no Valid");
  87. return false;
  88. }
  89. });
  90. if (mBundleInfo == null)
  91. {
  92. mDownLoadAssetInfo.mAssetNameList.Add(k.bundleName);
  93. }
  94. }
  95. if (mDownLoadAssetInfo.mAssetNameList.Count > 0)
  96. {
  97. mDownLoadAssetInfo.mAssetNameList.Add(AssetBundlePath.AssetDependentFileBundleName);
  98. }
  99. DebugSystem.Log("需要下载更新的个数:" + mDownLoadAssetInfo.mAssetNameList.Count);
  100. }
  101. private IEnumerator InitLoadWebServerVersionConfig()
  102. {
  103. string url = AssetBundlePath.Instance.WebServerPathUrl + "/" + AssetBundlePath.versionConfigBundleName;
  104. WWW www = new WWW(url);
  105. yield return www;
  106. if (www.isDone)
  107. {
  108. if (!string.IsNullOrEmpty(www.error))
  109. {
  110. DebugSystem.LogError("www Load Error:" + www.error);
  111. www.Dispose();
  112. yield break;
  113. }
  114. }
  115. AssetBundle mConfigBundle = www.assetBundle;
  116. TextAsset mVersionConfig = mConfigBundle.LoadAsset<TextAsset>(AssetBundlePath.versionConfigAssetName);
  117. mWebServerVersionId = ParseXML(mVersionConfig);
  118. mConfigBundle.Unload(false);
  119. www.Dispose();
  120. }
  121. private IEnumerator InitLoadExternalStoreVersionConfig()
  122. {
  123. string url = AssetBundlePath.Instance.ExternalStorePathUrl + "/" + AssetBundlePath.versionConfigBundleName;
  124. WWW www = new WWW(url);
  125. yield return www;
  126. if (www.isDone)
  127. {
  128. if (!string.IsNullOrEmpty(www.error))
  129. {
  130. DebugSystem.LogError("www Load Error:" + www.error);
  131. www.Dispose();
  132. yield break;
  133. }
  134. }
  135. AssetBundle mConfigBundle = www.assetBundle;
  136. TextAsset mVersionConfig = mConfigBundle.LoadAsset<TextAsset>(AssetBundlePath.versionConfigAssetName);
  137. mExternalStoreVersionId = ParseXML(mVersionConfig);
  138. mConfigBundle.Unload(false);
  139. www.Dispose();
  140. }
  141. private IEnumerator InitLoadStreamFolderVersionConfig()
  142. {
  143. string url = AssetBundlePath.Instance.StreamingAssetPathUrl + "/" + AssetBundlePath.versionConfigBundleName;
  144. WWW www = new WWW(url);
  145. yield return www;
  146. if (www.isDone)
  147. {
  148. if (!string.IsNullOrEmpty(www.error))
  149. {
  150. DebugSystem.LogError("www Load Error:" + www.error);
  151. www.Dispose();
  152. yield break;
  153. }
  154. }
  155. AssetBundle mConfigBundle = www.assetBundle;
  156. TextAsset mVersionConfig = mConfigBundle.LoadAsset<TextAsset>(AssetBundlePath.versionConfigAssetName);
  157. mStreamFolderVersionId = ParseXML(mVersionConfig);
  158. mConfigBundle.Unload(false);
  159. www.Dispose();
  160. }
  161. private IEnumerator InitLoadWebServerABConfig()
  162. {
  163. string url = AssetBundlePath.Instance.WebServerPathUrl + "/" + AssetBundlePath.AssetDependentFileBundleName;
  164. WWW www = new WWW(url);
  165. yield return www;
  166. if (www.isDone)
  167. {
  168. if (!string.IsNullOrEmpty(www.error))
  169. {
  170. DebugSystem.LogError("www Load Error:" + www.error);
  171. www.Dispose();
  172. yield break;
  173. }
  174. }
  175. AssetBundle mConfigBundle = www.assetBundle;
  176. AssetBundleManifest mAllBundleMainifest = mConfigBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
  177. if (mAllBundleMainifest == null)
  178. {
  179. DebugSystem.LogError("Mainifest is Null");
  180. www.Dispose();
  181. yield break;
  182. }
  183. string[] mAssetNames = mAllBundleMainifest.GetAllAssetBundles();
  184. if (mAssetNames != null)
  185. {
  186. foreach (var v in mAssetNames)
  187. {
  188. string bundleName = v;
  189. string[] bundleDependentList = mAllBundleMainifest.GetAllDependencies(v);
  190. Hash128 mHash = mAllBundleMainifest.GetAssetBundleHash(v);
  191. AssetBundleInfo mABInfo = new AssetBundleInfo(bundleName, mHash, bundleDependentList);
  192. mWebServerABInfoList.Add(mABInfo);
  193. }
  194. }
  195. else
  196. {
  197. DebugSystem.Log("初始化资源依赖文件: Null");
  198. }
  199. mConfigBundle.Unload(false);
  200. www.Dispose();
  201. }
  202. private IEnumerator InitLoadExternalStoreABConfig()
  203. {
  204. string url = AssetBundlePath.Instance.ExternalStorePathUrl + "/" + AssetBundlePath.AssetDependentFileBundleName;
  205. WWW www = new WWW(url);
  206. yield return www;
  207. if (www.isDone)
  208. {
  209. if (!string.IsNullOrEmpty(www.error))
  210. {
  211. DebugSystem.LogError("www Load Error:" + www.error);
  212. www.Dispose();
  213. yield break;
  214. }
  215. }
  216. AssetBundle mConfigBundle = www.assetBundle;
  217. AssetBundleManifest mAllBundleMainifest = mConfigBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
  218. if (mAllBundleMainifest == null)
  219. {
  220. DebugSystem.LogError("Mainifest is Null");
  221. www.Dispose();
  222. yield break;
  223. }
  224. string[] mAssetNames = mAllBundleMainifest.GetAllAssetBundles();
  225. if (mAssetNames != null)
  226. {
  227. foreach (var v in mAssetNames)
  228. {
  229. string bundleName = v;
  230. string[] bundleDependentList = mAllBundleMainifest.GetAllDependencies(v);
  231. Hash128 mHash = mAllBundleMainifest.GetAssetBundleHash(v);
  232. AssetBundleInfo mABInfo = new AssetBundleInfo(bundleName, mHash, bundleDependentList);
  233. mExternalStoreABInfoList.Add(mABInfo);
  234. }
  235. }
  236. else
  237. {
  238. DebugSystem.Log("初始化资源依赖文件: Null");
  239. }
  240. mConfigBundle.Unload(false);
  241. www.Dispose();
  242. }
  243. private IEnumerator InitLoadStreamFolderABConfig()
  244. {
  245. string url = AssetBundlePath.Instance.StreamingAssetPathUrl + "/" + AssetBundlePath.AssetDependentFileBundleName;
  246. WWW www = new WWW(url);
  247. yield return www;
  248. if (www.isDone)
  249. {
  250. if (!string.IsNullOrEmpty(www.error))
  251. {
  252. DebugSystem.LogError("www Load Error:" + www.error);
  253. www.Dispose();
  254. yield break;
  255. }
  256. }
  257. AssetBundle mConfigBundle = www.assetBundle;
  258. AssetBundleManifest mAllBundleMainifest = mConfigBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
  259. if (mAllBundleMainifest == null)
  260. {
  261. DebugSystem.LogError("Mainifest is Null");
  262. www.Dispose();
  263. yield break;
  264. }
  265. string[] mAssetNames = mAllBundleMainifest.GetAllAssetBundles();
  266. if (mAssetNames != null)
  267. {
  268. foreach (var v in mAssetNames)
  269. {
  270. string bundleName = v;
  271. string[] bundleDependentList = mAllBundleMainifest.GetAllDependencies(v);
  272. Hash128 mHash = mAllBundleMainifest.GetAssetBundleHash(v);
  273. AssetBundleInfo mABInfo = new AssetBundleInfo(bundleName, mHash, bundleDependentList);
  274. mStreamFolderABInfoList.Add(mABInfo);
  275. }
  276. }
  277. else
  278. {
  279. DebugSystem.Log("初始化资源依赖文件: Null");
  280. }
  281. mConfigBundle.Unload(false);
  282. www.Dispose();
  283. }
  284. /// <summary>
  285. /// 得到版本号
  286. /// </summary>
  287. /// <param name="mbytes"></param>
  288. /// <returns></returns>
  289. public int ParseXML(TextAsset mTextAsset)
  290. {
  291. XmlDocument mdoc = new XmlDocument();
  292. mdoc.LoadXml(mTextAsset.text);
  293. foreach (XmlNode v in mdoc.ChildNodes)
  294. {
  295. if (v.Name == "root")
  296. {
  297. foreach (XmlNode x in v.ChildNodes)
  298. {
  299. if (x.Name.Contains("versionId"))
  300. {
  301. return int.Parse(x.InnerText);
  302. }
  303. }
  304. }
  305. }
  306. return 0;
  307. }
  308. private IEnumerator DownLoadAllNeedUpdateBundle()
  309. {
  310. List<string> bundleList = mDownLoadAssetInfo.mAssetNameList;
  311. List<string>.Enumerator mIter = bundleList.GetEnumerator();
  312. uint addPro = (uint)Mathf.CeilToInt((LoadProgressInfo.MaxProgress - mTask.progress)/(float)bundleList.Count);
  313. while (mIter.MoveNext())
  314. {
  315. DebugSystem.LogError("下载的文件:" + mDownLoadAssetInfo.url + " | " + mIter.Current);
  316. yield return DownLoadSingleBundle(mDownLoadAssetInfo.url, mIter.Current);
  317. mTask.progress+=addPro;
  318. }
  319. }
  320. private IEnumerator DownLoadSingleBundle(string path, string bundleName)
  321. {
  322. string url = path + "/" + bundleName;
  323. WWW www = new WWW(url);
  324. yield return www;
  325. if (www.isDone)
  326. {
  327. if (!string.IsNullOrEmpty(www.error))
  328. {
  329. DebugSystem.LogError("www Load Error:" + www.error);
  330. www.Dispose();
  331. yield break;
  332. }
  333. }
  334. string savePath = AssetBundlePath.Instance.ExternalStorePath + "/" + bundleName;
  335. SaveDownLoadedFile(savePath, www.bytes);
  336. www.Dispose();
  337. }
  338. private void SaveDownLoadedFile(string path, byte[] mdata)
  339. {
  340. if (File.Exists(path))
  341. {
  342. File.Delete(path);
  343. }
  344. FileInfo mFileInfo = new FileInfo(path);
  345. FileStream mFileStream = mFileInfo.OpenWrite();
  346. mFileStream.Write(mdata, 0, mdata.Length);
  347. mFileStream.Flush();
  348. mFileStream.Close();
  349. }
  350. private class DownLoadAssetInfo
  351. {
  352. public string url;
  353. public List<string> mAssetNameList = new List<string>();
  354. }
  355. }
  356. }

现在 资源管理架构设计就完了。



二: C#反射热更新(网上热更新的传说,多数资料都是简单一笔带过)



1:如何编译Unity代码,生成程序集:

Unity工程本身编译的程序集会放在Project\Library\ScriptAssemblies文件夹下,所以刚开始我是直接拿这个去加载程序集的,后来发现不行。

因为加载的程序集,与本地程序集重名,Unity会认为加载的程序集,还是本地程序集。(测过结果就是这样)

所以后来,通过VS2015编译Unity工程,但遇到一个问题:build的时候报了一大堆错误,错误的原因在于,Proto 生成的cs文件 所用的.net版本过高导致的。

你可以重新新build Protobuf 源码,然后生成.net低版本的程序集,这样做是可以的。

2:加载程序集,代码如下
[csharp] view
plain
 copy

  1. using UnityEngine;
  2. using System.Collections;
  3. using System.Reflection;
  4. using xk_System.Debug;
  5. using System;
  6. namespace xk_System.AssetPackage
  7. {
  8. public class AssemblyManager : Singleton<AssemblyManager>
  9. {
  10. private Assembly mHotUpdateAssembly;
  11. private Assembly mCurrentAssembly;
  12. /// <summary>
  13. /// 加载程序集
  14. /// </summary>
  15. /// <returns></returns>
  16. public IEnumerator LoadAssembly()
  17. {
  18. AssetInfo mAssetInfo = ResourceABsFolder.Instance.scripts.mtest;
  19. string path = AssetBundlePath.Instance.ExternalStorePathUrl;
  20. string bundleName1 = mAssetInfo.bundleName;
  21. string url = path + "/" + bundleName1;
  22. WWW www = new WWW(url);
  23. yield return www;
  24. if (www.isDone)
  25. {
  26. if (!string.IsNullOrEmpty(www.error))
  27. {
  28. DebugSystem.LogError("www Load Error:" + www.error);
  29. yield break;
  30. }
  31. }
  32. AssetBundle mConfigBundle = www.assetBundle;
  33. TextAsset mAsset = mConfigBundle.LoadAsset<TextAsset>(mAssetInfo.assetName);
  34. mHotUpdateAssembly = Assembly.Load(mAsset.bytes);
  35. if (mHotUpdateAssembly != null)
  36. {
  37. DebugSystem.Log("加载程序集:" + mHotUpdateAssembly.FullName);
  38. }
  39. else
  40. {
  41. DebugSystem.Log("加载程序集: null");
  42. }
  43. mCurrentAssembly = this.GetType().Assembly;
  44. DebugSystem.Log("当前程序集:" + mCurrentAssembly.FullName);
  45. if (mCurrentAssembly.FullName.Equals(mHotUpdateAssembly.FullName))
  46. {
  47. DebugSystem.LogError("加载程序集名字有误");
  48. }
  49. mConfigBundle.Unload(false);
  50. }
  51. public object CreateInstance(string typeFullName)
  52. {
  53. if (mHotUpdateAssembly != null)
  54. {
  55. return mHotUpdateAssembly.CreateInstance(typeFullName);
  56. }
  57. else
  58. {
  59. return mCurrentAssembly.CreateInstance(typeFullName);
  60. }
  61. }
  62. /// <summary>
  63. /// 仅仅写入口时,调用。(否则,会使程序变得混乱,反正我是搞乱了)
  64. /// </summary>
  65. /// <param name="obj"></param>
  66. /// <param name="typeFullName"></param>
  67. /// <returns></returns>
  68. public Component AddComponent(GameObject obj, string typeFullName)
  69. {
  70. DebugSystem.Log("Type: " + typeFullName);
  71. if (mHotUpdateAssembly != null)
  72. {
  73. Type mType = mHotUpdateAssembly.GetType(typeFullName);
  74. return obj.AddComponent(mType);
  75. }
  76. else
  77. {
  78. Type mType = typeFullName.GetType();
  79. return obj.AddComponent(mType);
  80. }
  81. }
  82. }
  83. }

3:加载完程序集后该如何使用这个程序集就是重点了。

刚开始想这个问题的时候感觉无非反射了这么简单的问题,后来越想感觉越复杂,幸好,崩溃完了之后,发现其实,你只要遵守2点即可实现游戏代码全部更新。(1):我们前面已经做完了,加载资源和加载程序集的工作,那么我们现在要做的工作,就是实现这个新加载的程序集的入口。切记,这个入口要通过动态添加组件的方式出现。如:  

              Type mType = mHotUpdateAssembly.GetType(typeFullName);obj.AddComponent(mType);

(2):要注意所有的预制件上要动态添加脚本,否则,预制件会去寻找【本地程序集】的脚本添加上去,并且还会导致【本地程序集】与【热更新程序集】相互访问的问题。

在这里要注意一点:因为我们已经提供了【热更新程序集】的入口,所以,接下来程序动态添加脚本就会使用【热更新程序集】里脚本。千万不要再去反射程序集里某某个脚本了,加载脚本,还和以前一样写就好了。如:


obj.AddComponent<T>(); 这里与入口动态添加的方式可不一样啊。(没有反射的)。

最新文章

  1. Keepalived使用梳理
  2. Adobe 软件防止联网激活更改Hosts文件
  3. [Java] arraycopy 数组复制(转)
  4. 遗传算法在JobShop中的应用研究(part3:交叉)
  5. PullToRefresh 下拉刷新的样式修改
  6. NFS文件共享系统
  7. 各种非标232,485协议,自定义协议转modbus协议模块定制开发,各种流量计协议转modbus,
  8. SuperSocket+unity 网络笔记
  9. C++ Primer 笔记 第一章
  10. Mybatis-简单基于源码了解获取动态代理对象
  11. JGUI源码:实现蒙版层显示(18)
  12. exportfs命令
  13. 为GHOST远控添加ROOTKIT功能
  14. DevExpress SpreadSheet报表模板设置 z
  15. 命令运行带参数的jar
  16. [Erlang34]erlang.mk的源码阅读1-入门makefile
  17. 网络电台-SHOUTcast
  18. linux下安装mysql-5.7.25
  19. 01 lucene基础 北风网项目培训 Lucene实践课程 索引
  20. UI Automator Viewer的使用

热门文章

  1. Eclipse中servlet显示无法导入javax.servlet包问题的解决方案
  2. JS dom最常用API
  3. 剑指Offer:栈的压入、弹出序列【31】
  4. Spring Boot 生成接口文档 swagger2
  5. ES6 Fetch API HTTP请求实用指南
  6. hihocoder 在线测试 补提交卡 (Google)
  7. poj1753 Flip Game —— 二进制压缩 + dfs / bfs or 递推
  8. JAVA- JDBC之DBHelper
  9. iOS审核策略重磅更新:Guideline 2.1批量拒审
  10. html5--1.11列表