起因
- UE蓝图异步节点浅析 - AkiKurisu - https://zhuanlan.zhihu.com/p/20155522886
- The easiest way to memory leak in Unreal Engine
在阅读了这两篇文章后,想起了自己第一次见到RF_StrongRefOnFrame标记时的那份好奇,今日便一探究竟吧!
RF_StrongRefOnFrame在引擎中的使用
RF_StrongRefOnFrame在源码中的注释:References to this object from persistent function frame are handled as strong ones.
简单翻译一下:蓝图UberGraph函数的持久性栈帧对此对象的引用将被当做强引用处理。
在引擎中搜索RF_StrongRefOnFrame,结果如下图所示:
其中不乏一些常用节点的身影:
蓝图中构造对象节点ConstructObjectFromClass在其内部会调用UGameplayStatics::SpawnObject,于是所有由此节点构造出来的对象都会被标记上RF_StrongRefOnFrame。
播放蒙太奇节点UK2Node_PlayMontage内部使用的回调代理对象UPlayMontageCallbackProxy也设置了RF_StrongRefOnFrame标记。
UGameplayTask直接在构造函数里设置了RF_StrongRefOnFrame标记。而UAbilityTask继承自UGameplayTask,于是所有的UAbilityTask天生就自带RF_StrongRefOnFrame标记。
蓝图对象内存结构图
UE会将所有EventGraph的所有Event都合并到一个总的Graph中(也就是UberGraph)。UE为何要这样处理?因为UE希望允许Event之间互相使用彼此的局部变量(有点像闭包)。要达到此目的就需要确保这些局部变量的内存没有被提前释放掉。于是UE将UberGraph的执行栈帧UbergraphFrame的生命周期保持跟蓝图对象一样长。这样就用额外的内存占用为代价带来了跨Event引用变量的便利性。为了避免这些被大大延长了生命周期的局部变量的强引用造成内存泄漏,引擎默认这些UbergraphFrame上的UObject指针局部变量为弱引用。
什么是弱引用
首先弱引用是一个UObject类型的指针,并且它是EventGraph里的局部变量,且指向的UObject身上没有标记RF_StrongRefOnFrame。例如,上面的内存示意图中标记成红色的O2指针就有可能是弱引用。一个更直观的例子,下图中的Object指针就可能是一个弱引用。当这个Object标记了RF_StrongRefOnFrame那么这个指针就是强引用,否则就是弱引用。另外注意区分弱引用和TWeakObjectPtr,虽然它们在GC方面的作用相同但是内部实现原理却是大不相同的。
UE GC的自动置nullptr功能
UE的GC能自动帮忙置空那些指向无效对象的指针。例如一个指针指向了一个Actor,然后调用DestroyActor销毁这个Actor,一段时间后触发了GC,在GC中会扫描到这个指针并置为nullptr。这个自动设置nullptr不仅适用于强引用也适用于弱引用!当然这个神奇的魔法并不是对有所的UObject指针都自动生效,在C++中你需要标记UProperty,蓝图则不用。Lambda捕获的变量也不会被UE的GC扫描到。
个人对RF_StrongRefOnFrame的理解
首先,对于Actor以及ActorComponent没必要使用RF_StrongRefOnFrame标记的。因为Actor会被Level引用,而Level又被World引用。所以Actor根本不缺其它的强引用就可以确保自身不会被GC掉。而ActorComponent的生命周期一般会跟Actor绑定。
目前引擎中,RF_StrongRefOnFrame标记主要用于回调代理对象上,这个回调代理对象主要作用是将C++的事件通知到蓝图侧。至于为啥非要多一个代理对象,因为部分原始委托可能根本就没暴露给蓝图侧,也可能是因为原始委托是单播委托而蓝图侧只支持动态多播委托。
虽然代理委托会被绑定到原始委托,但是委托中是用TWeakObjectPtr存的代理委托对象,也就是C++侧并不会强引用这个代理对象,于是对这个代理对象进行强引用的责任落在了蓝图侧。而给回调代理对象添加RF_StrongRefOnFrame标记使得UberGraph中指向回调代理对象的指针从弱引用升级成了强引用,从而保证回调代理对象不会被提前GC掉。
遗憾的是引擎中很多地方遗漏了对RF_StrongRefOnFrame标记的清除,也就造成了一定程度上的泄漏。为啥说一定程度的泄漏,因为UberGraph中指向回调代理对象的指针只会指向最新的,当指向新的回调代理对象时旧对象就自动被GC回收了,故而一个回调代理对象指针最多只会造成一个对象泄漏。
修复泄漏
两种修复方式:
- ClearFlags(RF_StrongRefOnFrame);清除UberGraphFrame对此对象的强引用。
- MarkAsGarbage();明确销毁此对象,并且指向此对象的所有指针将在GC中自动置为nullptr。
给官方提了一个PR:
https://github.com/EpicGames/UnrealEngine/pull/12907