3.2.2 针对移动端App特点的引擎生命周期改造
移动端App相对于游戏而言,内存和CPU资源有限,需要进行优化和节约;用户在使用App时,往往需要同时处理其他任务,例如接电话、收短信等,所以需要考虑App的生命周期。针对以上特点,需对引擎的生命周期进行改造和优化,以适应移动端App的需求。
3.2.2.1 引擎生命周期改造的目的和整体思路
改造引擎生命周期的目的有两个:使宿主App更方便地被拉起及结束UE的功能;减少UE资源消耗、提高App性能和稳定性。
主要思路是通过完善引擎的生命周期来提供作为SDK的对外接口。Active、Quit、Reenter、Destroy、Pause、Resume是几种改变引擎生命周期的方式。其中Pause和Resume是提供给宿主App用来暂停和恢复UE使用的接口,当不需要UE进行渲染但仍然希望UE处于可被立即唤醒的状态时调用。
3.2.2.2 Active
引擎唤起的逻辑相对比较简单,在重构的启动器下,引擎会有一个完整的初始化流程。当引擎初始化成功后,会加载项目需要的地图关卡,进入正常的Game Thread运行逻辑。这里会有第一次初始化加载的时间,相较而言会稍微久一点儿,这里对UE初始化做了一些精简。整个过程对于这样完整的引擎拉起操作只有一次,如图3.9所示。
图3.9 引擎唤起的流程
3.2.2.3 Quit
引擎的退出实际上不是一次完整的析构流程,而是把一些重度资源进行释放。从宿主App传递退出事件开始,引擎就会进入退出的状态。首先,会把Gameplay用到的数据进行清理,然后将引擎退出的事件广播出去,让一些模块自行析构。其次,调用LoadMap方法,加载一个NullMap。NullMap是定义的一个概念,可以理解为没有资源的场景。引擎退出的时候加载这个NullMap场景,这样做的目的是执行一遍场景的析构逻辑,把当前地图引用的资源全部进行垃圾回收,在加载NullMap的Game Thread执行结尾处,把引擎的一些模块停掉。由于在加载NullMap的时候已经执行了一帧Engine Loop的逻辑,所以渲染线程实际上也没有需要再Flush的内容了,可以放心地把Render Thread停掉。同时把与着色器代码相关的资源卸载,引擎退出后就保持在NullMap的状态,如图3.10所示。
图3.10 引擎退出的流程
在停止Render Thread及卸载与着色器代码相关的资源之后,也会把RHI线程停掉。前文提到需要把引擎相对重度的资源释放掉,渲染资源实际上就是这部分比较重度的内容。这里定义一个RHI析构的方法,把渲染需要的RHI资源释放掉了,并且记录了资源的引用信息,在UE被第二次拉起的时候,会通过一些方法把它重载进来,而不是一个完整的UObject的初始化流程,这样就可以让UE退出的时候尽可能保持轻量:
3.2.2.4 Reenter
UE的初始化是一个较重度的操作,并不希望每次调起UE的时候,加载时间都较长。引擎第二次被拉起时,会加载一个EmptyMap,这个EmptyMap和NullMap是一个相对的概念,会在引擎二次进入的时候把引擎退出时析构掉的引擎基础模块及资源加载回来,其中包含析构掉的RHI模块,还需要把析构掉的UObject再加载回来。当所有加载任务完成后,会打开Render Thread,加载实际业务逻辑用到的地图,然后就可以走正常的游戏逻辑了,如图3.11所示。
图3.11 Engine Reenter
这里ReloadObject方法的内容是把引擎退出时析构掉的UObject重新加载进来:
3.2.2.5 Destroy
在Destroy(销毁)的流程中并没有对UE进行完整销毁的操作,UE本身没有考虑这种完整析构的情况,所以在设计生命周期时,UE的Destroy的生命周期是随着宿主App的,只要UE的代码和资源加载进来,析构就是随着宿主App进行的。
在Destroy流程中会挑选一些相对重度的模块进行析构处理,比如与整个渲染相关的资源,包括贴图、网格顶点数据等。除了这些,在iOS下,在Metal内部,Metal Queue(队列)的命令缓冲区、Metal解码器这些内容,都会占据大量的内存资源,这些内容并不是游戏制作中的资源,而是Metal自身所占据的资源,Metal运行时需要的东西所占用的资源。这些内容大概会占据20MB左右的基础资源。对于一个独立的游戏包体来讲,这个大小可能并不是很大,但是在App中就比较夸张了。所以这里选择的方式就是把RHI模块整个析构掉,将关联的整个RHI资源都清掉。并且要注意,这里在处理iOS下Metal占有的资源时,如果当一个进程内部出现多个Command Queue(命令队列)时,即便是把某一个Command Queue中所有的资源都释放掉,这个Command Queue依然会产生这个依赖,在最终的形态中也是释放不掉的,Metal的运行时设计就是这样的。
3.2.2.6 Inactive
在某些情况下,UE需要被暂时挂起,比如用户临时返回QQ聊天窗口,并且在切回UE的应用的时候,可以立即响应,但是又不能让UE的程序占用宿主App大量的资源,所以这里对引擎有一个Inactive状态的处理。当引擎收到Pause事件时,会进入挂起状态,停掉Engine Loop,转而进入更轻量的Tick(周期函数),如图3.12所示。
图3.12 引擎的Inactive流程
为什么需要这个Tick,而不是完全停掉所有线程的计算呢?原因是,这样依然可以保证UE的View窗口大小会随着宿主App的窗口大小变化实时更新,这样当从App切回UE的时候,就不会由于View大小设置得不及时,而产生视口缩放的效果,影响用户体验。当然这只是一个例子,可以再做不同程度的处理。但是在Inactive状态下是不可以做渲染的,渲染线程已经被停掉了,UE也不会承接这样的任务。如果没有任何需要Tick承载的任务,Game Thread也会进入休眠状态,从而进入更为轻量的模式。