Tencent / puerts

PUER(普洱) Typescript. Let's write your game in UE or Unity with TypeScript.
Other
5.07k stars 704 forks source link

[UE] UMG的嵌套使用导致内存泄漏. #1174

Open zzhprogram opened 1 year ago

zzhprogram commented 1 year ago

前提: 由两个UMG类, WBP_Main, WBP_Child, WBP_Main中引用了WBP_Child蓝图,在TS代码中,使用Mixin的方式进行蓝图混合(不确定是否和Mixin有关联),并在TS代码中正常使,创建UMG蓝图。

表现: 在游戏TS逻辑中正常使用 WBP_Main 蓝图。 使用 UE 控制台命令 memreport -full 查看内存情况,会发现WBP_Child存在多个,并且在切换场景的时候, WBP_Main, WBP_Child 和均不会被释放。
如果删除WBP_Main 中的 WBP_Child, 改为在 TS 中运行时动态加载 WBP_Child 绑定到 WBP_Main 上,则场景切换时内存可以正常释放。

分析: 通过 refs name = xxx(ClassName) shortest 指令查看引用链,发现了引用链条(WBP_Child 即为链条中的 WBP_ShopAppearanceExteriorColor_M9_C) LogReferenceChain: (root) GCObjectReferencer /Engine/Transient.GCObjectReferencer_0::AddReferencedObjects(): FBlueprintActionDatabase LogReferenceChain: BlueprintVariableNodeSpawner /Engine/Transient.BlueprintVariableNodeSpawner_20599->Property LogReferenceChain: WidgetBlueprintGeneratedClass /Game/M9City/Blueprints/UMG/City/WBP_ShopAppearanceExterior_Main.SKEL_WBP_ShopAppearanceExterior_Main_C::AddReferencedObjects(): SKEL_WBP_ShopAppearanceExterior_Main_C LogReferenceChain: WidgetBlueprintGeneratedClass /Game/M9City/Blueprints/UMG/City/WBP_ShopAppearanceExteriorColor_M9.WBP_ShopAppearanceExteriorColor_M9_C

通过资料查找,unlua 也有类似的情况,在unlua的提交中,也有对此问题的修复。 image

chexiongsheng commented 1 year ago

方便提供个简单的例子么?最好是fork这个工程:https://github.com/chexiongsheng/puerts_unreal_demo ,在它基础上加代码。

chexiongsheng commented 1 year ago

是不是WBP_Child的ts代码里头,有类似注册了一个js回调到WBP_Child的Delegate,js回调中引用WBP_Main了呢?

zzhprogram commented 1 year ago

git@github.com:zzhprogram/PuertsDemo.git 可以clone一下这个demo,里面的Demo 解释.doxc 描述了如何使用项目。

chexiongsheng commented 1 year ago

应该就是循环引用了。 之所以你注释掉“AddReferencedObjects”,就能gc,并不能说明是这导致了问题。

FObjectRetainer的作用就是当js还在使用(引用)那个ue对象,告诉引擎别回收那个ue对象,本来如果js那没引用了,这个FObjectRetainer自动就会释放那UE对象。你注释掉了,就无论js有没持有都会释放。这也是“你觉得解决问题的原因,因为肯定不持有”,这也是你注释掉后崩溃的原因,因为仅js持有对象被ue认为没引用,释放了,后续js还操作就崩溃了。

两套gc因为信息不完整,如果两边的引用形成了个环后,都会循环引用导致两边的对象同时释放不了。这是类似的方案都会存在的问题。数学上无解。

你需要做的是业务手动解环,比如你的实例代码里头,找个地方把注册到umg的所有js回调都Clear掉,应该就能释放了

Tangxinwei commented 1 year ago

这个其实需要自己在ts层有个ui的框架,umg和actor不太一样,umg只会destruct,真实的gc需要依赖没有引用关系,只要ts里面还持有这个umg,就会出问题。简单一点的解决方案,在destruct的时候延迟一帧markpendingkill(list的重排也会触发)。复杂一点的话,就是umg的uobject不要直接暴露给外部,而是ui框架层持有,destruct的时候,ui框架释放引用。

zzhprogram commented 1 year ago

这个其实需要自己在ts层有个ui的框架,umg和actor不太一样,umg只会destruct,真实的gc需要依赖没有引用关系,只要ts里面还持有这个umg,就会出问题。简单一点的解决方案,在destruct的时候延迟一帧markpendingkill(list的重排也会触发)。复杂一点的话,就是umg的uobject不要直接暴露给外部,而是ui框架层持有,destruct的时候,ui框架释放引用。

我更新了一下项目,换成单例管理,创建UMG,并且把能想到的puerts对UMG的引用都清理掉了,但是切换关卡仍无法GC,puerts仍保留引用,可以帮忙看看原因吗?仓库地址: https://github.com/zzhprogram/PuertsDemo.git, 具体看Demo解释.docx文件。

Tangxinwei commented 1 year ago

简单看了下代码

  1. destruct拼错了
  2. mixin的时候应该objectTakeByNative为true(不过即便是false,只要正常清理了,也不会泄露,只是设置为true更好) 然后可以通过obj refs 配合inspector看看uobject在ts里面的引用链,这样方便确认是puerts层的引用没释放,还是自己的ts引用没释放
zzhprogram commented 1 year ago

简单看了下代码

  1. destruct拼错了
  2. mixin的时候应该objectTakeByNative为true(不过即便是false,只要正常清理了,也不会泄露,只是设置为true更好) 然后可以通过obj refs 配合inspector看看uobject在ts里面的引用链,这样方便确认是puerts层的引用没释放,还是自己的ts引用没释放

可以看看我 <Demo解释.docx> 文档,里面我没用Mixin的UMG,入口文件是UMGNestedTest.ts, 仅仅是一个原生的 UMG类,绑定按钮函数,切换关卡后解绑按钮函数,并清除ts对UMG的持有,但是切换关卡后,obj refs 仍显示 Puerts_UserObjectRetainer保留了引用。

Tangxinwei commented 1 year ago

你这个里面显示的是uclass的引用链,能不能显示下uobject的。这个是打包之后的吗?如果是editor的话,好像uclass是不会回收的

Tangxinwei commented 1 year ago

还有就是ts的gc不一定非常快,你要连着inspector,手动强行gc一次ue和v8