文章目录
- 概述
- 明确两套记录
- 推荐阅读
- 1.引用计数只在 Resource.destroy 里面连锁
- 2.加载 .lh 时,谁握住了图集
- 3.createNodes 之后,界面又握了一次贴图
- 4. 为什么只 destroy 窗口,图集还在
- 5.clearRes 清的是根地址
- 6.和 GRoot / GWindow 的接缝
- src和封装的对应
- 自己改框架时核对这几件事
概述
- 当前只覆盖「GRoot 上分层、窗口继承 GWindow、内容是 .lh 预制体并赋给 contentPane」这条链路。
明确两套记录
| 显示节点 | 资源对象 | |
|---|---|---|
| 是什么 | GWindow、contentPane、里面的 GImage | .lh 对应的 Prefab、图集 AtlasResource、贴图 Texture / Texture2D |
| 放在哪 | 父节点的子节点列表,业务上挂在层里 | Loader.loadedMap(按 url 缓存)和 Resource._idResourcesMap(可被统一扫描的资源) |
| 怎么结束 | Node.destroy | Resource.destroy,或 Loader.clearRes |
| 计数 | 没有引用计数 | Resource.referenceCount |
- Laya.loader.load / getRes 命中缓存时,只把缓存里的对象交出来,不会给 referenceCount 加一。所以「缓存里还有」和「还有人引用」是两件事。——如果进缓存就算引用加一,计数永远到不了 0,这批资源就清不掉了。
- 业务租约(ResLease.leaseCount)是第三个数,只记录「还有几个窗口或 pin 声明要留着这个根 url」。它用来决定能不能对这个根 url 调用 clearRes。
补充:
- 图集 AtlasResource
- 1、textures<Texture。>:大图页(里面每个元素是一整页大图的 Texture),真正占显存的那几张位图(Texture.bitmap<Texture2D.>)。
- 2、frames<Texture.>:每一张小图。AtlasLoader 用 Texture.create(大图, x, y, 宽, 高) 切出来,所以每个 frame 都是一个 Texture,很多 frame 共用同一张大图。——Texture.create(大图Texture, x, y, 宽, 高) 切出小图,这些小图的 bitmap 都指向大图那个 Texture2D。
- 3、因此界面上的一个图标往往不是一张独立图片。它只是图集大图上的一个切块。回收时放掉这一个切块,显存里的大图还在;要等这张大图上的切块都不再被引用,对应的 Texture2D 才会释放。
- Texture
- 绘制时用的一块区域,记录从哪张位图的哪个矩形来画。
- GImage 画的是它。这块区域可以是整张图,也可以只是大图上的一小块。
- Texture2D
- png加载出来在显存的位图。——load(“a.png”) 交给调用方的主资源是包好的 Texture,不是直接把 Texture2D 交出来。Texture2D 在 Texture.bitmap 上。
- 贴图:
- 没打进图集的:既指代单张png,又指代显存位图(Texture2D)。文件和显存对象是同一张图的两个阶段。——一张 png 对应一个 Texture2D,再包一个盖住整张图的 Texture。文件、显存位图、绘制对象是三层
- 打进图集的:图集大图本身也是一张 png,它加载成一张 Texture2D(再包成 textures 里的整页 Texture)。界面上的每个小图标是 Texture,只记录这张大图上的一块矩形,很多小图标共用这一张 Texture2D。这时「单张小图」不是一张 Texture2D。
- (贴图 > Texture2D) >>> (Texture< bitmap指向Texture2D>)
- 引用计数具体场景例子:用一个只含一张图的窗口来看。窗口预制体叫 Bag.lh,里面只有一个 GImage,画的那张图叫 T(就是 GImage 握着的 Texture)。
- 这种情况有2个引用,1个是绘制命令增加的,一个是预制体的deps握着的。
- 去引用一般是先 拆界面:window.destroy()。 这一步会销毁GImage节点。绘制命令被 recover()——调用了this._texture._removeReference();【T引用为1了】
- clearRes(“Bag.lh”)。 Loader._clearRes 把缓存条目删掉,并调用 Bag.lh.destroy()。destroy 里先把 _disposingCounter 加 1,再对 deps 里的每个资源 _removeReference。——这时候条件都满足,T.destroy() 就在这一步被调用,成功销毁T。
推荐阅读
- resource/Resource.ts:_addReference、_removeReference、addDep、destroy、destroyUnusedResources
- net/Loader.ts:_cacheRes、getRes、_clearRes、createNodes
- loaders/HierarchyLoader.ts 和 resource/HierarchyResource.ts、resource/PrefabImpl.ts
- display/cmd/DrawTextureCmd.ts 的 texture setter 和 recover,再看 ui2/ImageRenderer.ts 的 destroy
- display/Node.ts 的 destroy,display/Sprite.ts 的 destroy,ui2/GImage.ts 的 destroy
- ui2/GWindow.ts 的 contentPane、destroy、hideImmediately,ui2/GRoot.ts 的 showWindow、hideWindowImmediately
- 读完应能回答:关掉一个窗口之后,图集为什么还在显存里,以及哪一次调用才会把它清掉。
- 与后续标题一一对应解答。
1.引用计数只在 Resource.destroy 里面连锁
Resource._removeReference(约 295 行)减计数。减到 0 以后,只有下面四个条件同时成立才会立刻 destroy:
- 当前正处于某次 Resource.destroy(_disposingCounter > 0)
- referenceCount <= 0
- lock 不为 true
- destroyedImmediately 为 true
显示节点销毁时的减引用,<比如绘制命令被 recover()>发生在这次 Resource.destroy 之外,_disposingCounter 是 0,所以贴图不会在拆界面的那一步被销毁。
Resource.destroy(约 358 行)自己做四件事:
- 调用 _disposeResource
- 对 deps 里每个资源 _removeReference(连锁从这里开始)
- 从 Resource._idResourcesMap 删掉自己
- 若有 url,调用 Loader.clearRes(url, this),把缓存条目去掉
destroyUnusedResources(约 69 行)另开一条路:扫描 _idResourcesMap,把 lock 不为 true 且 referenceCount === 0 的资源 destroy。它不打开 Loader.loadedMap 逐个看。一个资源若构造时 managed === false,就不在这张表里,扫描看不到它。
Prefab 的构造是 super(false)(HierarchyResource.ts 约 21 行),默认不进这张表。onLoad 才把它登记进去。因此加载完成的 .lh 会被 destroyUnusedResources 扫到。
2.加载 .lh 时,谁握住了图集
HierarchyLoader._load(约 51 行)在依赖都加载完之后:
newPrefabImpl→onLoad()→addDeps(依赖数组)→ 返回给 Loader 写入缓存onLoad()//记录到 Resource._idResourcesMapaddDeps(依赖数组)//持有依赖资源;依赖资源引用计数+1;同时这里持有资源对象,是为了后续减少引用用。返回给 Loader 写入缓存;//Loader的_load2里面接收Promise,得到返回后,作为内容调用_cacheRes()写入loadedMapLoader._cacheRes(约 1061 行)只是 loadedMap[url] = [typeId, data]。这一步不加引用计数。
所以一个刚加载好、还没被别的预制体依赖的根 .lh,常见状态是:
- loader.getRes(url) 能拿到它
- 它自己的 referenceCount 仍是 0
- 它的 deps 里每个图集的 referenceCount 大于 0
注意:
- referenceCount === 0 在根 .lh 上是正常状态,表示「没有别的 Resource 把它列进 deps」,不表示界面上没有人在用它。
- Texture._addReference(Texture.ts 约 398 行)会把同一次加计传递给 _bitmap 和 _atlas。AtlasResource 构造时(约 88 行)会给自己的帧和图集页各加一次引用。读计数时看到的是这条链叠出来的结果,不会等于窗口个数。
3.createNodes 之后,界面又握了一次贴图
- 生成节点树
- instantiate -> 加载 -> 创建
- Loader.createNodes(约 1039 行)是 getRes(url).create()。——得到Prefab实例<PrefabImpl.>
- PrefabImpl.create(约 89 行)解析 json,生成节点树,并把根节点的 url 设成预制体地址。——解析.lh的JSON为节点树。
- GImage - 管资源和尺寸
- ImageRenderer - 选并维护绘制命令 —— 有removeRef 和 addRef;创建绘制命令后把 cmd.lock = true;
- DrawTextureCmd - 在渲染时把这张图画出来 —— set texture会addRef,recover有removeRef。
- Graphics - 控件上的 graphics 就是一个 Graphics 实例,里面的 _cmds 保存绘制命令。—— Graphics.destroy 遇到 lock 的命令不会 recover。;GImage 的贴图引用是在 ImageRenderer.destroy 里还掉的,不是在 Graphics.destroy 里。
- Sprite - Sprite.texture 是另一条路(Sprite.ts 约 1267 行和 destroy 约 311 行),同样成对加减引用。
- GWindow的contentPane - 对旧内容只 removeChild,不 destroy。旧节点若不再挂到别的地方,要自己销毁,否则它握着的贴图引用还在。(本封装只赋值一次;销毁整窗时,Node.destroy 会递归销毁子节点,contentPane 作为子节点一起拆掉。
4. 为什么只 destroy 窗口,图集还在
拆掉 GWindow 之后:
- 绘制命令上的贴图引用已经还掉
- 但Prefab 仍在 loadedMap 里
- 但Prefab.deps 仍握着图集和贴图 ——【***】
- 虽然根 .lh 自己的 referenceCount 往往还是 0
调用 Resource.destroyUnusedResources(),扫描会看到这个 .lh:lock 是 false,referenceCount 是 0,于是 destroy 它。destroy 再对 deps _removeReference。若某个图集因此到 0,并且没有 lock,它会在这次连锁里被销毁。
5.clearRes 清的是根地址
- 本封装的 ResLease.flush 只处理根 url,并且还要这个根资源自己的 referenceCount === 0。根 .lh 的计数不包含绘制命令,只在别的 Resource 把它列进 deps 时大于 0。没有这种依赖时,这个数就是 0,clearRes 可以执行。若它仍大于 0,这次跳过,等那个持有者 destroy 时再 _removeReference,计数到 0 才连锁销毁。
- Loader.clearRes(url) 不带第二个参数时,_clearRes(约 1127 行)先删掉 loadedMap 整条,再对里面还没销毁的 Resource 调用 destroy。这里不读 referenceCount,也不看别的对象的 deps。
- Resource.destroy 末尾会再调 clearRes(url, this)。这时对象已经标记销毁,这次只从缓存摘掉自己,不会第二次 destroy。
- 共享图集的正确释放方式是:只对根 .lh(以及你额外单独 load 的那个 url)调用 clearRes。图集留在 deps 里。Prefab.destroy 对它走 _removeReference,计数还大于 0 就不会连锁 destroy。另一份计数可以来自另一个窗口的 .lh(它 addDep 过这张图集),也可以来自还在显示的 DrawTextureCmd。
- 对依赖的 url,例如图集地址,直接 clearRes,走的是上面的无条件 destroy。别的预制体的 deps 里虽然还指着这个对象,那只是一个数组,不会挡住这次 destroy。正在显示的 DrawTextureCmd 同样挡不住。图集的 _disposeResource 会把帧和图集页的引用减掉,画面上的图会坏。那个预制体的 deps 里则留下一个已经销毁的对象。
- 情景:bag.lh 和 shop.lh 共用 ui.atlas 时,只对已经不用的那个根 .lh 调用 clearRes。背包关掉之后清 bag.lh,商店还开着就留着 shop.lh。商店也关掉、画面上的图片命令已经还掉引用之后,再清 shop.lh。最后一份预制体引用被还掉时,图集计数到 0,会跟着被释放。
- 结论:整个过程都不要对 ui.atlas 调用 clearRes。预制体还在 deps 里握着它,或者窗口还在画它的时候去清图集地址,会直接把图集销毁,正在显示的图会坏。
6.和 GRoot / GWindow 的接缝
- GWindow.show 调用 GRoot.showWindow,窗口成为 GRoot 的直接子节点(GRoot.ts 约 75 行)。
- GWindow.hideImmediately 调用 GRoot.hideWindowImmediately,后者只在 win.parent == this(父节点就是 GRoot)时 removeChild(约 107 行)。窗口若挂在层节点上,这条官方路径不会把它从层上拿下来。
- GRoot.adjustModalLayer(约 286 行)只检查 GRoot 的直接子节点里有没有 modal 的 GWindow。层上的窗口进不了这套遮罩。
所以业务层是自己放在 GRoot 上的全屏 GWidget,窗口是层的子节点。UIWindow 重写了 show、hideImmediately、bringToFront。遮罩是同层里窗口下面的一个 GWidget,只挡这一层。 - GWindow.destroy(约 305 行)若还在父节点上,会先 hideImmediately,再 super.destroy()。UIWindow.destroy 调用它,返回之后才归还租约。这样这份窗口的绘制命令已经还掉引用。随后的clearRes 销毁预制体时,只会再还 deps 里的那一份。别的预制体或还在显示的绘制命令若仍握着同一张图集**,图集不会在这次连锁里被销毁。
src和封装的对应
| src行为 | 封装 |
|---|---|
| Loader.load + loadedMap | UIWindow.loadContent 里 Laya.loader.load |
| Prefab.addDeps 握住依赖,根预制体 referenceCount 仍常为 0 | ResLease 在租约大于 0 时对根资源 lock = true |
| Loader.createNodes / PrefabImpl.create | Laya.Loader.createNodes,结果赋给 contentPane |
| DrawTextureCmd.recover 在节点销毁时还贴图引用 | UIWindow.destroy 先 super.destroy(),再 dropLease |
| Loader.clearRes 不看引用计数 | ResLease.flush 只在租约为 0 且根资源 referenceCount === 0 时调用 |
| deps 的连锁释放 | 不单独 clearRes 图集 url |
| destroyUnusedResources 会扫到未 lock 的根 .lh | 默认不自动调用;resLease.gc() 显式调用。租约还在的根资源已被 lock |
| GRoot.showWindow 把窗口挂到 GRoot | UILayerHost 建层,UIManager.open 把窗口 addChild 到层上 |
| hideWindowImmediately 只处理父节点是 GRoot 的窗口 | UIWindow.hideImmediately 从当前父节点移除 |
自己改框架时核对这几件事
- 租约是业务这边记的「这个根 url 还要留着」。引擎的 loadedMap 和 referenceCount 都不知道窗口还要不要用它。
- 新加的常驻界面,用 resLease.pin(urls),让根 .lh 在整个过程中保持 lock。
- 窗口里又 Laya.loader.load 了预制体依赖之外的 url,把这个 url 放进 extraUrls,让它和预制体同一份租约。
- 漏掉的话,这个 url 没有租约,flush 不会 clearRes 它,会一直留在 loadedMap。
- 已经在根 .lh 的 deps 里的图集、嵌套预制体不用放进 extraUrls。
- 替换 contentPane 时,把被 removeChild 的旧节点 destroy 掉(为的是还掉它握着的贴图引用)。
- 销毁顺序保持「先整棵显示树,再还租约」。
- 顺序反了的结果:
- 先 clearRes 时,绘制命令还握着贴图,图集计数到不了 0,连锁销毁不会发生。
- 等节点随后销毁,减引用又落在 Resource.destroy 之外,图集计数即使到 0 也会留到下一次 destroyUnusedResources。
- 只对根 url 调用 clearRes。图集、嵌套 .lh 走 deps。
- 打开 resLease.debug,需要对照引擎日志时再设 Laya.Resource.DEBUG = true。用 resLease.inspect(url) 看 leaseCount、engineRef、locked、deps。根 .lh 的 engineRef 为 0 且 locked 为 true,是打开期间的正常结果。
- resLease.gc() / autoGc 会销毁所有未 lock 且 referenceCount === 0 的资源。
- 没有走租约、只是预加载在缓存里的 .lh 会在这一步被拆掉。
- 正在显示但根 .lh 没 lock 的窗口也一样:预制体会被拆掉,节点还在;
- 图集只要绘制命令还握着,就不会一起没。