在<<WebKit模块化分析>>中说到WebKit2中的多进程模型。多进程模型已经是浏览器的基本架构要素,下面展开分析一下WebKit2中的多进程模型。
协作决定接口,确立责任分工后,对于模块或系统间最重要的事莫过于接口定义,而且是有着简洁明确的定义。对于WebKit2中三个进程中的交互也是相当频繁和多样,如果使用传统的查表法对应解析执行,就会面临巨大的维护成本。WebKit2使用了Encoder和Decoder的概念的很好地将消息解析的工作放到各个功能上。于是提供的公共接口主要关注于消息的分发,而不再是似乎拥有一切的解析执行功能。
WebKit2在多进程接口上Process Launcher、CoreIPC以及WorkQueue构成了核心控制功能 (进程本身的Run Loop自然也是,但非此处的关注点),Encoder/Decoder/FunctionWrapper则是重要的工具类。下面就是对它们进行的学习和分析。
多线程模型的基本架构
先从基本结构看起。多进程模型是在WebKit中实现的,由WebKit中的三个Process分别处理应用主进程,网页处理进程及插件进程。
它们在具体的实现上是基于Process Launcher(WebKit2/UIProcess/Launcher)进行进程管理,再通过Core IPC进行跨进程通讯(IPC)。
CoreIPC做为一个公共,且由各个进程共享的模块,日后会被放置到WebCore或WTF中。放在WTF中较为合适,因为它在模块化的角色就是公共功能模块。
Process Launcher具体创建进程的代码需要平台适配,它使用了如下的方式:
ProcessLauncher.h和ProcessLauncher.cpp有通用的定义和公共代码的实现。再为不同平台建立不同的单元文件来实现平台差异的部分。负责创建新进程的launchProcess就在其中。
Core IPC也是类似的实现方式。在不同的平台下使用不同的IPC技术。
Mach Port是Mac OS下类似pipes技术的实现,而XPC则是新推出一种更为安全的IPC技术,目前国内相关的中文资料并不多,还是要花时研究官方文档(Creating
XPC Services)和示例代码(SandboxedFetch)。另外网友rainbird还推荐一个对XPC的封装。
多进程模型的交互机制
WebKit2中使用来Proxy来与其它进程进行通讯。
WebProcessProxy与PluginProcessProxy均会继承自ProcessLauncher::Client, 最后是通过CoreIPC::Connection来完成通讯。因为存在页面上的对应关系,所以消息传递时都会带一个Page ID来标识,这样在共享Web Process时就不会出现混淆的问题。
下图是一个进程通讯的架构, CoreIPC::Connection会通过与其它几个类一起完成进程间消息传递:
流程也很容易理解,消息发送者通过Encoder对要发送的信息进行编码,再由Work Queue控制触发发送或接收操作,最后由Decoder进行解码,最后交由目标类解析执行。
消息发送者Sender Class会继承自CoreIPC::MessageSender,而接收者Target Class会继承自CoreIPC::MessageReceiver。
Encoder的实现与平台无关。Work Queue自己会有队列管理机制触发发送操作,再由注册回调函数触发接收操作。无论发送和接收的函数都是由Connection通Function Wrapper提供的,Work Queue就是负责在适当的时机下触发。它在不同平台使用了不同的技术:
关于GCD可以参考一下这篇文章:
Windows下的Thread Pool的说明可以看这里:
下面Work Queue的类图:
.dispatch用于发送时,由Connection调用并传入负责发送的函数句柄。
.executeFunction则是用于执行指定的函数指针。
.invalidate则是用于清除当前排入的任务队列。
.platformXXX表示的是对应于各个平台的不同实现。其跨平台模式类似于ProcessLauncher。
.registerXXX与unregisterXXX用于注册和取消处理接收到新消息的回调函数,这个函数也是由Connection指定的( Mac OS下和Windows下的函数名不同)。
下面是Connection与WorkQueue交互示意的序列图:
多进程模型中消息数据设计
上面提到了消息在传递过程中外发的消息(outgoing message)和收到的消息(incoming message)都由CoreIPC::Connection类来管理,而且包含
一个编解码的过程。
1. 消息
每一个消息都有一个特定的定义。如下所示,Arguement1和Argument2是两个模板。
首先CoreIPC为不同数量参数的消息定义了一套模板,就是ArgumentX, X从1到8,就是最大表示8个参数的消息,模板数据类型指定的是各个参数的类型。
其中一部分使用了修饰模式。不同参数的模板都采用对前一个模板的实现来达到复用的目的。 这种定义方式和一般的查表法是不同的,目的在于消息本身就能表示自己,而不需要再做额外的对消息的映射。
下面是LoadURL消息的定义:
struct LoadURL : CoreIPC::Arguments2<constWTF::String&, constWebKit::SandboxExtension::Handle&>
{
static const Kind messageID = LoadURLID;
static CoreIPC::StringReference receiverName() { return messageReceiverName();
}
staticCoreIPC::StringReference name() { returnCoreIPC::StringReference("LoadURL");
}
staticconstbool isSync = false;
typedefCoreIPC::Arguments2<constWTF::String&, constWebKit::SandboxExtension::Handle&>
DecodeType;
LoadURL(const WTF::String& url, const WebKit::SandboxExtension::Handle&
sandboxExtensionHandle)
: CoreIPC::Arguments2<const WTF::String&, const WebKit::SandboxExtension::Handle&>(url,
sandboxExtensionHandle)
{
}
};
2. Encoder
所谓Encoder就是信息的各个参数组装到一个buffer中的操作。
Encode操作都是通过CoreIPC::MessageEncoder来完成的。它对Message的Message Name, Receiver Name, Destination ID(即Page ID)以及各个参数进行编码。
*encode方法有不同类型的副本。
*m_inlineBuffer是字符数组,会比m_buffer动态分配的方式效率要高。
下面Encode的结果(以loadURL为例):
实际发送时就是将这个buffer的内容发送出去。再由接收到使用对应的Decoder解析出来。
通过ReceiverName可以创建一个MessageReceiver对象,即此消息的处理对象。就这样,这个消息就确定应当由谁来处理了。
bool MessageReceiverMap::dispatchMessage(Connection* connection, MessageID messageID, MessageDecoder&
decoder)
{
if (MessageReceiver* messageReceiver = m_globalMessageReceivers.get(decoder.messageReceiverName()))
{
ASSERT(!decoder.destinationID());
messageReceiver->didReceiveMessage(connection, messageID, decoder);
return true;
}
……
}
和传统的处理方式相比,是不是简洁很多。虽然Recevier Names还是要有表存在,但相对常常的switch/case或查表法,它的变动性已经被极大的缩小了,也就降低了日后的维护成本。如果要接受消息处理,只要调用MessageReceiverMap::addMessageReceiver()添加一项Receiver,而参数就是Receiver的名称和类的实例。
再到具体的Message处理对象中处理,对于要接受消息处理的类,会继承自MessageReceiver,使得它们都有一个消息处理的入口。如:
voidWebPage::didReceiveWebPageMessage(CoreIPC::Connection*, CoreIPC::MessageID, CoreIPC::MessageDecoder& decoder)
{
……
if (decoder.messageName() == Messages::WebPage::LoadURL::name())
{
CoreIPC::handleMessage<Messages::WebPage::LoadURL>(decoder, this,
&WebPage::loadURL);
return;
}
……
}
最终通过callMemberFunction()来执行消息对应的函数,即上面代码指定的this和&WebPage::loadURL,decoder中则存储着参数。
3. Function Wrapper
Function Wrapper的功能就是将函数或者某个对象的成员封装成函数指针。在Work Queue与Connection的函数调用过程都是使用Function Wrapper来完成的。
Function Wrapper也是一个模板类,用于达到多态以支持不同的函数类型。比如下面支持类成员函数的定义(Functional.h):
template<typename R, typename C>
class FunctionWrapper<R (C::*)()> {
public:
typedef R ResultType;
static const bool shouldRefFirstParameter = HasRefAndDeref<C>::value;
static const bool shouldValidateFirstParameter = true;
explicit FunctionWrapper(R (C::*function)())
: m_function(function)
{
}
R operator()(C* c)
{
return (c->*m_function)();
}
private:
R (C::*m_function)();
};
Function Wrapper对Work Queue要封装就是Connection::sendOutgoingMessage和Connection::dispatchOneMessage()两个分别用于处理发送和收到的消息。
多进程模型的交互序列图示例
以下是一个交互的序列图,方便更好地理解它的设计。
在UI上加载页面时,UIProcess中的WebPageProxy向WebProcess中的WebPage发起loadURL请求。首先会通过调用Connection增加一个message到自己的outgoing message queue中。然后将自身的发送函数(sendOutgoingMessages)发送给WorkQueue对象执行,而不是直接把消息发送到WorkQueue中执行。所以WorkQueue只是一个控件类,而不是接口类。
WorkQueue会在适当的时机执行提交的任务,由executeFunction来执行相应的发送函数,将消息通过系统的机制发送出去。
而在WebProcess中,它会先在WorkQueue(不同的实例对象)中注册一个事件处理函数,当事件进来后,它就会被执行到。进行触发Connection对新的消息进行解析并加入到自己的incoming message queue中。
Web Process会在自己的Run Loop中要求Connection解析收到的消息,并在解析后调用对应的对象的处理函数。通过callMemberFunction()就最终调用到了WebPage::loadURL()函数。
转载请注明出处:http://blog.csdn.net/horkychen