Urho3D 1.7.1 源代码分析(一)
1. 概述
Urho3D依据功能划分成若干子系统,如:
- Graphics 包装与显示编程的接口(OpenGL或者DirectX,这里只以OpenGL为例说明)。
- UI 包括与用户交互的组件。
- Renderer 包括与建模对象的组件。
- Input 负责键盘、鼠标设备输入。
- Audio 负责声音。
- ResourceCache 负责Image、XML文件等资源加载。
- FileSystem 包装文件系统接口。
- Log 包装日志记录接口。
最重要的三个子系统是Graphics、UI和Renderer。
Context是各子系统运行的上下文环境。子系统都必须向它注册,通过这种方式,Context将各子系统粘合在一起,使它们能互相发现、互相发送通知(事件)、互相调用。这就如同设计模式中的中介者(Mediator)模式。
Engine也是一个子系统。没有将它列在上述的列表中,是因为它其实是其他子系统的组织者,由它负责创建和驱动其他子系统。Engine也要向Context注册,以便与其他子系统通信。
Application是应用程序类。 使用者应该从它派生自己的类。这里的HelloWorld是Urho3D的HelloWorld例子中定义的派生类。

Object类的成员context_是对Context的引用。实际上系统只有唯一的Context实例,构造Object对象时,将context_指向这个实例。需要与其他子系统的类都从Object派生,以便与其他子系统交互。

2. Object注册与创建
2.1 Object Factory
所有可以从Context创建的对象,除了从Object派生,还得有自己的ObjectFactory类,也就是ObjectFactoryImpl<T>。Context的成员factories_保存了对象类工厂的实例。
TypeInfo保存类的标识信息,也就是成员type_,这是一个根据类名称生成的全局唯一的Hash值。TypeInfo还保存指向父类的链接,也就是成员baseTypeInfo_。

