当前位置:   article > 正文

Cocos Creator 通用框架设计 —— 资源管理_cc.asset classname

cc.asset classname

转载自:https://www.cnblogs.com/ybgame/p/11711086.html

如果你想使用Cocos Creator制作一些规模稍大的游戏,那么资源管理是必须解决的问题,随着游戏的进行,你可能会发现游戏的内存占用只升不降,哪怕你当前只用到了极少的资源,并且有使用cc.loader.release来释放之前加载的资源,但之前使用过的大部分资源都会留在内存中!为什么会这样呢?

cocos creator 资源管理存在的问题

资源管理主要解决3个问题,资源加载,资源查找(使用),资源释放。这里要讨论的主要是资源释放的问题,这个问题看上去非常简单,在Cocos2d-x中确实也很简单,但在js中变得复杂了起来,因为难以跟踪一个资源是否可以被释放。

在Cocos2d-x中我们使用引用计数,在引用计数为0的时候释放资源,维护好引用计数即可,而且在Cocos2d-x中我们对资源的管理是比较分散的,引擎层面只提供如TextureCache、AudioManager之类的单例来管理某种特定的资源,大多数的资源都需要我们自己去管理,而在cocos creator中,我们的资源统一由cc.loader来管理,大量使用prefab,prefab与各种资源复杂的引用关系增加了资源管理的难度。

资源依赖

资源A可能依赖资源B、C、D,而资源D又依赖资源E,这是非常常见的一种资源依赖情况,如果我们使用cc.loader.loadRes("A")加载资源A,B~E都会被加载进来,但如果我们调用cc.loader.release("A")则只有资源A被释放。

image

 

每一个加载的资源都会放到cc.loader的_cache中,但cc.loader.release只是将传入的资源进行释放,而没有考虑资源依赖的情况。

如果对cc.loader背后的资源加载流程感兴趣可以参考: https://www.cnblogs.com/ybgame/p/10576884.html

如果我们希望将依赖的资源也一起释放,cocos creator提供了一个笨拙的方法,cc.loader.getDependsRecursively;,递归获取指定资源依赖的所有资源,放入一个数组并返回,然后在cc.loader.release中传入该数组,cc.loader会遍历它们,将其逐个释放。

这种方式虽然可以将资源释放,但却有可能释放了不应该释放的资源,如果有一个资源F依赖D,这时候就会导致F资源无法正常工作。由于cocos creator引擎没有维护好资源的依赖,导致我们在释放D的时候并不知道还有F依赖我们。即使没有F依赖,我们也不确定是否可以释放D,比如我们调用cc.loader加载D,而后又加载了A,此时D已经加载完成,A可以直接使用。但如果释放A的时候,将D也释放了,这就不符合我们的预期,我们期望的是在我们没有显式地释放D时,D不应该随着其它资源的释放而自动释放。

可以简单地进行测试,可以打开Chrome的开发者模式,在Console面板中进行输入,如果是旧版本的cocos creator可以在cc.textureCache中dump所有的纹理,而新版本移除了textureCache,但我们可以输入cc.loader._cache来查看所有的资源。如果资源太多,只关心数量,可以输入Object.keys(cc.loader._cache).length来查看资源总数,我们可以在资源加载前dump一次,加载后dump一次,释放后再dump一次,来对比cc.loader中的缓存状态。当然,也可以写一些便捷的方法,如只dump图片,或者dump与上次dump的差异项。

 

image

资源使用

除了资源依赖的问题,我们还需要解决资源使用的问题,前者是cc.loader内部的资源组织问题,后者是应用层逻辑的资源使用问题,比如我们需要在一个界面关闭的时候释放某资源,同样会面临一个该不该释放的问题,比如另外一个未关闭的界面是否使用了该资源?如果有其他地方用到了该资源,那么就不应该释放它!

ResLoader

