3.2.1 启动器改造—集成移动端App的关键起点
为了和宿主App更好地交互以适应作为SDK的需要,需要修改UE的启动部分,同时也要保证UE在App中的稳定性,预期UE的运行环境与宿主App的运行环境能够相对独立。
3.2.1.1 Android方案
App的进程结构在整体上分成两个部分:Host Process和UE Process,如图3.2所示。UE相关的内容放在一个独立的UE进程中运行。这样做的好处是,UE的环境会更加纯净,可以保证App运行得更加稳定。比如,UE进程崩溃了,但是并不会影响宿主App。
图3.2 Android结构设计
在UE的进程内部又分为以下三个部分。
● UE4:UE运行需要的线程集合。
● Service:负责从宿主App拉起UE的运行环境。
● Activity:负责承载UE的一些功能,例如,小游戏之类的内容。
宿主App唤起UE的流程如图3.3所示,以Service来启动UE4,并且将Surface(Surface使应用程序能够渲染图像以在屏幕上呈现)传递到UE,UE提供切换Surface的机制,可以使用不同类型的Surface来适应不同的情况。这些情况包括需要叠加到Native UI上的QQ秀业务场景,以及全屏游戏业务场景等。
在Android中,Surface提供了一种将图像渲染到屏幕上的方法,是图像的源,无论开发者使用什么渲染API,一切内容都会渲染到Surface上。宿主App和UE进行交互的核心就在于Android的Surface是能够跨进程传输的,如图3.4所示[1]。
图3.3 UE4中的Surface切换
图3.4 Surface跨进程传输
在本质上,UE侧向宿主App获取了一项内容,就是这个Surface。这样宿主App只需把Surface传递到UE Process这个独立的进程中,UE就会根据这个Surface进行处理。
在UE环境初始化时,宿主App会传输一个Surface,并且调用一个接口,通过一些方法直接把这个对象序列化出来,UE拿到结果后根据这个Surface对象搭建自己的EGL环境,包括整个与交换缓冲区相关的内容,整体的流程基本上就走通了。代码如下所示。
图 3.5[2]展示了Android图形渲染中关键组件是如何协同工作的。
图3.5 Android图形渲染流程
在实际使用中,通常不会直接操作Surface对象,而是使用基于Surface的视图控件(这里用到了SurfaceView和TextureView),其封装了Surface的创建、管理和销毁等内容,更方便使用。同时视图控件还可以自动处理与Activity生命周期相关的操作,可以避免内存泄漏或者资源浪费问题。
那选择SurfaceView还是TextureView呢?SurfaceView对应的Surface直接被SurfaceFlinger消费,并与SurfaceFlinger相关联,SurfaceView可以在单独的线程中进行绘图操作,不会影响UI线程的性能,性能较高,如图 3.6所示[3]。
图3.6 SurfaceView渲染流程
TextureView对应的Surface需要被二次渲染到ViewRoot上(也就是Activity对应的Surface),并且这个过程需要在UI线程中执行,所以效率较低,如图3.7所示[3]。
图3.7 TextureView渲染流程
相比较而言,TextureView能做的事情更多[4],最终它是融合到UI框架体系中的,可以把它叠加到Native的UI上。比如,做一个动作表情并把它贴到聊天窗口中,需要一个Alpha信息,这时就可以应用TextureView去解决这个问题。在游戏模式下会选择SurfaceView,效率更高。
3.2.1.2 iOS方案
相较于Android,iOS是不允许多进程结构的,所以只能把UE嵌入宿主App,没有很好的办法规避其产生的不稳定行为。
App的帧率要求相较游戏来讲是比较高的,在每秒60帧以上。但是在有些情况下UE的单个线程耗时就会达到16ms,甚至更多。比如UE启动的过程,或者是加载复杂场景的过程,耗时相对较长,所以需要想办法避免这种情况导致的FPS抖动问题。这里增加了一个Engine Thread(引擎线程)来解决问题。Engine Thread是用来与UE SDK进行通信的,同时它也负责和UI Thread(主线程)进行交互。这个线程中的所有操作都是异步的,这样能保证在使用App的时候,UI Thread不会受到任何阻塞。
在iOS中唤起UE的流程如图3.8所示。UI Thread将启动引擎的请求发送到Engine Thread中去处理,Engine Thread会实际处理UE SDK中的引擎启动逻辑。当引擎启动成功,或者引擎启动失败时,UE SDK会将返回值给到Engine Thread,Engine Thread会将结果同步给UI Thread,引擎的启动流程就完成了。
图3.8 在iOS中唤起UE的流程
有一个问题需要考虑,在实际场景中使用静态库还是动态库。这里根据需求做了一些变动,对静态库或动态库都是支持的。QQ在iOS端支持的最低操作系统版本是iOS 9,但是在UE4.26版本中,由于Metal版本的限制,最低只支持到iOS 12,使用静态库不能兼容,所以后面的工程便以动态库的方式集成到QQ里面了。
还有一点需要注意,UE内部有大量的Global Data(全局数据),包括各个模块的注册、Vertexfactory Shader Type(顶点工厂着色器类型)相关的内容,以及各种反射数据,全都是基于Global Data的这些数据进行链接的。在静态库中,这些Global Data默认是不会被链接进去的,需要把所有的Global Data都链接进去,否则会缺少部分运行时功能。
链接成动态库也有一些问题。比如,UE本身重载了New Delete 操作符,这个符号表一旦被导出去,就会引起宿主App的崩溃。再比如,宿主App新建了一个对象,这个时候UE的库还没有被加载进来,Global New和Global Delete还是原来默认的实现,但是一旦把UE的库加载进来,就相当于把原来的实现覆盖了,这时再去调用Delete就会进入UE的库。这是由于Clang底层的一个问题导致的,通过下面这个宏可修复—它可以确保在构建时不导出任何符号,从而提高库的集成性和可移植性: