代码改变世界

深入研究虚幻4反射系统实现原理(三)

2016-08-07 19:31  风恋残雪  阅读(5458)  评论(1编辑  收藏  举报

前面两篇文章介绍了虚幻引擎中对于反射的支持(如果没读过前两篇文章,推荐你仔细看下,否则你可能不知道我在讲什么),不过还差一点内容,就是这些信息是如何 加入到运行时的,讲完这些那么这个系列也就算是基本结束了,下面进入正文。

信息注册

虚幻引擎使用一系列静态变量来注册需要生成反射信息的函数,这个我们前面的文章已经比较详细的讲过。至于用生成的C++的代码所带来的好处,我前面翻译的文章中也讲过。下面我把它贴到这里。

用生成的C++代码来存储反射数据的一个最大好处就是,它可以保证跟二进制做到同步。你永远也不会加载陈旧或者过时的反射数据,因为它是跟引擎的其它代码同时编译的,并且它会在程序启动的时候使用C++表达式来计算成员偏移等,而不是通过针对特定平台/编译器/优化的组合中进行逆向工程。UHT作为一个单独的不使用任何生成头文件的程序来构建,因此它也避免了鸡生蛋、蛋生鸡的问题,这个在虚幻3的脚本编译器中一直被诟病。

UCLASS

对于类的反射支持,虚幻4分为两步来做的。

  1. IMPLEMENT_CLASS() 这个宏用于在程序启动时注册这个类,包括生成UClass类以及注册C++原生函数等操作。
  2. static FCompiledInDefer 创建一个静态变量,用于向DeferredCompiledInRegistration这个静态数组中添加注册函数,来初始化默认的反射属性。包括函数、成员变量、元数据等。

USTRUCT

对于结构体的支持,虚幻4引擎也是分为两步来做的。

  1. static FCompiledInDeferStruct 存储一个用于构建结构体的一个单例函数,用于在程序启动的时候调用,读者可以自行查看代码就知道这个过程了。
  2. 还会创建一个静态对象,这个对象在构造函数中会调用UScriptStruct::DeferCppStructOps,它用来向这个DeferredCppStructOps map 中注册一个动态管理结构体构造、析构的一个类。

UENUM

枚举比较简单,只有一步。

  1. static FCompiledInDeferEnum 创建一个静态变量,用于在程序启动时存储一个创建枚举反射对象的一个单例函数。

启动过程分析

上面我们讲解了这个注册信息的过程,而它们的执行是伴随着当前模块的加载而执行的,我们都知道静态变量的初始化是先于Main函数执行的。下面我们简单画了一下虚幻编辑器的启动流程,这样我们就可以准确地看到整个注册反射信息的过程了。

image

可以看到void ProcessNewlyLoadedUObjects()这个函数就是我们主要关注的函数,我们前面讲到的注册的信息,包括类、结构体以及枚举类型的反射信息都会在这里进行注册,它的代码如下所示:

void ProcessNewlyLoadedUObjects()
{
	DECLARE_SCOPE_CYCLE_COUNTER(TEXT("ProcessNewlyLoadedUObjects"), STAT_ProcessNewlyLoadedUObjects, STATGROUP_ObjectVerbose);

#if WITH_HOT_RELOAD
	UClassGenerateCDODuplicatesForHotReload();
#endif
	UClassRegisterAllCompiledInClasses();

	while( AnyNewlyLoadedUObjects() )
	{
		UObjectProcessRegistrants();
		UObjectLoadAllCompiledInStructs();
		UObjectLoadAllCompiledInDefaultProperties();		
	}
#if WITH_HOT_RELOAD
	UClassReplaceHotReloadClasses();
#endif
}

下面我们对上面代码做一个简单的解释,读者也可以自行翻阅代码来仔细查看它是怎么实现的。

  1. 代码中WITH_HOT_RELOAD这个宏是用来处理C++代码热加载使用的。
  2. UClassRegisterAllCompiledInClasses()用来注册所有要加载的类,这里面的所有类就是通过前面IMPLEMENT_CLASS()宏添加进来的。
  3. UObjectProcessRegistrants()用于处理自动注册的对象,并把它们添加到ObjectArray中去,用于后期的检索。
  4. UObjectLoadAllCompiledInStructs()用于注册结构体和枚举的反射信息。数组里面的数组是通过FCompiledInDeferStruct和FCompiledInDeferEnum创建的静态对象注册进去的。
  5. UObjectLoadAllCompiledInDefaultProperties()用于注册类的反射信息并且创建一个默认对象(CDO)。

总结

到此为此,这个系列的3篇文章通过代表示例的方式向读者展示了虚幻中反射系统的实现方式,原理其实很简单,网上也有一些如何用C++支持反射类的方法,虚幻实现的是其中一种,因为有了UHT的帮助,所以好多脏活、累活都不用我们来做了,比如最笨的方法是我们自己实现一些宏用来注册各种反射信息,但这样效率还是比较低。而有的实现方式会从编译器产生的调试信息入手,比如vs生成的pdb文件,或者clang生成的文件等,这样做也可以,但是它有一个比较大的问题就是跨平台的支持不是特别友好。所以虚幻4中这一套UHT工具总体上来说还是很不错的,如果后面有时间,希望给大家带来对UHT工具的分析。对了,如果你有什么想对虚幻4引擎比较想了解的,也欢迎在下面留言,我也会挑大家比较感兴趣的模块来先做分析。接下来,我可能会主要把精力放在虚幻中蓝图的实现原理和基于物理的渲染具体实现上。敬请期待!