在这里我设计了一个ResLoader,来解决cc.loader没有解决好的问题,关键是为每一个资源创建一个CacheInfo来记录资源的依赖和使用等信息,以此来判断资源是否可以释放,使用ResLoader.getInstance().loadRes()来替代cc.loader.loadRes(),ResLoader.getInstance().releaseRes()来替代cc.loader.releaseRes()。

对于依赖,在资源加载的时候ResLoader会自动建立起映射,释放资源的时候会自动取消映射,并检测取消映射后的资源是否可以释放,是才走释放的逻辑。

对于使用,提供了一个use参数,通过该参数来区别是哪里使用了该资源,以及是否有其他地方使用了该资源,当一个资源即没有倍其他资源依赖,也没有被其它逻辑使用,那么这个资源就可以被释放。

  1. /**
  2. * 资源加载类
  3. * 1. 加载完成后自动记录引用关系,根据DependKeys记录反向依赖
  4. * 2. 支持资源使用,如某打开的UI使用了A资源,其他地方释放资源B,资源B引用了资源A,如果没有其他引用资源A的资源,会触发资源A的释放,
  5. * 3. 能够安全释放依赖资源(一个资源同时被多个资源引用,只有当其他资源都释放时,该资源才会被释放)
  6. *
  7. * 2018-7-17 by 宝爷
  8. */
  9. // 资源加载的处理回调
  10. export type ProcessCallback = (completedCount: number, totalCount: number, item: any) => void;
  11. // 资源加载的完成回调
  12. export type CompletedCallback = (error: Error, resource: any) => void;
  13. // 引用和使用的结构体
  14. interface CacheInfo {
  15. refs: Set<string>,
  16. uses: Set<string>
  17. }
  18. // LoadRes方法的参数结构
  19. interface LoadResArgs {
  20. url: string,
  21. type?: typeof cc.Asset,
  22. onCompleted?: CompletedCallback,
  23. onProgess?: ProcessCallback,
  24. use?: string,
  25. }
  26. // ReleaseRes方法的参数结构
  27. interface ReleaseResArgs {
  28. url: string,
  29. type?: typeof cc.Asset,
  30. use?: string,
  31. }
  32. // 兼容性处理
  33. let isChildClassOf = cc.js["isChildClassOf"]
  34. if (!isChildClassOf) {
  35. isChildClassOf = cc["isChildClassOf"];
  36. }
  37. export default class ResLoader {
  38. private _resMap: Map<string, CacheInfo> = new Map<string, CacheInfo>();
  39. private static _resLoader: ResLoader = null;
  40. public static getInstance(): ResLoader {
  41. if (!this._resLoader) {
  42. this._resLoader = new ResLoader();
  43. }
  44. return this._resLoader;
  45. }
  46. public static destroy(): void {
  47. if (this._resLoader) {
  48. this._resLoader = null;
  49. }
  50. }
  51. private constructor() {
  52. }
  53. /**
  54. * 从cc.loader中获取一个资源的item
  55. * @param url 查询的url
  56. * @param type 查询的资源类型
  57. */
  58. private _getResItem(url: string, type: typeof cc.Asset): any {
  59. let ccloader: any = cc.loader;
  60. let item = ccloader._cache[url];
  61. if (!item) {
  62. let uuid = ccloader._getResUuid(url, type, false);
  63. if (uuid) {
  64. let ref = ccloader._getReferenceKey(uuid);
  65. item = ccloader._cache[ref];
  66. }
  67. }
  68. return item;
  69. }
  70. /**
  71. * loadRes方法的参数预处理
  72. */
  73. private _makeLoadResArgs(): LoadResArgs {
  74. if (arguments.length < 1 || typeof arguments[0] != "string") {
  75. console.error(`_makeLoadResArgs error ${arguments}`);
  76. return null;
  77. }
  78. let ret: LoadResArgs = { url: arguments[0] };
  79. for (let i = 1; i < arguments.length; ++i) {
  80. if (i == 1 && isChildClassOf(arguments[i], cc.RawAsset)) {
  81. // 判断是不是第一个参数type
  82. ret.type = arguments[i];
  83. } else if (i == arguments.length - 1 && typeof arguments[i] == "string") {
  84. // 判断是不是最后一个参数use
  85. ret.use = arguments[i];
  86. } else if (typeof arguments[i] == "function") {
  87. // 其他情况为函数
  88. if (arguments.length > i + 1 && typeof arguments[i + 1] == "function") {
  89. ret.onProgess = arguments[i];
  90. } else {
  91. ret.onCompleted = arguments[i];
  92. }
  93. }
  94. }
  95. return ret;
  96. }
  97. /**
  98. * releaseRes方法的参数预处理
  99. */
  100. private _makeReleaseResArgs(): ReleaseResArgs {
  101. if (arguments.length < 1 || typeof arguments[0] != "string") {
  102. console.error(`_makeReleaseResArgs error ${arguments}`);
  103. return null;
  104. }
  105. let ret: ReleaseResArgs = { url: arguments[0] };
  106. for (let i = 1; i < arguments.length; ++i) {
  107. if (typeof arguments[i] == "string") {
  108. ret.use = arguments[i];
  109. } else {
  110. ret.type = arguments[i];
  111. }
  112. }
  113. return ret;
  114. }
  115. /**
  116. * 生成一个资源使用Key
  117. * @param where 在哪里使用,如Scene、UI、Pool
  118. * @param who 使用者,如Login、UIHelp...
  119. * @param why 使用原因,自定义...
  120. */
  121. public static makeUseKey(where: string, who: string = "none", why: string = ""): string {
  122. return `use_${where}_by_${who}_for_${why}`;
  123. }
  124. /**
  125. * 获取资源缓存信息
  126. * @param key 要获取的资源url
  127. */
  128. public getCacheInfo(key: string): CacheInfo {
  129. if (!this._resMap.has(key)) {
  130. this._resMap.set(key, {
  131. refs: new Set<string>(),
  132. uses: new Set<string>()
  133. });
  134. }
  135. return this._resMap.get(key);
  136. }
  137. /**
  138. * 开始加载资源
  139. * @param url 资源url
  140. * @param type 资源类型,默认为null
  141. * @param onProgess 加载进度回调
  142. * @param onCompleted 加载完成回调
  143. * @param use 资源使用key,根据makeUseKey方法生成
  144. */
  145. public loadRes(url: string, use?: string);
  146. public loadRes(url: string, onCompleted: CompletedCallback, use?: string);
  147. public loadRes(url: string, onProgess: ProcessCallback, onCompleted: CompletedCallback, use?: string);
  148. public loadRes(url: string, type: typeof cc.Asset, use?: string);
  149. public loadRes(url: string, type: typeof cc.Asset, onCompleted: CompletedCallback, use?: string);
  150. public loadRes(url: string, type: typeof cc.Asset, onProgess: ProcessCallback, onCompleted: CompletedCallback, use?: string);
  151. public loadRes() {
  152. let resArgs: LoadResArgs = this._makeLoadResArgs.apply(this, arguments);
  153. console.time("loadRes|"+resArgs.url);
  154. let finishCallback = (error: Error, resource: any) => {
  155. // 反向关联引用(为所有引用到的资源打上本资源引用到的标记)
  156. let addDependKey = (item, refKey) => {
  157. if (item && item.dependKeys && Array.isArray(item.dependKeys)) {
  158. for (let depKey of item.dependKeys) {
  159. // 记录该资源被我引用
  160. this.getCacheInfo(depKey).refs.add(refKey);
  161. // cc.log(`${depKey} ref by ${refKey}`);
  162. let ccloader: any = cc.loader;
  163. let depItem = ccloader._cache[depKey]
  164. addDependKey(depItem, refKey)
  165. }
  166. }
  167. }
  168. let item = this._getResItem(resArgs.url, resArgs.type);
  169. if (item && item.url) {
  170. addDependKey(item, item.url);
  171. } else {
  172. cc.warn(`addDependKey item error1! for ${resArgs.url}`);
  173. }
  174. // 给自己加一个自身的引用
  175. if (item) {
  176. let info = this.getCacheInfo(item.url);
  177. info.refs.add(item.url);
  178. // 更新资源使用
  179. if (resArgs.use) {
  180. info.uses.add(resArgs.use);
  181. }
  182. }
  183. // 执行完成回调
  184. if (resArgs.onCompleted) {
  185. resArgs.onCompleted(error, resource);
  186. }
  187. console.timeEnd("loadRes|"+resArgs.url);
  188. };
  189. // 预判是否资源已加载
  190. let res = cc.loader.getRes(resArgs.url, resArgs.type);
  191. if (res) {
  192. finishCallback(null, res);
  193. } else {
  194. cc.loader.loadRes(resArgs.url, resArgs.type, resArgs.onProgess, finishCallback);
  195. }
  196. }
  197. /**
  198. * 释放资源
  199. * @param url 要释放的url
  200. * @param type 资源类型
  201. * @param use 要解除的资源使用key,根据makeUseKey方法生成
  202. */
  203. public releaseRes(url: string, use?: string);
  204. public releaseRes(url: string, type: typeof cc.Asset, use?: string)
  205. public releaseRes() {
  206. /**暂时不释放资源 */
  207. // return;
  208. let resArgs: ReleaseResArgs = this._makeReleaseResArgs.apply(this, arguments);
  209. let item = this._getResItem(resArgs.url, resArgs.type);
  210. if (!item) {
  211. console.warn(`releaseRes item is null ${resArgs.url} ${resArgs.type}`);
  212. return;
  213. }
  214. cc.log("resloader release item");
  215. // cc.log(arguments);
  216. let cacheInfo = this.getCacheInfo(item.url);
  217. if (resArgs.use) {
  218. cacheInfo.uses.delete(resArgs.use)
  219. }
  220. this._release(item, item.url);
  221. }
  222. // 释放一个资源
  223. private _release(item, itemUrl) {
  224. if (!item) {
  225. return;
  226. }
  227. let cacheInfo = this.getCacheInfo(item.url);
  228. // 解除自身对自己的引用
  229. cacheInfo.refs.delete(itemUrl);
  230. if (cacheInfo.uses.size == 0 && cacheInfo.refs.size == 0) {
  231. // 解除引用
  232. let delDependKey = (item, refKey) => {
  233. if (item && item.dependKeys && Array.isArray(item.dependKeys)) {
  234. for (let depKey of item.dependKeys) {
  235. let ccloader: any = cc.loader;
  236. let depItem = ccloader._cache[depKey]
  237. this._release(depItem, refKey);
  238. }
  239. }
  240. }
  241. delDependKey(item, itemUrl);
  242. //如果没有uuid,就直接释放url
  243. if (item.uuid) {
  244. cc.loader.release(item.uuid);
  245. cc.log("resloader release item by uuid :" + item.url);
  246. } else {
  247. cc.loader.release(item.url);
  248. cc.log("resloader release item by url:" + item.url);
  249. }
  250. }
  251. }
  252. /**
  253. * 判断一个资源能否被释放
  254. * @param url 资源url
  255. * @param type 资源类型
  256. * @param use 要解除的资源使用key,根据makeUseKey方法生成
  257. */
  258. public checkReleaseUse(url: string, use?: string): boolean;
  259. public checkReleaseUse(url: string, type: typeof cc.Asset, use?: string): boolean
  260. public checkReleaseUse() {
  261. let resArgs: ReleaseResArgs = this._makeReleaseResArgs.apply(this, arguments);
  262. let item = this._getResItem(resArgs.url, resArgs.type);
  263. if (!item) {
  264. console.log(`cant release,item is null ${resArgs.url} ${resArgs.type}`);
  265. return true;
  266. }
  267. let cacheInfo = this.getCacheInfo(item.url);
  268. let checkUse = false;
  269. let checkRef = false;
  270. if (resArgs.use && cacheInfo.uses.size > 0) {
  271. if (cacheInfo.uses.size == 1 && cacheInfo.uses.has(resArgs.use)) {
  272. checkUse = true;
  273. } else {
  274. checkUse = false;
  275. }
  276. } else {
  277. checkUse = true;
  278. }
  279. if ((cacheInfo.refs.size == 1 && cacheInfo.refs.has(item.url)) || cacheInfo.refs.size == 0) {
  280. checkRef = true;
  281. } else {
  282. checkRef = false;
  283. }
  284. return checkUse && checkRef;
  285. }
  286. }

