转载自http://blog.csdn.net/qq295445028/article/details/7993069的一系列文章
http://blog.csdn.net/qq295445028/article/details/7993048
Chromium的技术优点,需要去分析下来内容:
1.它是如何利用多进程(其实也会有多线程一起)做并发的,又是如何解决多进程间的一些问题的,比如进程间通信,进程的开销;
2.做为一个后来者,它的扩展能力如何,如何去权衡对原有插件的兼容,提供怎么样的一个插件模型;
3.它的整体框架是怎样,有没有很NB的架构思想;
4.它如何实现跨平台的UI控件系统;
5.传说中的V8,为啥那么快
Chromium的设计文档
https://sites.google.com/a/chromium.org/dev/developers/design-documents
Chromium的多进程模式
https://sites.google.com/a/chromium.org/dev/developers/design-documents/multi-process-architecture
Chrome有一个主进程,称为Browser进程,管理Chrome大部分的日常事务;
有很多Renderer进程,它们圈地而治,各管理一组站点的显示和通信(Chrome在宣传中一直宣称一个tab对应一个进程,其实是很不确切的...),它们彼此互不搭理,只和老大说话,由老大负责权衡各方利益。它们和老大说话的渠道,称做IPC(Inter-Process Communication),这是Google搭的一套进程间通信的机制。
Chrome的进程模型
Google在宣传的时候一直都说,Chrome是one tab one
process的模式,其实,这只是为了宣传起来方便如是说而已,基本等同广告,实际疗效,还要从代码中来看。实际上,Chrome支持的进程模型远比宣传丰富,你可以参考一下这里,简单的说,Chrome支持以下几种进程模型:
ØProcess-per-site-instance:就是你打开一个网站,然后从这个网站链开的一系列网站都属于一个进程。这是Chrome的默认模式。
ØProcess-per-site:同域名范畴的网站放在一个进程,比如www.google.com和www.google.com/bookmarks就属于一个域名内(google有自己的判定机制),不论有没有互相打开的关系,都算作是一个进程中。用命令行--process-per-site开启。
ØProcess-per-tab:这个简单,一个tab一个process,不论各个tab的站点有无联系,就和宣传的那样。用--process-per-tab开启。
ØSingle Process:这个很熟悉了吧,传统浏览器的模式,没有多进程只有多线程,用--single-process开启。
大家可以用Shift+Esc观察各模式下进程状况,至少我是观察失败了(每种都和默认的一样...),原因待跟踪。。。
不论是Browser进程还是Renderer进程,都不只是光杆司令,它们都有一系列的线程为自己打理各种业务。对于Renderer进程,它们通常有两个线程,一个是Main thread,它负责与老大进行联系,有一些幕后黑手的意思;另一个是Render thread,它们负责页面的渲染和交互,一看就知道是这个帮派的门脸级人物。相比之下,Browser进程既然是老大,小弟自然要多一些,除了大脑般的Main thread,和负责与各Renderer帮派通信的IO thread,其实还包括负责管文件的file thread,负责管数据库的db thread等等
同一个进程内的线程,往往需要很多的协同工作,这一坨线程间的并发管理,是Chrome最出彩的地方之一了。
Chrome的线程模型走的是另一个路子,即,极力规避锁的存在。换更精确的描述方式来说,Chrome的线程模型,将锁限制了极小的范围内(仅仅在将Task放入消息队列的时候才存在),并且使得上层完全不需要关心锁的问题(当然,前提是遵循它的编程模型,将函数用Task封装并发送到合适的线程去执行),大大简化了开发的逻辑。
不过,从实现来说,Chrome的线程模型并没有什么神秘的地方,它用到了消息循环的手段。每一个Chrome的线程,入口函数都差不多,都是启动一个消息循环(参见MessagePump类),等待并执行任务。而其中,唯一的差别在于,根据线程处理事务类别的不同,所起的消息循环有所不同。比如处理进程间通信的线程(注意,在Chrome中,这类线程都叫做IO线程)启用的是MessagePumpForIO类,处理UI的线程用的是MessagePumpForUI类,一般的线程用到的是MessagePumpDefault类。不同的消息循环类,主要差异有两个,一是消息循环中需要处理什么样的消息和任务,第二个是循环流程(比如是死循环还是阻塞在某信号量上)。
Chrome中的Task
Chrome中的线程从实现层面来看没有任何区别,它的区别只存在于职责层面,不同职责的线程,会处理不同的Task
Task就是一个类,一个包含了void Run()抽象方法的类(参见Task类...)。一个真实的任务,可以派生Task类,并实现其Run方法。每个MessagePump类中,会有一个MessagePump::Delegate的类的对象(MessagePump::Delegate的一个实现,请参见MessageLoop类...),在这个对象中,会维护若干个Task的队列。当你期望,你的一个逻辑在某个线程内执行的时候,你可以派生一个Task,把你的逻辑封装在Run方法中,然后实例一个对象,调用期望线程中的PostTask方法,将该Task对象放入到其Task队列中去,等待执行。
在Chrome中,线程模型是统一且唯一的,这就相当于有了一套标准,它需要满足在各个线程上执行的几十上百种任务的需求,因此,必须在灵活性和易用性上有良好的表现,这就是设计标准的难度。为了满足这些需求,Chrome在底层库上做了足够的功夫:
它提供了一大套的模板封装(参见task.h),可以将Task摆脱继承结构、函数名、函数参数等限制(就是基于模板的伪function实现,想要更深入了解,建议直接看鼻祖《Modern C++》和它的Loki库);
同时派生出CancelableTask、ReleaseTask、DeleteTask等子类,提供更为良好的默认实现;
在消息循环中,按逻辑的不同,将Task又分成即时处理的Task、延时处理的Task、Idle时处理的Task,满足不同场景的需求;
Task派生自tracked_objects::Tracked,Tracked是为了实现多线程环境下的日志记录、统计等功能,使得Task天生就有良好的可调试性和可统计性;
在Chrome的多线程模型下,加锁这个事情只发生在将Task放入某线程的任务队列中,其他对任何数据的操作都不需要加锁。如果你熟悉设计模式,你会发现这是一个Command模式,将创建与执行的环境相分离,在一个线程中创建行为,在另一个线程中执行行为。Command模式的优点在于,将实现操作与构造操作解耦,这就避免了锁的问题,使得多线程与单线程编程模型统一起来,其次,Command还有一个优点,就是有利于命令的组合和扩展,在Chrome中,它有效统一了同步和异步处理的逻辑。。。
Command模式
Command模式,是一种看上去很酷的模式,传统的面向对象编程,我们封装的往往都是数据,在Command模式下,我们希望封装的是行为。这件事在函数式编程中很正常,封装一个函数作为参数,传来传去,稀疏平常的事儿;但在面向对象的编程中,我们需要通过继承、模板、函数指针等手法,才能将其实现。。。
应用Command模式,我们是期望这个行为能到一个不同于它出生的环境中去执行,简而言之,这是一种想生不想养的行为。我们做Undo/Redo的时候,会把在任一一个环境中创建的Command,放到一个队列环境中去,供统一的调度;在Chrome中,也是如此,我们在一个线程环境中创建了Task,却把它放到别的线程中去执行,这种寄居蟹似的生活方式,在很多场合都是有用武之地的。。。
在一般的多线程模型中,我们需要分清楚啥是同步啥是异步,在同步模式下,一切看上去和单线程没啥区别,但同时也丧失了多线程的优势(沦落成为多线程串行...)。而如果采用异步的模式,那写起来就麻烦多了,你需要注册回调,小心管理对象的生命周期,程序写出来是嗷嗷恶心。在Chrome的多线程模型下,同步和异步的编程模型区别就不复存在了,如果是这样一个场景:A线程需要B线程做一些事情,然后回到A线程继续做一些事情;在Chrome下你可以这样来做:生成一个Task,放到B线程的队列中,在该Task的Run方法最后,会生成另一个Task,这个Task会放回到A的线程队列,由A来执行。如此一来,同步异步,天下一统,都是Task传来传去,想不会,都难了。。。
最根本的缺陷,是锁和条件变量不支持模块化的编程。比如一个转账业务中,A账户扣了100元钱,B账户增加了100元,即使这两个动作单独用锁保护维持其正确性,你也不能将两个操作简单的串在一起完成一个转账操作,你必须让它们的锁都暴露出来,重新设计一番。好好的两个函数,愣是不能组在一起用,这就是锁的最大悲哀;
通过这些缺点的描述,也就可以明白Chrome多线程模型的优点。它解决了锁的最根本缺陷,即,支持模块化的编程,你只需要维护对象和线程之间的职能关系即可,这个摊子,比之锁的那个烂摊子,要简化了太多。对于程序员来说,负担一瞬间从泰山降成了鸿毛。。。
毕竟,在客户端,不会和服务器一样,存在超规模的并发处理任务,而只是需要尽可能的改善用户体验
Chrome的进程间通信
1.Chrome进程通信的基本模式
进程间通信,叫做IPC(Inter-Process Communication),在Chrome不多的文档中,有一篇就是介绍这个的,在这里。Chrome最主要有三类进程,
一类是Browser主进程,我们一直尊称它老人家为老大;
还有一类是各个Render进程,前面也提过了;
另外还有一类一直没说过,是Plugin进程,每一个插件,在Chrome中都是以进程的形式呈现,等到后面说插件的时候再提罢了。
Render进程和Plugin进程都与老大保持进程间的通信,Render进程与Plugin进程之间也有彼此联系的通路,唯独是多个Render进程或多个Plugin进程直接,没有互相联系的途径,全靠老大协调。
进程与进程间通信,需要仰仗操作系统的特性,能玩的花着实不多,在Chrome中,用到的就是有名管道(Named Pipe),只不过,它用一个IPC::Channel类,封装了具体的实现细节。Channel可以有两种工作模式,一种是Client,一种是Server,Server和Client分属两个进程,维系一个共同的管道名,Server负责创建该管道,Client会尝试连接该管道,然后双发往各自管道缓冲区中读写数据(在Chrome中,用的是二进制流,异步IO...),完成通信。。。
管道名字的协商。在Socket中,我们会事先约定好通信的端口,如果不按照这个端口进行访问,走错了门,会被直接乱棍打出门去的。与之类似,有名管道期望在两个进程间游走,就需要拿一个两个进程都能接受的进门暗号,这个就是有名管道的名字。在Chrome中(windows下...),有名管道的名字格式都是:\\.\pipe\chrome.ID。其中的ID,自然是要求独一无二,比如:进程ID.实例地址.随机数。通常,这个ID是由一个Process生成(往往是Browser
Process),然后在创建另一个进程的时候,作为命令行参数传进去,从而完成名字的协商。。。
Channel中,有三个比较关键的角色,
一个是Message::Sender,
一个是Channel::Listener,
最后一个是MessageLoopForIO::Watcher。
Channel本身派生自Sender和Watcher,身兼两角,而Listener是一个抽象类,具体由Channel的使用者来实现。顾名思义,Sender就是发送消息的接口,Listener就是处理接收到消息的具体实现,但这个Watcher是啥?如果你觉得Watcher这东西看上去很眼熟的话,我会激动的热泪盈眶的,没错,在前面(第一部分第一小节...)说消息循环的时候,从那个表中可以看到,IO线程(记住,在Chrome中,IO指的是网络IO,*_*)的循环会处理注册了的Watcher。其实Watcher很简单,可以视为一个信号量和一个带有OnObjectSignaled方法对象的对,当消息循环检测到信号量开启,它就会调用相应的OnObjectSignaled方法。。。
温柔的消息循环
其实,Chrome的很多消息循环,也不是都那么霸道,也是会被阻塞在某些信号量或者某种场景上的,毕竟客户端不是它家的服务器,CPU不能被全部归在它家名下。。。
比如IO线程,当没有消息来到,又没有信号量被激活的时候,就会被阻塞,具体实现可以去看MessagePumpForIO的WaitForWork方法。。。
不过这种阻塞是集中式的,可随时修改策略的,比起Channel直接阻塞在信号量上,停工的时间更短。。。
2.进程间的跨线程通信和同步通信
在Chrome中,任何底层的数据都是线程非安全的,在每一个进程中,只能有一个线程来负责操作Channel,这个线程叫做IO线程。但是有时候(其实是大部分时候...),我们需要从非IO线程与别的进程相通信,这该如何是好?如果,你有看过我前面写的线程模型,你一定可以想到,做法很简单,先将对Channel的操作放到Task中,将此Task放到IO线程队列里,让IO线程来处理即可。当然,由于这种事情发生的太频繁,每次都人肉做一次颇为繁琐,于是有一个代理类,叫做ChannelProxy,来帮助你完成这一切。。。
从接口上看,ChannelProxy的接口和Channel没有大的区别(否则就不叫Proxy了...),你可以像用Channel一样,用ChannelProxy来Send你的消息,ChannelProxy会辛勤的帮你完成剩余的封装Task等工作。不仅如此,ChannelProxy还青出于蓝胜于蓝,在这个层面上做了更多的事情,比如:发送同步消息。。。
不过能发送同步消息的类不是ChannelProxy,而是它的子类,SyncChannel。在Channel那里,所有的消息都是异步的(在Windows中,也叫Overlapped...),其本身也不支持同步逻辑。为了实现同步,SyncChannel并没有另造轮子,而只是在Channel的层面上加了一个等待操作。当ChannelProxy的Send操作返回后,SyncChannel会把自己阻塞在一组信号量上,等待回包,直到永远或超时。从外表上看同步和异步没有什么区别,但在使用上还是要小心,在UI线程中使用同步消息,是容易被发指的。。
3.Chrome中的IPC消息格式
说了半天,还有一个大头没有提过,那就是消息包。如果说,多线程模式下,对数据的访问开销来自于锁,那么在多进程模式下,大部分的额外开销都来自于进程间的消息拆装和传递。不论怎么样的模式,只要进程不同,消息的打包,序列化,反序列化,组包,都是不可避免的工作。。。
在Chrome中,IPC之间的通信消息,都是派生自IPC::Message类的。对于消息而言,序列化和反序列化是必须要支持的,Message的基类Pickle,就是干这个活的。Pickle提供了一组的接口,可以接受int,char,等等各种数据的输入,但是在Pickle内部,所有的一切都没有区别,都转化成了一坨二进制流。这个二进制流是32位齐位的,比如你只传了一个bool,也是最少占32位的,同时,Pickle的流是有自增逻辑的(就是说它会先开一个Buffer,如果满了的话,会加倍这个Buffer...),使其可以无限扩展。Pickle本身不维护任何二进制流逻辑上的信息,这个任务交到了上级处理(后面会有说到...),但Pickle会为二进制流添加一个头信息,这个里面会存放流的长度,Message在继承Pickle的时候,扩展了这个头的定义,其中,黄色部分是包头,定长96个bit,绿色部分是包体,二进制流,由payload_size指明长度。从大小上看这个包是很精简的了,除了routing位在消息不为路由消息的时候会有所浪费。消息本身在有名管道中是按照二进制流进行传输的(有名管道可以传输两种类型的字符流,分别是二进制流和消息流...),因此由payload_size
+ 96bits,就可以确定是否收了一个完整的包。。。
消息的序列化
前不久读了Google Protocol Buffers的源码,是用在服务器端,用做内部机器通信协议的标准、代码生成工具和框架。它主要的思想是揉合了key/value的内容到二进制中,帮助生成更为灵活可靠的二进制协议。。。
在Chrome中,没有使用这套东西,而是用到了纯二进制流作为消息序列化的方式。我想这是由于应用场景不同使然。在服务端,我们更关心协议的稳定性,可扩展性,并且,涉及到的协议种类很多。但在一个Chrome中,消息的格式很统一,这方面没有扩展性和灵活性的需求,而在序列化上,虽然key/value的方式很好很强大,但是在Chrome中需要的不是灵活性而是精简性,因此宁可不用Protocol Buffers造好的轮子,而是另立炉灶,花了好一把力气提供了一套纯二进制的消息机制。
从逻辑上来看,IPC消息分成两类,一类是路由消息(routed
message),还有一类是控制消息(control message)。路由消息是私密的有目的地的,系统会依照路由信息将消息安全的传递到目的地,不容它人窥视;控制消息就是一个广播消息,谁想听等能够听得到。。。
1.定义IPC消息
一个标准的IPC消息定义应该是类似于这样的,你需要从Message(或者其他子类)派生出一个子类,该子类有一个独一无二的ID值,该子类接受一个参数,你需要对这个参数进行序列化。两个麻烦的地方看的很清楚,如何生成独一无二的ID值?如何更方便的对任何参数可以自动的序列化?。。。
在Chrome中,解决这两个问题的答案,就是宏+模板。Chrome为每个消息安排了一种ID规格,用一个16bits的值来表示,高4位标识一个Channel,低12位标识一个消息的子id,也就是说,最多可以有16种Channel存在不同的进程之间,每一种Channel上可以定义4k的消息。目前,Chrome已经用掉了8种Channel(如果A、B进程需要双向通信,在Chrome中,这是两种不同的Channel,需要定义不同的消息,也就是说,一种双向的进程通信关系,需要耗费两个Channel种类...),他们已经觉得,16bits的ID格式不够用了,在将来的某一天,估计就被扩展成了32bits的。Chrome是这么来定义消息ID的,用一个枚举类,让它从高到低往下走,就像这样:
enum SomeChannel_MsgType
{
SomeChannelStart = 5 << 12,
SomeChannelPreStart = (5 << 12) - 1,
Msg1,
Msg2,
Msg3,
...
MsgN,
SomeChannelEnd
};
这是一个类型为5的Channel的消息ID声明,由于指明了最开始的两个值,所以后续枚举的值会依次递减,如此,只要维护Channel类型的唯一性,就可以维护所有消息ID的唯一性了(当然,前提是不能超过消息上限...)。
定义一个ID还不够,你还需要定义一个使用该消息ID的Message子类。这个步骤不但繁琐,最重要的,是违反了DIY原则,为了添加一个消息,你需要在两个地方开工干活,是可忍孰不可忍,于是Google祭出了宏这颗原子弹,需要定义消息,格式如下:
IPC_BEGIN_MESSAGES(PluginProcess, 3)
IPC_MESSAGE_CONTROL2(PluginProcessMsg_CreateChannel,
int /* process_id */,
HANDLE /* renderer handle */)
IPC_MESSAGE_CONTROL1(PluginProcessMsg_ShutdownResponse,
bool /* ok to shutdown */)
IPC_MESSAGE_CONTROL1(PluginProcessMsg_PluginMessage,
std::vector/* opaque data */)
IPC_MESSAGE_CONTROL0(PluginProcessMsg_BrowserShutdown)
IPC_END_MESSAGES(PluginProcess)
多次展开宏的技巧
这是Chrome中用到的一个技巧,定义一次宏,展开多段代码,我孤陋寡闻,第一次见,一个类似的例子,如下:
首先,定义一个macro.h,里面放置宏的定义:
#undef SUPER_MACRO
#if
defined(FIRST_TIME)
#undef FIRST_TIME
#define
SUPER_MACRO(label, type) \
enum IDs { \
label##__ID = 10 \
};
#elif
defined(SECOND_TIME)
#undef SECOND_TIME
#define
SUPER_MACRO(label, type) \
class TestClass \
{\ };
这是Chrome中,定义PluginProcess消息的宏,如果你想添加一条消息,只需要添加一条类似与IPC_MESSAGE_CONTROL0东东即可,这说明它是一个控制消息,参数为0个。你基本上可以这样理解,IPC_BEGIN_MESSAGES就相当于完成了一个枚举开始的声明,然后中间的每一条,都会在枚举里面增加一个ID,并声明一个子类。这个一宏两吃,可以参看ipc_message_macros.h,或者看下面一宏两吃的一个举例。。。
此外,当接收到消息后,你还需要处理消息。接收消息的函数,是IPC::Channel::Listener子类的OnMessageReceived函数。在这个函数中,会放置一坨的宏,
IPC_BEGIN_MESSAGE_MAP_EX(RenderProcessHost,
msg, msg_is_ok)
IPC_MESSAGE_HANDLER(ViewHostMsg_PageContents,OnPageContents)
IPC_MESSAGE_HANDLER(ViewHostMsg_UpdatedCacheStats,
OnUpdatedCacheStats)
IPC_MESSAGE_UNHANDLED_ERROR()
IPC_END_MESSAGE_MAP_EX()
这个东西很简单,展开后基本可以视为一个Switch循环,判断消息ID,然后将消息,传递给对应的函数。
通过宏的手段,可以解决消息类声明和消息的分发问题,但是自动的序列化还不能支持(所谓自动的序列化,就是不论你是什么类型的参数,几个参数,都可以直接序列化,不需要另写代码...)。在C++这种语言中,所谓自动的序列化,自动的类型识别,自动的XXX,往往都是通过模板来实现的。这些所谓的自动化,其实就是通过事前的大量人肉劳作,和模板自动递推来实现的,如果说.Net或Java中的自动序列化是过山轨道,这就是那挑夫的骄子,虽然最后都是两腿不动到了山顶,这底下费得力气真是天壤之别啊。具体实现技巧,有兴趣的看看《STL源码剖析》,或者是《C++新思维》,或者Chrome中的ipc_message_utils.h,这要说清楚实在不是一两句的事情。。。
总之通过宏和模板,你可以很简单的声明一个消息,这个消息可以传入各式各样的参数(这里用到了夸张的修辞手法,其实,只要是模板实现的自动化,永远都是有限制的,在Chrome的模板实现中,参数数量不要超过5个,类型需要是基本类型、STL容器等,在不BT的场合,应该够用了...),你可以调用Channel、ChannelProxy、SyncChannel之类的Send方法,将消息发送给其他进程,并且,实现一个Listener类,用Message Map来分发消息给对应的处理函数。如此,整个IPC体系搭建完成。。。
Chrome的进程模型
1.基本的进程结构
Chrome是一个多进程的架构,不过所有的进程都会由老大,Browser进程来管理,走的是集中化管理的路子。在Browser进程中,有xxxProcessHost,每一个host,都对应着一个Process,比如RenderProcessHost对应着RenderProcess,PluginProcessHost对应着PluginProcess,有多少个host的实例,就有多少个进程在运行。。。
这是一个比较典型的代理模式,Browser对Host的操作,都会被Host封装成IPC消息,传递给对应的Process来处理,对于大部分上层的类,也就隔离了多进程细节。。。
2.Render进程
前面说了,一个Process一个tab,只是广告用语,实际上,每一个web页面内容(包括在tab中的和在弹出窗口中的...),在Chrome中,用RenderView表示一个web页面,每一个RenderView可以寄宿在任一一个RenderProcess中,它只是依托RenderProcess帮助它进行通信。每一个RenderProcess进程都可以有1到N个RenderView实例。。。
Chrome支持不同的进程模型,可以一个tab一个进程,一个site instance一个进程等等。但基本模式都是一致的,当需要创建一个新的RenderView的时候,Chrome会尝试进行选择或者是创建进程。比如,在one site one
process的模式下,如果存在此site,就会选择一个已有的RenderProcessHost,让它管理这个新的RenderView,否则,会创建一个RenderProcessHost(同时也就创建了一个Process),把RenderView交给它。。。
在默认的one site instance one process的模式中,Chrome会为每个新的site instance创建一个进程(从一个页面链开来的页面,属于同一个site instance),但,Render进程总数是有个上限的。这个上限,根据内存大小的不同而异,比如,在我的机器上(2G内存),最多可以容纳20个Render进程,当达到这个上限后,你再开新的网站,Chrome会随机为你选择一个已有的进程,把这个网站对应的RenderView给扔进去。。。
Chrome并没有像我YY的一样做啥进程池之类的特殊机制,而是简单的履行有就创建、没有就销毁的策略。
3.进程开销控制算法
Chrome没有在进程创建和销毁上做功夫,但是当进程运行起来后,还是做了一些工作的。。。
节约工作首先从CPU耗时上做起,优先级越高的进程中的线程,越容易被调度,从而耗费CPU时间,于是,当一个页面不再直接面对用户的时候,Chrome会将它的进程优先级切到Below Normal的级别,反之,则切回Normal级别。通过这个步骤,小节约了一把时间。。。
当然这只是一道开胃小菜,满汉全席是控制进程的工作集大小,以达到降低进程实际内存消耗的目的(Chrome为了体现它对内存的节约,用了“更为精确”的内存消耗计算方法...)。提到这一点,Chrome颇为自豪,在文档中,顺着道把单进程的模式鄙视了一下,基本意思是:在多进程的模式下,各个页面实际占用的内存数量,更容易被控制,而在单进程的模式下,几乎是不能作出控制的,所以,很多时候,多进程模式耗费的内存,是会小于多线程模式的。这个说法靠不靠谱,大家心里都有谱,就不多说了。。。
具体说来,Chrome对进程工作集的控制算法还是比较简单的。首先,在进程启动的时候,需要指明进程工作的内存环境,是高内存,低内存,还是中等内存,默认模式下,是中等内存(我以为Chrome会动态计算的,没想到竟然是启动时指定...)。在高内存模式,不存在对工作集的调整,使劲用就完事了;在低内存的模式下,调整也很简单,一旦一个进程不再有页面面对观众了,尝试释放其所有工作集。相比来说,中等模式下,算法相对复杂一些,当一个进程从直接面对观众,沦落到切换到后台的悲惨命运,其工作集会缩减,算法为:TargetWorkingSetSize = (LastWorkingSet/2 + CurrentWorkingSet) /2;其中,TargetWorkingSetSize指的是预期降到的工作集大小,CurrentWorkingSet指的是进程当前的工作集(在Chrome中,工作集的大小,包含私有的和可共享的两部分内存,而不包含已经共享了的内存空间...),LastWorkingSet,等于上一次的CurrentWorkingSet除以DampingFactor,默认的DampingFactor为2。而反之,当一个进程从幕后走向台前,它的工作集会被放大为LastWorkingSet * DampingFactor * 2,了解过LastWorkingSet的含义,你已经知道,这就是将工作集放大两倍的另类版写法。。。
Chrome的Render进程工作集调整,除了发生在tab切换(或新页面建立)的时候,还会发生在整个Chrome的idle事件触发后。Chrome有个计时器,统计Chrome空闲的时长,当时长超过30s后(此工作会反复进行...),Chrome会做一系列工作,其中就包括,调整进程的工作集。被调整的进程,不仅仅是Render进程,还包括Plugin进程和Browser进程,换句话描述,就是所有Chrome进程。。。
Chrome的UI绘制
1.Chrome的窗口控件
Chrome提供了自己的一个UI控件库。Chrome的窗口、按钮、菜单之类的控件,都直接或间接派生自View,这个是控件基类。Chrome的View具有树形结构,其内部有一个子View数组,由此构成一个控件常用的组合模式。。。
有一个比较特殊的View子类,叫做RootView,顾名思义,它是整个View控件树的根,在Chrome中,一个正确的树形的控件结构,必须由RootView作为根。之所以要这样设计,是因为RootView有一个比较特殊的功能,那就是分发消息。。。
我们知道,一般的Windows控件,都有一个HWND,用与占据一块屏幕,捕获系统消息。Chrome中的View只是保存控件相关信息和绘制控件,里面没有HWND句柄,因此不能够捕获系统消息。在Chrome中,完整的控件架构是这样的,首先需要有一个ViewContainer,它里面包含一个RootView。ViewContainer是一个抽象类,在Window中的一个子类是HWNDViewContainer,同时,HWNDViewContainer还是MessageLoopForUI::Observer的子类。如果你看过本文第一部分描述的线程通信的内容的话,你就应该还记得,Observer是用于监听本线程内系统消息的东东。。。
当有系统消息进入此线程消息循环后,HWNDViewContainer会监听到这个情况,如果和View相关的消息,它就会调用RootView的相关方法,传递给控件。在RootView的内部,会遍历整个控件树上的控件,将消息传递给各个控件。当然,有的消息是可以独占的,比如鼠标移动发生在某个View所管辖的范围内,它会告知RootView(通过方法的返回值...),这个消息我要了,那么RootView会停止遍历。。。
在设计的时候,View对消息的处理,采取的是大而全的接口模式。就是说在View内部,提供了所有可能的消息处理接口,并提供了默认实现,所有子类只需要覆盖自己需要的消息处理函数即可。
每一个View的子类控件,比如Button之类的,会存储一些数据,根据消息做一些行为,并且绘制出自己。在Chrome中,画图的东西是ChromeCanvas这个类,在其内部,通过Skia和GDI实现绘制。Skia是Android团队开发的一个跨平台的图形引擎,在Chrome中负责除了文字之外,所有内容的绘制;而文字绘制的重担,在Windows中交到了GDI的手上。这样的设计会给跨平台带来一些困难,估计是由Skia实现文本绘制会比较繁琐,才会带出如此一个设计的模式。
2.Chrome的页面加载和绘制
上面这些UI控件,都是用在窗口上的(比如浏览器的外框,菜单,对话框之类的...)。我们在浏览器中看到的大部分内容,是网页页面。页面的绘制(绘制,就是把一个HTML文件变成一个活灵活现的页面展示的过程...),只有一半轮子是Chrome自己做的,还有一部分来自于WebKit,这个Apple打造的Web渲染器。。。
之所以说是一半轮子来源于WebKit,是因为WebKit本身包含两部分主要内容,一部分是做Html渲染的,另一部分是做JavaScript解析的。在Chrome中,只有Html的渲染采用了WebKit的代码,而在JavaScript上,重新搭建了一个NB哄哄的V8引擎。目标是,用WebKit + V8的强强联手,打造一款上网冲浪的法拉利,从效果来看,还着实做的不错。。。
不过,虽说Chrome和WebKit都是开源的,并联手工作。但是,Chrome还是刻意的和WebKit保持了距离,为其始乱终弃埋下了伏笔。Chrome在WebKit上封装了一层,称为WebKit Glue。Glue层中,大部分类型的结构和接口都和WebKit类似,Chrome中依托WebKit的组件,都只是调用WebKit Glue层的接口,而不是直接调用WebKit中的类型。按照Chrome自己文档中的话来说,就是,虽然我们再用WebKit实现页面的渲染,但通过这个设计(加一个间接层...)已经从某种程度大大降低了与WebKit的耦合,使得可以很容易将WebKit换成某个未来可能出现的更好的渲染引擎。。。
当你键入一个Url并敲下回车后,Chrome会在Browser进程中下载Url对应的页面资源(包括Web页面和Cookie),而不是直接将Url发送给Render进程让它们自行下载(你会越来越发现,Render进程绝对是100%的名符其实,除了绘制,几乎啥多余的事情都不会干的...)。与各个Render进程各自为战,各自管好自己所需的资源相比,这种策略仿佛会增加大量的进程间通信。之所以采用,主要有三个优点,一个是避免子进程与网络通信,从而将网络通信的权限牢牢握在主进程手中,Render进程能力弱了,想造反干坏事的可能性就降低了(可以更好控制各个Render进程的权限...);另一个是有利于Cookie等持久化资源在不同页面中的共享,否则在不同Render进程中传递Cookie这样的事情,做起来更麻烦;还有一点很重要的,是可以控制与网络建立HTTP连接的数量,以Browser为代表与网络各方进行通信,各种优化策略都比较好开展(比如池化)。。。
当然,在Browser进程中进行统一的资源管理,也就意味着不再方便用WebKit进行资源下载(WebKit当然有此能力,不过再次被Chrome抛弃了...),而是依托WinHTTP来做的。WinHTTP在接受数据的过程中,会不停的把数据和相关的消息通过IPC,发送给负责绘制此页面的Render进程中对应的RenderView。在这里,路由消息中的那个ID值起了关键的作用,系统依照此ID,能够准确的将相关的消息发送到相关的View头上,这玩意发错了地方还真不是和有人把钱错到你账户上一样,因为错收的进程基本上无福消受这个意外来客,轻者页面显示混乱,重者消化不良直接噎死。。。
RenderView接收到页面信息,会一边绘制一边等待更多的资源到来,在用户看来,所请求的页面正在一点一点显示出来。当然,如果是一个通知传输开始、传输结束这样的消息,通过序列化到消息参数里面,经由IPC发过来,代价还是可以承受的,但是,想资源内容这样大段大段的字节流,如果通过消息发过来,浪费两边进程大量空间和时间,就不合适了。于是这里用到了共享内存。Browser进程将下载到的资源写到共享内存中,并将共享内存的句柄和共享区域的大小序列化在消息中发送给Render进程。Render进程拿到这个句柄,就可以通过它访问到共享内存相关的区域,读取信息并进行绘制。通过这样的方式,即享用到了统一资源管理的优点,由避免了很高的进程通信开销,左右逢源,好不快活。。。
3.Chrome页面的消息响应
Render进程是一个娇生惯养的进程,这一点从上面一段已经可以看出来了。它自己的资源它自己都不下载,而是由Browser进程来帮忙。不过Render进程也许比你想象的还要懒惰一些,它不但不自己下载资源,甚至,连自己的系统消息都不接收。。。
Render进程中不包含HWND,当你鼠标在页面上划来划去,点上点下,这些消息其实都发到了Browser进程,它们拥有页面呈现部分的HWND。Browser会将这些消息转手通过IPC发送给对应的Render进程中的RenderView,很多时候WebKit会处理此类消息,当它发现出现了某种值得告诉Browser进程的事情,它会组个包回赠给Browser进程。举个例子,你打开一个页面,然后拿鼠标在页面上乱晃。Browser这时候就像一个碎嘴大婶,不厌其烦的告诉Render进程,“鼠标动了,鼠标动了”。如果Render对这个信息无所谓,就会很无聊的应答着:“哦,哦”(发送一个回包...)。但是,当鼠标划过链接的时候,矜持的Render进程坐不住了,会大声告诉Browser进程:“换鼠标,换鼠标~~”,Browser听到后,会将鼠标从箭头状换成手指状,然后继续以上过程。。。
比较麻烦的是Paint消息,重新绘制页面是一个太频繁发生的事情,不可能重绘一次就序列化一坨字节流过去。于是策略也很清楚了,就是依然用共享内存读写,用消息发句柄。在Render进程中,会有一个共享内存池(默认值为2...),以size为key,以共享内存为值,简单的先入先出淘汰算法,利用局部性的特征,避免反复的创建和销毁共享内存(这和资源传递不一样,因为资源传递可以开一块固定大小的共享内存...)。Render进程从共享内存池中拿起一块(二维字节数组...),就好像拿着一块屏幕似的,拼了命往上绘制,为了让Render安心觉着有成就感,Browser会偷偷帮Render把这些内容绘制到屏幕上,造成Render进程直接绘制屏幕的假象。这可就苦了屏幕取词的工具们,因为在HWND上压根就没啥字符信息,全部就是一坨图像而已,啥也取不着。于是Google金山词霸,网易有道词霸各自发挥智慧,另辟蹊径,也算是都利用Chrome做了一把广告。。。
为什么不让Render进程自己拥有HWND,自己管理自己的消息,既快捷又便利。在Chrome的官方Blog上,有一篇文章基本上是这个意思,速度是必须快的发指的,但是为了用户响应,放弃一些速度是必要的,毕竟,没有人喜欢总假死的浏览器。在Browser进程中,基本上是杜绝任何同步Render进程的工作,所有操作都是异步完成。因为Render进程是不靠谱的,随时可能牺牲掉,同步它们往往导致主进程停止响应,从而导致整个浏览器停下来甚至挂掉,这个代价是不可以容忍的。但是,Windows有一个恶习,喜欢往整个HWND继承体系中发送同步消息(我不是很清楚这个状况,有人能解释么?...),这时候,如果HWND在Render进程中,就务必会导致主进程与Render进程的同步,Chrome无法控制Windows,于是,它们只能够控制Render,把它们的HWND搬到主进程中,避免同步操作,换取用户响应的速度。。。
4.结论
整个Chrome的UI架构,就是一个权责分配的问题。可以把Browser进程看成是一个类似于朱元璋般的勤劳皇帝(详见《明朝那些事一》...),把大多数的权利都牢牢把握在手中,这样,虽然Browser很操劳,但是整体上的协调和同步,都进行的非常顺畅。Render进程就是皇帝手下的傀儡宰相们,只负责自己的一亩三分地,听从皇帝的调配即可。这这样的环境下,Render进程的生死变得无足轻重,Render的死亡,只是少了一个绘制页面的工具而已,其他一切如故。通过控制权力,换取天下太平,这招在coding界,同样是一个不错的策略,但是,唯一的意外来自于Plugin。按照规范,Chrome的Plugin是可以创立窗口的(HWND),这必然导致同步问题,Chrome没有办法通过控制权力的方式解决这个问题,只能想些别的亡羊补牢的招来搞定。。。
Chrome的插件模型
1. NPAPI
为了紧密的与各个开源浏览器团结起来,共同抗击IE的垄断,Chrome的插件,也遵循了NPAPI(Netscape
Plugin Application Programming Interface)标准,支持这个标准的浏览器需要实现一组规定的API供插件调用,这组API形如NPN_XXX,比如NPN_GetURL,插件可以利用这些API进行二次开发。而NPAPI插件以一个Dll之类的作为物理载体(windows下dll,linux下是so...)进行提供,里面同样也实现了一组规定的API。形式包括NP_XXX和NPP_XXX,NP_XXX是系统需要默认调用的方法,用于认知这个插件,比如NP_Initialize,而NPP_XXX是用于插件完成一些实际功能,比如NPP_New。。。
所有的插件dll都需要放置在指定目录下(根据操作系统的不同而不同...),每个插件可以处理一种或多种MIME格式的数据,比如application/pdf,说明该插件可以处理pdf相关的文档。在Chrome中键入about:plugins,可以查看当前Chrome中具有的插件信息。。。
NPAPI是一个很经典的插件方案,用dll进行注入,用协定的API进行通信,用字符串描述插件能力。插件宿主(在这里就是浏览器...),会根据能力描述,动态加载插件,并负责插件调用的流程和生命周期管理。而插件中,负责真实逻辑的处理,并可以构造UI与用户交流。以此类方式实现的插件系统,往往是处理的逻辑比较固定适用范围一般(用API写死了逻辑...),但可扩展性不错(用字符串描述能力,可无限扩展...)。。。
在Chrome中nphostapi.h中,定义了所有NPAPI相关的函数指针和结构,这个文件放置在glue目录下,如果看过前面碰过的文章就知道,在WebKit内肯定也有一套相同的东西;在npapi.h/.cc中,提供了Chrome浏览器端的NPN_XXX系列函数的实现;每一个插件物理实例,用PluginLib类来表示,而每一个插件的逻辑实例,用PluginInstance类来表示。这个概念牵强附会的可以用windows中的句柄来类比,当你想操作一个内核对象,你需要获得一个内核对象的句柄,每个进程中的句柄肯定不相同,但后面的内核对象却是同一个,内核对象的生命周期通过句柄的计数来控制,有人用则活,无人用则死(当然这个类比相当的牵强,主要是想说明引用计数和逻辑与物理的关系,但一个关键性的区别在于,PluginLib与PluginInstance都是在一个进程内的,不能跨越进程边界...)。在Chrome中,PluginLib负责加载和销毁一个dll,拿到所有导出函数的函数指针,PluginInstance对这些东西进行了封装,可以更好的来调用。。。
关于NPAPI的更多细节,Chrome并没有提供任何文档,但是,各个先驱的浏览器们都提供了大量丰富的文档。比如,你可以到这里,查看firefox中的NPAPI文档,基本通用。。。
2. Chrome的多进程插件模型
Chrome的插件模型,与早先的浏览器的最大不同,是它采用了多进程的方式,每一个插件,都有一个单独的进程来承载(Shift + Esc打开Chrome进程管理器,可以看到现在已经加载的插件进程...)。当WebKit进行页面渲染的时候,发现了未知的MIME类型数据,它会告知给Browser进程,召唤它提供一个插件来解析。如果该插件还未加载,Browser会在指定目录中搜寻出具有此实力的插件(如果没有此类人才只能作罢...),并为它创建一个进程,让它负责所有的该插件相关的任务,然后建立起一个IPC通路,与它“保持通话”。这套流程一定不会太陌生,因为它与Render进程的创建大同小异换汤不换药。。。
Plugin进程与Render进程最大的区别在于,Render需要与Browser进程大量通信,因为它的HWND归Browser老大掌管着,相关所有内容都需要通信完成。但Plugin不需要与Browser频繁联系,它大部分的通信都是与Render进程发生的。如果Plugin与Render之间的通信,还需要走Browser中转一下,这就显得有些脱裤子放屁了,虽然Browser是大头,但不是冤大头,它不会干这种吃力不讨好的事情。他只是做了一回Render与Plugin间的媒婆而已。当Plugin与Browser建立好了IPC通路后,它会让Render建立一个新IPC通路,用以与Plugin通信,IPC的有名管道名,经由Browser通知给Plugin。完成名字协商后,Render与Plugin的通信关系就建立好了,它们之间就可以直接进行通信了。。。
整个通信模式,可以看这里。这是一个很标准的代理模式的应用,稍有了解的都可以跳过我后面会做的一段罗嗦的描述,一看官方文档中的图便能知晓。在Render进程端,WebPluginImpl是WebPlugin的一个子类,WebPlugin是供Webkit进行调用的一个接口,利用依赖倒置,实现了扩展。在Plugin进程端,实现了一个WebPluginDelegateImpl类,该类会调用PluginInstance的相关接口实现真实的插件功能。这样的话,只需要WebPluginImpl调用WebPluginDelegateImpl中的相应方法,就可以实现功能。但问题是WebPluginImpl与WebPluginDelegateImpl天各一方各处于一个进程,很显然,这里需要一个代理模式。这里沿用了COM的架构,Delegate + Stub + Proxy。WebPluginImpl调用代理WebPluginDelegateProxy,该代理会将调用转换成消息,通过IPC发送给Plugin进程,在Plugin端,通过WebPluginDelegateStub监听消息,并转换成对真实WebPluginDelegateImpl的调用,从而完成了跨进程的一个调用,反之亦然。。。