Slate 组件问题排查总结简介
首先是一个工作中遇到的BUG: 用slua添加子节点到父节点上的时候,第二次打开无法显示对应的子节点Widget。对应Lua代码如下
local comboBox = ui_manager.ShowUI(ui_manager.UI_Config.ui_coupon_combobox,2,price,buyUIInfo.shopInfo.id)
if comboBox then
self:AttachChildWindow("ScaleBox_Coupon",comboBox)
end
因为我们引入了UI对象的内存池概念,很自然而然的想到了缓存住的控件是不是释放有问题。检查slua的release代码之后发现没有任何问题。基于ScaleBox 只能有1个子物体这一个机制,在猜测BUG原因的时候,尝试用CanvasPanel替换ScaleBox组件。发现果然能解决这个问题。
但是也带来了2个新问题:
1、ScaleBox子物体在第一次关闭和第二次打开的时候经历了什么?
2、为什么CanvasPanel就可以而ScaleBox不可以?
为了解决以上疑问,开启我们的排查过程。
为了能使大家更好的了解这个流程,先对Slate组件创建销毁流程进行介绍。Slate组件创建销毁流程
1、将我们日常使用的一个组件,ScaleBox拆出来看(如下图):有如下的对应关系。里面包含了 UScaleBox,UScaleBoxSlot,SscaleBox 。
2、创建过程
3、销毁过程
问题排查
针对ScaleBox,进入断点调试。
在第一次关闭界面的时候,发现UscaleBox Release 有正常跑到(下面代码部分)。
void
UScaleBox::ReleaseSlateResources(bool bReleaseChildren)
{
Super::ReleaseSlateResources(bReleaseChildren);
MyScaleBox.Reset();
}
但是在第二次打开的时候,发现没有进入RebuildWidget(Swidget的实例化部分,也就是下面代码部分)
TSharedRef<SWidget>
UScaleBox::RebuildWidget()
{
MyScaleBox
=
SNew(SScaleBox)
.SingleLayoutPass(bSingleLayoutPass);
if
(
GetChildrenCount()
>
0
)
{
CastChecked<UScaleBoxSlot>(GetContentSlot())->BuildSlot(MyScaleBox.ToSharedRef());
}
return
MyScaleBox.ToSharedRef();
}
这是为什么呢? 原来,在UWidget的BUILD过程中,有一个查找共享指针MyWidget的操作。这个指针虽然被UscaleBox Reset了一次。但是在ScaleBox 第二次打开时候,这个指针依然存在引用。所以该实例并没有被销毁。那么在第二次打开的时候,由于没有走RebuildWidget。导致了子类UScaleBox 的指针引用已经被销毁,在AddChild时候调用添加了个空Object。(下图代码部分)
TSharedRef<SWidget>
UWidget::TakeWidget_Private(ConstructMethodType
ConstructMethod)
{
bool bNewlyCreated =
false;
TSharedPtr<SWidget>
PublicWidget;
// If the underlying widget doesn't exist we need to construct and cache the widget for the first run.
if
(!MyWidget.IsValid())
{
PublicWidget
=
RebuildWidget();
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
ensureMsgf(PublicWidget.Get()
!=
&SNullWidget::NullWidget.Get(), TEXT("Don't return SNullWidget from RebuildWidget, because we mutate the state of the return. Return a SSpacer if you need to return a no-op widget."));
#endif
MyWidget
=
PublicWidget;
bNewlyCreated =
true;
}
else
{
PublicWidget
=
MyWidget.Pin();
}
那这个指针是被谁Hold住了呢? 经过排查与调试,发现是被UScaleBoxSlot给Hold住了导致了这份资源没有被释放。
为什么在UScaleBox中的共享指针已经释放了,但是UScaleBoxSlot所持有的相同指针没有被释放呢?请看Slot释放的地方(下面代码模块):
// If the child is a UserWidget, we should let it manage it's own slate resources instead of forcing a clear here. This fixes issues such as UE-39106
if
(PanelSlot->Content
&&
!PanelSlot->Content->IsA<UUserWidget>())
{
const
bool bReleaseChildren =
true;
PanelSlot->ReleaseSlateResources(bReleaseChildren);
}
UE的注释写的很清楚了,我们所Add 的子物体,正是UserWidget 界面。在UE-39106版本中,让UserWidget 的Slate释放控制交给了上层。在这里没有自动释放。下面是UE 39106的改动链接。https://issues.unrealengine.com/issue/UE-39106
虚幻引擎UE为了补锅一个slate 被释放的问题,导致了这个新问题。 我们上层缓存了这份实例,销毁子物体的时候调用了RemoveFromParent。在ScaleBox被销毁的时候,因为其子物体为userwidget,而不去释放PanelSlot的SlateResourse。那么在第二次打开的时候,因为PanelSlot中共享指针的存在,没有New一份新的子物体实例。
OK,问题定位了。那么之前提到的第二个问题,为什么换成CanvasPanel就没有这个问题了呢?,既然查到了在ScaleBox 中是 ScaleBoxSlot 的指针Hold住了这份资源,那么我们去CanvasPanel对应的Slot 看一下。(下面代码)
void
UCanvasPanelSlot::BuildSlot(TSharedRef<SConstraintCanvas>
Canvas)
{
Slot
=
&Canvas->AddSlot()
[
Content
==
nullptr
?
SNullWidget::NullWidget
:
Content->TakeWidget()
];
SynchronizeProperties();
}
明显可以看出:原来CanvasPanelSlot 在Build过程中没有缓存那份实例!只是做了常规引用!在上面有贴出过ScaleBoxSlot build 过程的代码。所以就是ScaleBoxSlot 存住了这份指针,而CanvasPanelSlot 并没有。所以在都没有Release对应掉这份资源的情况下,CanvasPanel会走rebuild过程,而ScaleBox不会!
到这里,我们基本上就定位出了问题全部原因所在。解决办法
这里想到了4种解决方案,都能解决这个问题,这里简单说一下:
1、不判断指针是否还存在引用,全部走Rebuild过程。
2、在Remove判断是否为UserWidget过程中,对本身PanelSlot做一次释放。
3、删除UScaleBoxSlot中对SscaleBox指针的引用。
4、将UScaleBoxSlot中的共享引用改为弱引用。
第一种风险较大,因为所有UMG都会走的创建过程,不太保险。
第二种风险小于第一种,但是也在所有UMG都会走的创建过程,不太保险。
第三种和第四种个人感觉都是有效的方案,总的来看还是第四种改动小并优雅一点。
所以目前采用了第四种。