最近调试UE4在移动设备(准确说只有iOS设备)上反复打开UI面板导致崩溃的问题,发现了UE4 UObjects管理的特殊方式,很有意思,记录一下。

       一般C++的自动内存管理,都会使用智能指针,使用引用计数的方法,在一个对象的引用计数清零时将内存释放。在UE4中,自己创建的通常对象(非UObjects的派生类)也一般使用UE4提供的智能指针。但是,UObjects极其派生类则不使用智能指针,而是有自己的一套内存管理方式(当然大家都知道啦),无论类的创建还是销毁都有特殊的方法。

       UE4对于UObjects没有使用虚拟机,而是引擎手工管理所有UObjects的释放工作。在UE4中,有一个 FFixedUObjectArray 类(定义在 UOBjectArray.h 中)。这个类管理了 FUObjectItem 数组, FUObjectItem 就指向游戏中每个UObjects(及其派生类)。有点像UObjects的对象池。在每个UObjects执行 BeginDestroy() 逻辑后,就会被标记为 dirty。然后在游戏逻辑的tick中,逐步将被标记为dirty的每个UObject释放,避免了一次性GC太多对象导致的卡顿,对于保证使用streaminglevel的大型场景的流畅度非常有用,不然在场景block边缘游走总是GC结果卡了谁受得了。

       接下来讲一个之前提到了诡异的BUG:移动设备上反复打开UI面板导致崩溃。这里就不得不吐个槽,UE4的UI(UMG)真叫一个复杂,每个panel(canvas)都要上千个UObjects。对于一些复杂的UI,比如人物的装备界面,或者好友聊天界面,大地图界面,那就5、6千的UObjects了。

       为了内存不爆炸,是需要控制UObjects的对象的最大上限的。有个只读的consoleVar管理这个逻辑,就是 gc.MaxObjectsInGame 。这个变量UE4对于通常平台的设置是2,000,000。但是,请记住, FFixedUObjectArray 中保存了 FUObjectItem 数组,每个 FUObjectItem 对象中有一个指针,还有三个int32属性,就是说,在64位系统上每个 FUObjectItem 要占用 8 + 3 * 4 = 20 字节内存。200w对象的话,就要占用38m多一点内存,对于桌面和主机平台来说还好,但是对于移动设备,特别是iPhone6/6s这种内存只有1g可用内存更少的设备来说,开销还是很大的,所以还是要把这个值设置的小一些,一旦超过,UE4立即崩溃,毫不客气。

       这个问题,Epic的人在将堡垒之夜移植到移动平台的时候,发现了,开始他们只完成了堡垒之夜的iOS版本,相信在 4.19 的开发过程中他们也发现了这个问题,所以,他们 在iOSEngine.ini中,把 gc.MaxObjectsInGame 给写死了,值为131072,在iOS上占用正好 2.5m 内存(iOS已经没有32位了)。13w对象对于吃鸡这种场景不太复杂,UI也相对简单的项目来说也许够了,但是如果开发的是MMORPG,就要命了,场景复杂,同屏可动物体极多,UI面板数量也极大。如果你的UI是单独的Scene(level)到还好,如果是直接盖在场景上的话,那场景要占用7、8w的UObjects,然后每个面板都要3、4千甚至5、6千。打开面板然后关闭后,UI的UObjects并没有真的销毁只是被标记为dirty等待之后tick中真正的destroy,iOS短时间快速打开关闭UI导致崩溃的问题就产生了。

       总结一下,就是如果在移动平台开发UI比较重的游戏,最好还是把 iOS 上的 gc.MaxObjectsInGame 限制放宽一些,我们现在在 Android/iOS 上的限制均为 20w,现在看来运行应该还可以,这个数目也希望能给后人参考,在4g+内存设备成为主流之前,这个限制应该还是要保留一段时间。