每个类都要包括宏URHO3D_OBJECT(typename, baseTypeName)。 如下是宏展开后的部分代码。其中的函数GetTypeStatic()可以获得类的StringHash值,这个值是GetTypeInfoStatic()中定义的一个TypeInfo类型的静态变量typeInfoStatic。
static Urho3D::StringHash GetTypeStatic()
{
return GetTypeInfoStatic()->GetType();
}
static const Urho3D::TypeInfo* GetTypeInfoStatic()
{
static const Urho3D::TypeInfo typeInfoStatic(#typeName, BaseClassName::GetTypeInfoStatic());
return &typeInfoStatic;
}
2.2 Object注册
Context可创建的对象类应该提供静态函数RegisterObject()。

RegisterObject()的工作如下:
-
调用Context::RegisterFactory<T>创建对象工厂实例,并向Context注册,也就是保存在Context::factories_中。
-
调用Context的一组属性设置函数,属性保存在Context的成员attributes_中,这是一个从类的StringHash值到其属性数组的映射。
- AttributeInfo是属性类。成员type_是数据类型,name_是属性名称,offset_是属性在对象中的偏移,defaultValue_是缺省值,accessor_是属性访问函数。

-
这些属性可能的来源是:
- 调用URHO3D_COPY_BASE_ATTRIBUTES()从基类复制。
- 调用URHO3D_UPDATE_ATTRIBUTE_DEFAULT_VALUE()更新已有的属性。
- 调用URHO3D_ACCESSOR_ATTRIBUTE()注册新的属性。
2.3 Object创建
Context::CreateObject<T>()的工作如下:
- 调用T::GetTypeStatic()得到类的StringHash值
- 用该值调用Context::CreateObject()。它查找Context::factories_,找到对象工厂实例,然后调用ObjectFactoryImpl<T>::CreateObject()创建对象。

3. 事件处理机制
3.1 Event与EventHandler
每个event有自己唯一的StringHash值。EventNameRegistrar::RegiseterName()根据event的名字生成这个值。EventNameRegistrar的成员eventNames_保存了所有从event的StringHash值到名字的映射。

Object在成员eventHandlers_中保存自己的事件处理器EventHandler。

EventHandler抽象了事件处理器的接口。它的成员eventType_是event的StringHash值,成员receiver_是event的接收函数。
EventHandlerImpl<T>实现EventHandler。在EventHandleImpl<T>中,一般将receiver_设置成模板参数类T的成员函数,这样事件处理请求就被重定向了。
Context的成员eventReceivers_保存了从event的StringHash值到一组Object的映射,这组Object是该事件的接收者。

3.2 SubscribeToEvent()
SubscribeToEvent()负责订阅事件,如下的例子调用SubscribeToEvent()订阅E_SCREENMODE事件。
Object::SubscribeToEvent(E_SCREENMODE, URHO3D_HANDLER(Renderer, HandleScreenMode));
E_SCREENMODE是一个代表event type的StringHash值。它由URHO3D_EVENT()宏定义如下:
URHO3D_EVENT(E_SCREENMODE, ScreenMode)
{
URHO3D_PARAM(P_WIDTH, Width);
}
以上宏定义展开如下:
static const Urho3D::StringHash
E_SCREENMODE(Urho3D::EventNameRegistrar::RegisterEventName(“ScreenMode”));
namespace ScreenMode
{
static const Urho3D::StringHash P_WIDTH(“Width”);
}
RegisterEventName()从event名字”ScreenMode”计算出StringHash值。E_SCREENMODE被设置成该值。该事件还带参数P_WIDTH。
URHO3D_HANDLER()创建一个Event handler的实例。
#define URHO3D_HANDLER(className, function) \
(new Urho3D::EventHandlerImpl<className>(this, &className::function))
展开URHO3D_HANDLER(Renderer, HandleScreenMode) 的结果如下:
new Urho3D::EventHandlerImpl<Renderer>(this, &Renderer::HandleScreenMode)
EventHandlerImpl<Renderer>保存Renderer的实例,和event处理函数Render::HandlerScreenMode()。这样收到该event时,就调用EventHandlerImpl::Invoke()处理。
以上的步骤如下图所示。
- 创建事件处理器,也就是EventHandleImpl<Renderer>。
- 在Object::SubscriberToEvent()中,
- 这个Object是接收者。调用Object::FindSpecificEvent()检查它的成员eventHandlers_是否已有该event的处理器,如果有则替换,否则新增一个。
- 如果是新增,则还需要调用Context::AddEventReceiver()将调用者加入Context的成员eventReceiverGroup_中。

3.3 SendEvent()
SendEvent()负责发送事件。如下的例子发送E_SCREENMODE事件。
SendEvent(E_SCREENMODE, eventData);
如下是SendEvent()的工作。
-
调用Context::GetEventReceiver(),在成员eventReceiverGroup_中,找到订阅该事件的Object实例。然后遍历所有Object,调用Object::OnEvent()。
-
Object::OnEvent()在eventHandlers_中找到该事件对应的处理函数,也就是EventHandler的实例。调用EventHandler::Invoke(),进而调用EventHandlerImpl<T>指定模板类T的成员函数。这里是模板类Renderer的成员函数HandleScreenMode()。
可以看出,这是一个从Context 到Object的两级派发机制。

Object的成员eventHanders_允许为同一个事件指定两个处理函数,一个绑定了特定的事件发送对象,一个不绑定。前一个优先级更高。在Object::OnEvent()中,会先查找前一个。
4. 工作队列
4.1 WorkThread 与 WorkQueue
Thread是封装线程的类。WorkThread是Thread的派生类,它专门配合工作队列WorkQueue使用。WorkQueue的成员threads_保存了一组WorkThread实例。
WorkItem是工作队列中的任务,成员workFunction_是任务的回调函数。WorkQueue的成员queue_保存了当前要执行的任务列表。

WorkQueue::CreateThreads()创建WorkThread实例,并调用Thread::Run()创建实际的线程,线程句柄保存在Thread的成员handle_中。

线程的执行函数是ThreadFunctionStatic()。它调用Thread的虚拟函数ThreadFunction(),Thread的派生类应实现这个函数。

WorkThread的成员owner_指向它所属的WorkQueue实例。WorkerThread的ThreadFunction()调用这个实例的ProcessItems()。后者持续从成员queue_取出任务并执行(也就是取出WorkItem,并调用WorkItem::workFunction_)。
WorkQueue的多个线程可以并行地执行queue_中的任务。从queue_中取出任务时,使用Mutex作互斥。
除了成员queue_之外,WorkQueue的成员poolItems_保存空闲的WorkItem实例,成员workItems_保存正在使用中的WorkItem实例。使用workItems_和queue_两个列表保存工作队列,是为了尽量减低互斥对访问效率的影响。
4.2 使用WorkQueue
WorkQueue的一般的使用过程如下。
这里使用OcclusionBuffer:DrawTrianles()作为例子,说明WorkQueue的一般使用过程。
Octree的成员batches_是一组OcclusionBatch实例。Octree需要在一组工作线程中对它们执行OcclusionBatch::DrawBatch(),并等待所有任务完成。

-
遍历batches_,为每个OcclusionBatch实例增加WorkItem。
- 调用WorkQueue::GetFreeItem()得到空闲的WorkItem实例。优先从成员poolItems_中取,如果没有,则创建一个新实例。
- 设置WorkItem实例。这里设置成员workFunction_指向全局函数DrawOcclusionBatchWork(),这个函数将调用OcclusionBatch::DrawBatch()。OcclusionBatch实例作为成员start_传给全局函数。
- 调用WorkQueue::AddWorkItem()加入新WorkItem实例。先加入成员workItems_,再加入queue_中。WorkItem有执行优先级,保存在成员priority_中。添加到queue_时比较优先级,保证优先级高的优先执行。
-
将WorkItem加入queue_中,工作线程就从queue_中取出任务并执行。WorkItem的成员completed_表示任务状态,任务完成状态改成true。
-
调用WorkQueue::Complete()等待指定优先级以上的任务完成,并做清理工作。
- 首先调用IsCompleted(),遍历成员workItems_中的元素,检查指定优先级以上的任务是否完成。这里需要反复调用IsCompleted()确认,直到所有任务完成。
-
调用PurgeComplete(),遍历workItems_,将状态为完成的元素挪出,调用ReturnToPool(),放回到poolItems_中。
-
最后的问题是,任务少的时候如何回收WorkItem实例。 WorkQueue::HandleBeginFrame()中调用PurgePool()做这件事。PurgePool()记住每次调用时poolItems_的大小,如果前后两次相差值超过某一个阈值,则将多的WorkItem回收。

相关链接
Urho3D 1.7.1 源代码分析 (一)
Urho3D 1.7.1 源代码分析 (二)
Urho3D 1.7.1 源代码分析 (三)
Urho3D 1.7.1 源代码分析 (四)
Urho3D 1.7.1 源代码分析 (五)