使用ResLoader

ResLoader的使用非常简单,下面是一个简单的例子,我们可以点击dump按钮来查看当前的资源总数,点击cc.load、cc.release之后分别dump一次,可以发现,开始有36个资源,加载之后有40个资源,而执行释放之后,还有39个资源,只释放了一个资源。

如果使用ResLoader进行测试,发现释放之后只有34个资源,这是因为前面加载场景的资源也被该测试资源依赖,所以这些资源也被释放掉了,只要我们都使用ResLoader来加载和卸载资源,就不会出现资源泄露的问题。

image

 

示例代码:

  1. @ccclass
  2. export default class NetExample extends cc.Component {
  3. @property(cc.Node)
  4. attachNode: cc.Node = null;
  5. @property(cc.Label)
  6. dumpLabel: cc.Label = null;
  7. onLoadRes() {
  8. cc.loader.loadRes("Prefab/HelloWorld", cc.Prefab, (error: Error, prefab: cc.Prefab) => {
  9. if (!error) {
  10. cc.instantiate(prefab).parent = this.attachNode;
  11. }
  12. });
  13. }
  14. onUnloadRes() {
  15. this.attachNode.removeAllChildren(true);
  16. cc.loader.releaseRes("Prefab/HelloWorld");
  17. }
  18. onMyLoadRes() {
  19. ResLoader.getInstance().loadRes("Prefab/HelloWorld", cc.Prefab, (error: Error, prefab: cc.Prefab) => {
  20. if (!error) {
  21. cc.instantiate(prefab).parent = this.attachNode;
  22. }
  23. });
  24. }
  25. onMyUnloadRes() {
  26. this.attachNode.removeAllChildren(true);
  27. ResLoader.getInstance().releaseRes("Prefab/HelloWorld");
  28. }
  29. onDump() {
  30. let Loader:any = cc.loader;
  31. this.dumpLabel.string = `当前资源总数:${Object.keys(Loader._cache).length}`;
  32. }
  33. }

可以看到上面的例子是先移除节点,再进行释放,这是正确的使用方式,如果我没有移除直接释放呢??因为释放了纹理,所以cocos creator在接下来的渲染中会不断报错。

ResLoader只是一个基础,直接使用ResLoader我们不需要关心资源的依赖问题,但资源的使用问题我们还需要关心,在实际的使用中,我们可能希望资源的生命周期是以下几种情况:

  • 跟随某对象的生命周期,对象销毁时资源释放
  • 跟随某界面的生命周期,界面关闭时资源释放
  • 跟随某场景的生命周期,场景切换时资源释放

我们可以实现一个组件挂在到对象身上,当我们在该对象或该对象的其它组件中编写逻辑,加载资源时,使用这个资源管理组件进行加载,由该组件来维护资源的释放。界面和场景也类似。下一篇文章再聊一聊这个话题。

项目代码位于:https://github.com/wyb10a10/cocos_creator_framework ,打开Scene目录的ResExample场景即可查看

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/152539
推荐阅读
相关标签
  

闽ICP备14008679号