前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >UnLua invalid property问题定位与修复

UnLua invalid property问题定位与修复

原创
作者头像
Jozhn
修改2023-10-30 12:12:00
4210
修改2023-10-30 12:12:00
举报
文章被收录于专栏:Unreal游戏开发

现象

其实从UnLua1.0起就会偶尔遇到访问UObject上面的property是nil的情况,而且都是刚创建出来的UObject,就遇到了这个问题。

很显然,UnLua并没有每次都通过反射重新读UObject上面的property,而是读取了property的缓存。那就需要研究一下property的生命周期与UnLua是怎么管理并访问property的缓存的。

访问property原理

首先回顾一下UnLua是怎么访问一个UObject的property的。讲这个的文章太多了,UnLua从1.0以来这里核心逻辑其实没什么变化。这里就简单看一下,也不讲绑定了,可以自己看代码int FObjectRegistry::Bind(UObject\* Object)

在Lua代码中访问UObject的property时,会先走到UObject的Lua实例的元表的Index元方法(2.0起这些代码被放在了UnLuaLib.cpp中)。

这个元表就是我们实现UnLua接口GetModuleName中返回那个Lua在require后的Lua module(实际是一份拷贝,而不是那个module本身,目的是避免同时绑定到子类时冲突,可以看bool UUnLuaManager::BindClass(UClass\* Class, const FString& InModuleName, FString& Error)的实现),存在package.loaded里面。下面可以简称REQUIRED\_MODULE

这里可以忽略循环找Super,因为一般不会多重继承Lua。

关键点是先获取REQUIRED\_MODULE,然后local p = mt[k]。获取元表之后用key去访问,触发元表的Class\_Index元方法。这个元方法是来自这个UObject的UClass的元表,是首次绑定UObject时为UClass注册的,里面会在运行时缓存这个UClass的property。

undefined

Class\_Index核心是GetField,直接进去看。

GetField是先获取REQUIRED\_MODULE的metatable(就是放在Lua registry里的一个table,可以通过UE.XXX来访问,里面存储UClass的缓存信息FClassDesc等),然后看里面有没有这个property。

首次访问肯定是nil啊,那么看看GetFieldInternal做了什么。

GetFieldInternal比较长,我们看截图里这部分就够了。先把刚才拿到的nil pop出去。然后通过mt.\_\_name来拿这个UClass的名字,是蓝图的话一般是/Game/xxx/xxx.xxx\_C这样的名字。FieldName就是刚才lua中的mt[k]里的k,是property的名字。接着把ClassName pop出去。

下面通过ClassName获取ClassDesc,没有的话就会注册(其实既然已经绑定了不可能不存在)。然后通过ClassDesc获取这个property对应的Field。

再下面是关键,这里判断了Field->IsInherited(),如果这个变量是继承来的,就需要到父类的metatable中拿,因为生命周期是跟随父类UClass的。

如果父类metatable中有缓存,就说明是bCached的,也就是有缓存的。没有缓存的话就会走下面PushField重新从Class中拿然后再缓存了。

这里就把访问property的流程讲完了。

问题分析

那么问题可能出现在哪里?

  1. property是自己UClass中的失效的缓存
  2. property是父类UClass中的失效的缓存

就这两种情况。而且实际上,这两种问题是同时存在的。

首先,只有非Native的UClass才会被gc,其property才有可能失效。所以肯定都是蓝图类型的对象。

其次,不管是不是父类,缓存都存在property所属的UClass的metatable。

那么问题就是为什么UClass失效了,它的metatable没有被清理?

UClass的metatable是在NotifyUObjectDeleted时通过FClassRegistry::StaticUnregister清理的。

我们应该知道,UE的gc是有过程的,UObject被标记为没有引用到真正被gc清理是需要时间的。所以问题大概率是出在这里。

验证

我们可以构造一个环境,每帧创建蓝图对象,访问其property,然后移除引用等待gc。并且在UnLua蓝图类型的UClass注册和清理的地方增加日志查看时序。

另外问题2是来自父类,所以我们还要让蓝图对象继承自另外一个蓝图。

这样构造之后其实比较容易能够复现出来两个问题。

修复

问题1

问题1的原因是绑定UObject时会PushMetatable将对应UClass的metatable设置上去,但是这里并没有检查对应UClass的有效性,也就是UClass已经标记为代清理,但还没触发NotifyObjectDeleted事件,所以导致UObject绑定之后立即访问就遇到了失效的property缓存。

因此,在PushMetatable里面增加检查即可。

这个问题在去年9月提交给了UnLua的Github :

修复PushMetatable时会使用旧的metatable的问题 by jozhn · Pull Request #515 · Tencent/UnLua (github.com)

问题2

问题2原因和1很相似,但是复现概率会小一些,而且蓝图继承蓝图真的很少用。此外,频繁创建销毁也是不合理的操作,不过从逻辑上来说UnLua还是存在漏洞。

这个原因是蓝图B继承了蓝图A,在频繁创建销毁的某一次,B的实例创建之后访问继承自A的property,而A的类型处于BeginDestroy状态,但还没触发NotifyObjectDeleted。因此读到了A类型metatable中缓存的property。

这里为什么会用到旧的metatable呢,理论上GetFieldInternal里面会检查ClassDesc的有效性,无效就会注销并且清理metatable。但是有些情况下,通过FClassDesc::Load函数触发的重新加载UClass信息,不会清理metatable,所以产生了漏网之鱼。这也是这个问题复现概率更小的原因。

解决办法是针对访问的property来自继承的非Native的UClass时,检查其有效性(实际就是检查UClass的有效性),无效的话就忽略这个缓存,重新PushField,以取到最新的property。

这个代码目前也提交到了UnLua的Github:

修正:访问来自非native父类的property时检查有效性 #661 by jozhn · Pull Request #664 · Tencent/UnLua (github.com)

我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 现象
  • 访问property原理
  • 问题分析
  • 验证
  • 修复
    • 问题1
      • 问题2
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档