Chrome源码剖析


Chrome 源码剖析 作者:Venus 神庙 博文:http://flyvenus.net/wordpress/?tag=chrome 序 开源是口好东西,它让这个充斥着大量工业垃圾代码和教材玩具代码的行业,多了 一些艺术气息和美的潜质。它使得每个人,无论你来自米国纽约还是中国铁岭,都 有机会站在巨人的肩膀上,如果不能,至少也可以抱一把大腿。。。 现在我就是来抱大腿的,这条粗腿隶属于 Chrome(开源项目名称其实是 Chromium,本 来 Chrome 这个名字就够晦涩了,没想到它的本名还更上一层楼...), Google 那充满狼子野心的浏览器。每一个含着金勺子出生的人都免不了被仰慕并 被唾骂,Chrome 也不例外。关于 Chrome 的优劣好坏讨论的太多了,基本已经被 嚼成甘蔗渣了,没有人愿意再多张一口了。俗话说,内行看门道外行看热闹,大部 分所谓的外行,是通过使用的真实感受来评定优劣的,这无疑是最好的方式。但偏 偏还是有自诩的内行,喜欢说内行话办外行事,一看到 Chrome 用到多进程就说垃 圾废物肯定低能。拜托,大家都是搞技术的,你知道多进程的缺点,Google 也知 道,他们不是政客,除了搞个噱头扯个蛋就一无所知了,人家也是有脸有皮的,写 一坨屎一样的开源代码放出来遭世人耻笑难道会很开心?所谓技术的优劣,是不能 一概而论的,同样的技术在不同场合不同环境不同代码实现下,效果是有所不同的。 既然 Chrome 用了很多看上去不是很美的技术,我们是不是也需要了解一下它为什 么要用,怎么用的,然后再开口说话?(恕不邀请,请自行对号入座...)。。。 人说是骡子是马拉出来遛遛,Google 已经把 Chrome 这匹驴子拉到了世人面前, 大家可以随意的遛。我们一直自诩是搞科学的,就是在努力和所谓的艺术家拉开, 人搞超女评委的,可以随意塞着屁眼用嘴放屁,楞把李天王说是李天后,你也只能 说他是艺术品位独特。你要搞科学就不行,说的不对,轻的叫无知,重的叫学术欺 诈,结果一片惨淡。所以,既然代码都有了,再说话,就只能当点心注点意了,先 看,再说。。。 我已经开始遛 Chrome 这头驴了,确切一点,是头壮硕的肥驴,项目总大小接近 2G。这样的庞然大物要从头到脚每个毛孔的大量一遍,那估计不咽气也要吐血的, 咱又不是做 Code review,不需要如此拼命。每一个好的开源项目,都像是一个美 女,这世界没有十全十美的美女,自然也不会有样样杰出的开源项目。每个美女都 有那么一两点让你最心动不已或者倍感神秘的,你会把大部分的注意力都放在上面 细细品味,看开源,也是一样。Chrome 对我来说,有吸引力的地方在于(排名分 先后...): 1. 它是如何利用多进程(其实也会有多线程一起)做并发的,又是如何解决多 进程间的一些问题的,比如进程间通信,进程的开销; 2. 做为一个后来者,它的扩展能力如何,如何去权衡对原有插件的兼容,提供 怎么样的一个插件模型; 3. 它的整体框架是怎样,有没有很 NB 的架构思想; 4. 它如何实现跨平台的 UI 控件系统; 5. 传说中的 V8,为啥那么快。 但 Chrome 是一个跨平台的浏览器,其 Linux 和 Mac 版本正在开发过程中,所以 我把所有的眼光都放在了 windows 版本中,所有的代码剖析都是基于 windows 版本的。话说,我本是浏览器新手、win api 白痴以及并发处理的火星人,为了我 的好奇投身到这个溜驴的行业中来,难免有学的不到位看的走眼的时候,各位看官 手下超生,有错误请指正,实在看不下去,回家自己牵着遛吧。。。 扯淡实在是个体力活,所以后面我会少扯淡多说问题。。。 关于 Chrome 的源码下载和环境配置,大家看 这里(windows 版本),只想强调 一点,一定要严格按照说明来配置环境,特别是 vs2005 的补丁和 windows SDK 的安装,否则肯定是编译不过的。。。 最后,写这部分唯一不是废话的内容,请记住以下这幅图,这是 Chrome 最精华的 一个缩影,如果你还有空,一定要去 这里 进行阅读,其中重中之重是 这一篇 。。。 图 1 Chrome 的线程和进程模型 一、 Chrome 的多线程模型 0. Chrome 的并发模型 如果你仔细看了前面的图,对 Chrome 的线程和进程框架应该有了个基本的了解。 Chrome 有一个主进程,称为 Browser 进程,它是老大,管理 Chrome 大部分的 日常事务;其次,会有很多 Renderer 进程,它们圈地而治,各管理一组站点的显 示和通信(Chrome 在宣传中一直宣称一个 tab 对应一个进程,其实是很不确切的...), 它们彼此互不搭理,只和老大说话,由老大负责权衡各方利益。它们和老大说话的 渠道,称做 IPC(Inter-Process Communication),这是 Google 搭的一套进程 间通信的机制,基本的实现后面自会分解。。。 Chrome 的进程模型 Google 在宣传的时候一直都说,Chrome 是 one tab one process 的模式,其实, 这只是为了宣传起来方便如是说而已,基本等同广告,实际疗效,还要从代码中来 看。实际上,Chrome 支持的进程模型远比宣传丰富,你可以参考一下这里 ,简单 的说,Chrome 支持以下几种进程模型: 1. Process-per-site-instance:就是你打开一个网站,然后从这个网站链开的 一系列网站都属于一个进程。这是 Chrome 的默认模式。 2. Process-per-site:同域名范畴的网站放在一个进程,比如 www.google.com 和 www.google.com/bookmarks 就属于一个域名内 (google 有自己的判定机制),不论有没有互相打开的关系,都算作是一 个进程中。用命令行--process-per-site 开启。 3. Process-per-tab:这个简单,一个 tab 一个 process,不论各个 tab 的站 点有无联系,就和宣传的那样。用--process-per-tab 开启。 4. Single Process:这个很熟悉了吧,传统浏览器的模式,没有多进程只有多 线程,用--single-process 开启。 关于各种模式的优缺点,官方有官方的说法,大家自己也会有自己的评述。不论如 何,至少可以说明,Google 不是由于白痴而采取多进程的策略,而是实验出来的 效果。。。 大家可以用 Shift+Esc 观察各模式下进程状况,至少我是观察失败了(每种都和默 认的一样...),原因待跟踪。。。 不论是 Browser 进程还是 Renderer 进程,都不只是光杆司令,它们都有一系列的 线程为自己打理各种业务。对于 Renderer 进程,它们通常有两个线程,一个是 Main thread,它负责与老大进行联系,有一些幕后黑手的意思;另一个是 Render thread, 它们负责页面的渲染和交互,一看就知道是这个帮派的门脸级人物。相比之下, Browser 进程既然是老大,小弟自然要多一些,除了大脑般的 Main thread,和负 责与各 Renderer 帮派通信的 IO thread,其实还包括负责管文件的 file thread, 负责管数据库的 db thread 等等(一个更详细的列表,参见 这里 ),它们各尽其 责,齐心协力为老大打拼。它们和各 Renderer 进程的之间的关系不一样,同一个 进程内的线程,往往需要很多的协同工作,这一坨线程间的并发管理,是 Chrome 最出彩的地方之一了。。。 闲话并发 单进程单线程的编程是最惬意的事情,所看即所得,一维的思考即可。但程序员的 世界总是没有那么美好,在很多的场合,我们都需要有多线程、多进程、多机器携 起手来一齐上阵共同完成某项任务,统称:并发(非官方版定义...)。在我看来, 需要并发的场合主要是要两类: 1. 为了更好的用户体验。有的事情处理起来太慢,比如数据库读写、远程通信、 复杂计算等等,如果在一个线程一个进程里面来做,往往会影响用户感受, 因此需要另开一个线程或进程转到后台进行处理。它之所以能够生效,仰仗 的是单 CPU 的分时机制,或者是多 CPU 协同工作。在单 CPU 的条件下, 两个任务分成两拨完成的总时间,是大于两个任务轮流完成的,但是由于彼 此交错,更人的感觉更为的自然一些。 2. 为了加速完成某项工作。大名鼎鼎的 Map/Reduce,做的就是这样的事情, 它将一个大的任务,拆分成若干个小的任务,分配个若干个进程去完成,各 自收工后,在汇集在一起,更快的得到最后的结果。为了达到这个目的,只 有在多 CPU 的情形下才有可能,在单 CPU 的场合(单机单 CPU...),是无 法实现的。 在第二种场合下,我们会自然而然的关注数据的分离,从而很好的利用上多 CPU 的 能力;而在第一种场合,我们习惯了单 CPU 的模式,往往不注重数据与行为的对应 关系,导致在多 CPU 的场景下,性能不升反降。。。 1. Chrome 的线程模型 仔细回忆一下我们大部分时候是怎么来用线程的,在我足够贫瘠的多线程经历中, 往往都是这样用的:起一个线程,传入一个特定的入口函数,看一下这个函数是否 是有副作用的(Side Effect),如果有,并且还会涉及到多线程的数据访问,仔细 排查,在可疑地点上锁伺候。。。 Chrome 的线程模型走的是另一个路子,即,极力规避锁的存在。换更精确的描述 方式来说,Chrome 的线程模型,将锁限制了极小的范围内(仅仅在将 Task 放入 消息队列的时候才存在...),并且使得上层完全不需要关心锁的问题(当然,前提 是遵循它的编程模型,将函数用 Task 封装并发送到合适的线程去执行...),大大简 化了开发的逻辑。。。 不过,从实现来说,Chrome 的线程模型并没有什么神秘的地方(美女嘛,都是穿 衣服比不穿衣服更有盼头...),它用到了消息循环的手段。每一个 Chrome 的线程, 入口函数都差不多,都是启动一个消息循环(参见 MessagePump 类),等待并 执行任务。而其中,唯一的差别在于,根据线程处理事务类别的不同,所起的消息 循环有所不同。比如处理进程间通信的线程(注意,在 Chrome 中,这类线程都叫 做 IO 线程,估计是当初设计的时候谁的脑门子拍错了...)启用的是 MessagePumpForIO 类 ,处 理 UI 的线程用的是 MessagePumpForUI 类 ,一 般 的线程用到的是 MessagePumpDefault 类(只讨论 windows, windows, windows...)。不同的消息循环类,主要差异有两个,一是消息循环中需要处理什 么样的消息和任务,第二个是循环流程(比如是死循环还是阻塞在某信号量上...)。 下图是一个完整版的 Chrome 消息循环图,包含处理 Windows 的消息,处理各种 Task(Task 是什么,稍后揭晓,敬请期待...),处理各个信号量观察者(Watcher), 然后阻塞在某个信号量上等待唤醒。。。 图 2 Chrome 的消息循环 当然,不是每一个消息循环类都需要跑那么一大圈的,有些线程,它不会涉及到那 么多的事情和逻辑,白白浪费体力和时间,实在是不可饶恕的。因此,在实现中, 不同的 MessagePump 类,实现是有所不同的,详见下表: MessagePumpDefault MessagePumpForIO MessagePumpForUI 是否需要处理系统消息 否 是 是 是否需要处理 Task 是 是 是 是否需要处理 Watcher 否 是 否 是否阻塞在信号量上 否 是 是 2. Chrome 中的 Task 从上面的表不难看出,不论是哪一种消息循环,必须处理的,就是 Task(暂且遗忘 掉系统消息的处理和 Watcher,以后,我们会缅怀它们的...)。刨去其它东西的干 扰,只留下 Task 的话,我们可以这样认为:Chrome 中的线程从实现层面来看没 有任何区别,它的区别只存在于职责层面,不同职责的线程,会处理不同的 Task。 最后,在铺天盖地西红柿来临之前,我说一下啥是 Task。。。 简单的看,Task 就是一个类,一个包含了 void Run()抽象方法的类(参见 Task 类...)。 一个真实的任务,可以派生 Task 类,并实现其 Run 方法。每个 MessagePump 类 中,会有一个 MessagePump::Delegate 的类的对象(MessagePump::Delegate 的一个实现,请参见 MessageLoop 类...),在这个对象中,会维护若干个 Task 的 队列。当你期望,你的一个逻辑在某个线程内执行的时候,你可以派生一个 Task, 把你的逻辑封装在 Run 方法中,然后实例一个对象,调用期望线程中的 PostTask 方法,将该 Task 对象放入到其 Task 队列中去,等待执行。我知道很多人已经抄起 了板砖,因为这种手法实在是太常见了,就不是一个简单的依赖倒置,在线程池, Undo\Redo 等模块的实现中,用的太多了。。。 但,我想说的是,虽说谁家过年都是吃顿饺子,这饺子好不好吃还是得看手艺,不 能一概而论。在 Chrome 中,线程模型是统一且唯一的,这就相当于有了一套标准, 它需要满足在各个线程上执行的几十上百种任务的需求,因此,必须在灵活行和易 用性上有良好的表现,这就是设计标准的难度 。为了满足这些需求,Chrome 在底 层库上做了足够的功夫: 1. 它提供了一大套的模板封装(参见 task.h),可以将 Task 摆脱继承结构、 函数名、函数参数等限制(就是基于模板的伪 function 实现,想要更深入了 解,建议直接看鼻祖《Modern C++》和它的 Loki 库...); 2. 同时派生出 CancelableTask、ReleaseTask、DeleteTask 等子类,提供更 为良好的默认实现; 3. 在消息循环中,按逻辑的不同,将 Task 又分成即时处理的 Task、延时处理 的 Task、Idle 时处理的 Task,满足不同场景的需求; 4. Task 派生自 tracked_objects::Tracked,Tracked 是为了实现多线程环境下 的日志记录、统计等功能,使得 Task 天生就有良好的可调试性和可统计性; 这一套七荤八素的都搭建完,这才算是一个完整的 Task 模型,由此可知,这饺子, 做的还是很费功夫的。。。 3. Chrome 的多线程模型 工欲善其事,必先利其器。Chrome 之所以费了老鼻子劲去磨底层框架这把刀,就 是为了面对多线程这坨怪兽的时候杀的更顺畅一些。在 Chrome 的多线程模型下, 加锁这个事情只发生在将 Task 放入某线程的任务队列中,其他对任何数据的操作 都不需要加锁。当然,天下没有免费的午餐,为了合理传递 Task,你需要了解每一 个数据对象所管辖的线程,不过这个事情,与纷繁的加锁相比,真是小儿科了不知 道多少倍。。。 图 3 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 传来传去,想不会,都难了。。。 图 4 Chrome 的一种异步执行的解决方案 4. Chrome 多线程模型的优缺点 一直在说 Chrome 在规避锁的问题,那到底锁是哪里不好,犯了何等滔天罪责,落 得如此人见人嫌恨不得先杀而后快的境地。《代码之美》的第二十四章“美丽的并 发”中,Haskell 设计人之一的 Simon Peyton Jones 总结了一下用锁的困难之处, 我罚抄一遍,如下: 1. 锁少加了,导致两个线程同时修改一个变量; 2. 锁多加了,轻则妨碍并发,重则导致死锁; 3. 锁加错了,由于锁和需要锁的数据之间的联系,只存在于程序员的大脑中, 这种事情太容易发生了; 4. 加锁的顺序错了,维护锁的顺序是一件困难而又容易出错的问题; 5. 错误恢复; 6. 忘记唤醒和错误的重试; 7. 而最根本的缺陷,是锁和条件变量不支持模块化的编程。比如一个转账业务 中,A 账户扣了 100 元钱,B 账户增加了 100 元,即使这两个动作单独用锁 保护维持其正确性,你也不能将两个操作简单的串在一起完成一个转账操作, 你必须让它们的锁都暴露出来,重新设计一番。好好的两个函数,愣是不能 组在一起用,这就是锁的最大悲哀; 通过这些缺点的描述,也就可以明白 Chrome 多线程模型的优点。它解决了锁的最 根本缺陷,即,支持模块化的编程,你只需要维护对象和线程之间的职能关系即可, 这个摊子,比之锁的那个烂摊子,要简化了太多。对于程序员来说,负担一瞬间从 泰山降成了鸿毛。。。 而 Chrome 多线程模型的一个主要难点,在于线程与数据关系的设计上,你需要良 好的划分各个线程的职责,如果有一个线程所管辖的数据,几乎占据了大半部分的 Task,那么它就会从多线程沦为单线程,Task 队列的锁也将成为一个大大的瓶颈。。。 设计者的职责 一个底层结构设计是否成功,这个设计者是否称职,我一直觉得是有一个很简单的 衡量标准的。你不需要看这个设计人用了多少 NB 的技术,你只需要关心,他的设 计,是否给其他开发人员带来了困难。一个 NB 的设计,是将所有困难都集中在底 层搞定,把其他开发人员换成白痴都可以工作的那种;一个 SB 的设计,是自己弄 了半天,只是为了给其他开发人员一个长达 250 条的注意事项,然后很 NB 的说, 你们按照这个手册去开发,就不会有问题了。。。 从根本上来说,Chrome 的线程模型解决的是并发中的用户体验问题而不是联合工 作的问题(参见我前面喷的“闲话并发”),它不是和 Map/Reduce 那样将关注 点放在数据和执行步骤的拆分上,而是放在线程和数据的对应关系上,这是和浏览 器的工作环境相匹配的。设计总是和所处的环境相互依赖的,毕竟,在客户端,不 会和服务器一样,存在超规模的并发处理任务,而只是需要尽可能的改善用户体验, 从这个角度来说,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),然后在创建另一个进程的时候,作为 命令行参数传进去,从而完成名字的协商。。。 如果不了解并期待了解有关 Windows 下有名管道和信号量的知识,建议去看一些 专业的书籍,比如圣经级别的《Windows 核心编程》和《深入解析 Windows 操作 系统》,当然也可以去查看 SDK,你需要了解的 API 可能包括:CreateNamedPipe, CreateFile, ConnectNamedPipe, WaitForMultipleObjects, WaitForSingleObject, SetEvent, 等等。。。 Channel 中,有三个比较关键的角色,一个是 Message::Sender,一个是 Channel::Listener,最后一个是 MessageLoopForIO::Watcher。Channel 本身 派生自 Sender 和 Watcher,身兼两角,而 Listener 是一个抽象类,具体由 Channel 的使用者来实现。顾名思义,Sender 就是发送消息的接口,Listener 就是处理接收 到消息的具体实现,但这个 Watcher 是啥?如果你觉得 Watcher 这东西看上去很 眼熟的话,我会激动的热泪盈眶的,没错,在前面(第一部分第一小节...)说消息 循环的时候,从那个表中可以看到,IO 线程(记住,在 Chrome 中,IO 指的是网 络 IO,*_*)的循环会处理注册了的 Watcher。其实 Watcher 很简单,可以视为一 个信号量和一个带有 OnObjectSignaled 方法对象的对,当消息循环检测到信号量 开启,它就会调用相应的 OnObjectSignaled 方法。。。 图 5 Chrome 的 IPC 处理流程图 一图解千语,如上图所示,整个 Chrome 最核心的 IPC 流程都在图上了,期间,刨 去了一些错误处理等逻辑,如果想看原汁原味的,可以自查 Channel 类的实现。当 有消息被 Send 到一个发送进程的 Channel 的时候,Channel 会把它放在发送消息 队列中,如果此时还正在发送以前的消息(发送端被阻塞...),则看一下阻塞是否 解除(用一个等待 0 秒的信号量等待函数...),然后将消息队列中的内容序列化并 写道管道中去。操作系统会维护异步模式下管道的这一组信号量,当消息从发送进 程缓冲区写到接收进程的缓冲区后,会激活接收端的信号量。当接收进程的消息循 环,循到了检查 Watcher 这一步,并发现有信号量激活了,就会调用该 Watcher 相应的 OnObjectSignaled 方法,通知接受进程的 Channel,有消息来了!Channel 会尝试从管道中收字节,组消息,并调用 Listener 来解析该消息。。。 从上面的描述不难看出,Chrome 的进程通信,最核心的特点,就是利用消息循环 来检查信号量,而不是直接让管道阻塞在某信号量上。这样就与其多线程模型紧密 联系在了一起,用一种统一的模式来解决问题。并且,由于是消息循环统一检查, 线程不会随便就被阻塞了,可以更好的处理各种其他工作,从理论上讲,这是通过 增加 CPU 工作时间,来换取更好的体验,颇有资本家的派头。。。 温柔的消息循环 其实,Chrome 的很多消息循环,也不是都那么霸道,也是会被阻塞在某些信号量 或者某种场景上的,毕竟客户端不是它家的服务器,CPU 不能被全部归在它家名 下。。。 比如 IO 线程,当没有消息来到,又没有信号量被激活的时候,就会被阻塞,具体实 现可以去看 MessagePumpForIO 的 WaitForWork 方法。。。 不过这种阻塞是集中式的,可随时修改策略的,比起 Channel 直接阻塞在信号量上, 停工的时间更短。。。 2. 进程间的跨线程通信和同步通信 在 Chrome 中,任何底层的数据都是线程非安全的,Channel 不是太上老君(抑或 中国足球?...),它也没有例外。在每一个进程中,只能有一个线程来负责操作 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 的时候,扩展了这个头 的定义,完整的消息格式如下: 图 6 Chrome 的 IPC 消息格式 其中,黄色部分是包头,定长 96 个 bit,绿色部分是包体,二进制流,由 payload_size 指明长度。从大小上看这个包是很精简的了,除了 routing 位在消息不为路由消息 的时候会有所浪费。消息本身在有名管道中是按照二进制流进行传输的(有名管道 可以传输两种类型的字符流,分别是二进制流和消息流...),因 此 由 payload_size + 96bits,就可以确定是否收了一个完整的包。。。 从逻辑上来看,IPC 消息分成两类,一类是路由消息(routed message),还有一 类是控制消息(control message)。路由消息是私密的有目的地的,系统会依照 路由信息将消息安全的传递到目的地,不容它人窥视;控制消息就是一个广播消息, 谁想听等能够听得到。。。 消息的序列化 前不久读了 Google Protocol Buffers 的源码,是用在服务器端,用做内部机器通 信协议的标准、代码生成工具和框架。它主要的思想是揉合了 key/value 的内容到 二进制中,帮助生成更为灵活可靠的二进制协议。。。 在 Chrome 中,没有使用这套东西,而是用到了纯二进制流作为消息序列化的方式。 我想这是由于应用场景不同使然。在服务端,我们更关心协议的稳定性,可扩展性, 并且,涉及到的协议种类很多。但在一个 Chrome 中,消息的格式很统一,这方面 没有扩展性和灵活性的需求,而在序列化上,虽然 key/value 的方式很好很强大, 但是在 Chrome 中需要的不是灵活性而是精简性,因此宁可不用 Protocol Buffers 造好的轮子,而是另立炉灶,花了好一把力气提供了一套纯二进制的消息机制。。。 4. 定义 IPC 消息 如果你写过 MFC 程序,对 MFC 那里面一大堆宏有所忌惮的话,那么很不幸,在 Chrome 中的 IPC 消息定义中,你需要再吃一点苦头了,甚至,更苦大仇深一些; 如果你曾经领教过用模板的特化偏特化做 Traits、用模板做函数重载、用编译期的 Tuple 做变参数支持,之类机制的种种麻烦的话,那么,同样很遗憾,在 Chrome 中,你需要再感受一次。。。 不过,先让我们忘记宏和模板,看人肉一个消息,到底需要哪些操作。一个标准的 IPC 消息定义应该是类似于这样的: class SomeMessage : public IPC::Message { public: enum { ID = ...; } SomeMessage(SomeType & data) : IPC::Message(MSG_ROUTING_CONTROL, ID, ToString(data)) {...} ... }; 大概意思是这样的,你需要从 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 中,定义 PluginProcess 消息的宏,我挖过来放在这了,如果你想添 加一条消息,只需要添加一条类似与 IPC_MESSAGE_CONTROL0 东东即可,这说 明它是一个控制消息,参数为0个。你基本上可以这样理解,IPC_BEGIN_MESSAGES 就相当于完成了一个枚举开始的声明,然后中间的每一条,都会在枚举里面增加一 个 ID,并声明一个子类。这个一宏两吃,直逼北京烤鸭两吃的高超做法,可以参看 ipc_message_macros.h,或者看下面一宏两吃的一个举例。。。 多次展开宏的技巧 这是 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 \ { \ public: \ enum {ID = label##__ID}; \ TestClass(type value) : _value(value) {} \ type _value; \ }; #endif 可以看到,这个头文件是可重入的,每一次先 undef 掉之前的定义,然后判断进行 新的定义。然后,你可以创建一个 use_macro.h 文件,利用这个宏,定义具体内容: #include "macros.h" SUPER_MACRO(Test, int) 这个头文件在利用宏的部分不需要放到 ifundef...define...这样的头文件保护中,目 的就是为了可重入。在主函数中,你可以多次 define + include,实现多次展开的 目的: #define FIRST_TIME #include "use_macro.h" #define SECOND_TIME #include "use_macro.h" #include int _tmain(int argc, _TCHAR* argv[]) { TestClass t(5); std::cout << TestClass::ID << std::endl; std::cout << t._value << std::endl; return 0; } 这样,你就成功的实现,一次定义,生成多段代码了。。。 此外,当接收到消息后,你还需要处理消息。接收消息的函数,是 IPC::Channel::Listener 子类的 OnMessageReceived 函数。在这个函数中,会放 置一坨的宏,这一套宏,一定能让你想起 MFC 的 Message Map 机制: 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,然后将消 息,传递给对应的函数。与 MFC 的 Message Map 比起来,做的事情少多了。。。 通过宏的手段,可以解决消息类声明和消息的分发问题,但是自动的序列化还不能 支持(所谓自动的序列化,就是不论你是什么类型的参数,几个参数,都可以直接 序列化,不需要另写代码...)。在 C++这种语言中,所谓自动的序列化,自动的类 型识别,自动的 XXX,往往都是通过模板来实现的。这些所谓的自动化,其实就是 通过事前的大量人肉劳作,和模板自动递推来实现的,如果说.Net 或 Java 中的自 动序列化是过山轨道,这就是那挑夫的骄子,虽然最后都是两腿不动到了山顶,这 底下费得力气真是天壤之别啊。具体实现技巧,有兴趣的看看《STL 源码剖析》, 或者是《C++新思维》,或者 Chrome 中的 ipc_message_utils.h,这要说清楚实 在不是一两句的事情。。。 总之通过宏和模板,你可以很简单的声明一个消息,这个消息可以传入各式各样的 参数(这里用到了夸张的修辞手法,其实,只要是模板实现的自动化,永远都是有 限制的,在 Chrome 的模板实现中,参数数量不要超过 5 个,类型需要是基本类型、 STL容器等,在不BT的场合,应该够用了...),你可以调用Channel、ChannelProxy、 SyncChannel 之类的 Send 方法,将消息发送给其他进程,并且,实现一个 Listener 类,用 Message Map 来分发消息给对应的处理函数。如此,整个 IPC 体系搭建完 成。。。 苦力的宏和模板 不论是宏还是模板,为了实现这套机制,都需要写大量的类似代码,比如为了支持 0~N 个参数的 Control 消息,你就需要写 N+1 个类似的宏;为了支持各种基础数 据结构的序列化,你就需要写上十来个类似的 Write 函数和 Traits。。。 之所以做如此苦力的活,都是为了用这些东西的人能够尽可能的简单方便,符合 DIY 原则。规约到之前说的设计者的职责上来,这是一个典型的苦了我一个幸福千万人 的负责任的行为。在 Chrome 中,如此的代码随处可见,光 Tuple 那一套拳法,我 现在就看到了使了不下三次(我曾经做过一套,直接吐血...),如此兢兢业业,真 是可歌可泣啊。。。 三、 Chrome 的进程模型 1. 基本的进程结构 Chrome 是一个多进程的架构,不过所有的进程都会由老大,Browser 进程来管理, 走的是集中化管理的路子。在 Browser 进程中,有 xxxProcessHost,每一个 host, 都对应着一个 Process,比如 RenderProcessHost 对应着 RenderProcess, PluginProcessHost 对应着 PluginProcess,有多少个 host 的实例,就有多少个进 程在运行。。。 这是一个比较典型的代理模式,Browser 对 Host 的操作,都会被 Host 封装成 IPC 消息,传递给对应的 Process 来处理,对于大部分上层的类,也就隔离了多进程细 节。。。 2. Render 进程 先不扯 Plugin 的进程,只考虑 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 给扔进去。。。 每一次你新输入一个站点信息,在默认模式下,都必然导致一个进程的诞生,很可 能,伴随着另一个进程的死亡(如果这个进程没有其他承载的 RenderView 的话, 他就自然死亡了,RenderView 的个数,就相当于这个进程的引用计数...)。比 如 , 你打开一个新标签页的时候,系统为你创造了一个进程来承载这个新标签页,你输 入 www.baidu.com,于是新标签页进程死亡,承载 www.baidu.com 的进程诞生。 你用 baidu 搜索了一下,毫无疑问,你基本对它的搜索结果很失望,于是你重新输 入 www.google.com,老的承载 baidu 的进程死亡,承载 google 的进程被构建 出来。这时候你想回退到之前 baidu 的搜索结果,乐呵乐呵的话,一个新的承载 baidu 的进程被创造,之前 Google 的进程死亡。同样,你再次点击前进,又来到 Google 搜索结果的时候,一个新的进程有取代老的进程出现了。。。 以上现象,你都可以自己来检验,通过观察 about:memory 页面的信息,你可以了 解整个过程(记得每做一步,需要刷新一下 about:memory 页面)。我唧唧歪歪说 了半天,其实想表达的是,Chrome 并没有像我 YY 的一样做啥进程池之类的特殊 机制,而是简单的履行有就创建、没有就销毁的策略。我并不知道有没有啥很有效 的多进程模型,这方面一点都没玩过,猜测 Chrome 之所以采取这样的策略,是经 过琢磨的,觉得进程生死的代价可以承受,比较可行。。。 3. 进程开销控制算法 说开销无外乎两方面的内容,一为时间,二则空间。Chrome 没有在进程创建和销 毁上做功夫,但是当进程运行起来后,还是做了一些工作的。。。 节约工作首先从 CPU 耗时上做起,优先级越高的进程中的线程,越容易被调度,从 而耗费 CPU 时间,于是,当一个页面不再直接面对用户的时候,Chrome 会将它的 进程优先级切到 Below Normal 的级别,反之,则切回 Normal 级别。通过这个步 骤,小节约了一把时间。。。 进程的优先级 在 windows 中,进程是有优先级的,当然,这个优先级不是真实的调度优先级, 而是该进程中,线程优先级计算的基准。在《Windows via C/C++》(也就是 《windows 核心编程》的第五版)中,有一张详细的表,表述了线程优先级和进程 优先级的具体对应关系,感觉设计的很不错,我就不罚抄了,有兴趣的自行动手翻 书。。。 当然这只是一道开胃小菜,满汉全席是控制进程的工作集大小,以达到降低进程实 际内存消耗的目的(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 页 面 ,你 发现页面一片惨白,一阵硬盘的骚动过后,好不容易恢复了原貌。 如果再切,相同的事情又会发生,孜孜不倦,直到你切过每一个进程。这个惨案发 生的主要原因,就是由于所有 Chrome 进程的工作集都被释放了,页面的重载和 Render 需要不少的一坨时间,这就大大影响了用户感受,毕竟,总看到惨白的画 面,容易产生不好的情绪。强烈感觉这个不算一个很出色的策略,应该有一个工作 集切换的底限,或者是在 Chrome 从 idle 中被激活的时候,偷偷摸摸的统一扩大工 作集,发几个事件刺激一下,把该加载的东西加载起来。。。 整体感觉,Chrome 对进程开销的控制,并不像想象中的有非常精妙绝伦的策略在 里面,通过工作集这总手段并不算华丽,而且,如果想很好的工作的话,有一个非 常非常重要的前提,就是被切换的页面,很少再被继续浏览。个人觉得这个假设并 不是十分可靠,这就使得在某些情况下,产生非常不好的用户体验,也许 Chrome 需要进一步在这个地方琢磨点方法的。。。 四、 Chrome 的 UI 绘制 1. Chrome 的窗口控件 Chrome 提供了自己的一个 UI 控件库,相关文档可以参见 这里 。用 Chrome 自己 的话来说,我觉得市面上的七荤八素的图形控件库都不好用,于是自己倒腾倒腾实 现了一套。。。 广告虽如此说,不过,Chrome 的图形控件结构,我还未发现有啥非常非常特别的 地方。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 内部,提供了所有可能的消息处理接口,并提供了默认实现,所有子类只需要覆盖 自己需要的消息处理函数即可。如果对 MFC 的消息映射有了解的话,可以知道两 者的区别。MFC 在设计的时候,觉得无法提供大而全的接口,因为消息总类实在太 多,而且还是可扩展的,于是就有了消息映射着一套繁琐的宏。但 Chrome 的图形 框架,显然没有做一个通用的 Framework 的打算,因此,可以采用这样的策略, 使得子类的派生变得简单而自然。。。 每一个 View 的子类控件,比如 Button 之类的,会存储一些数据,根据消息做一些 行为,并且绘制出自己。在 Chrome 中,画图的东西是 ChromeCanvas 这个类, 在其内部,通过 Skia 和 GDI 实现绘制。Skia 是 Android 团队开发的一个跨平台的 图形引擎,在 Chrome 中负责除了文字之外,所有内容的绘制;而文字绘制的重担, 在 Windows 中交到了 GDI 的手上。这样的设计会给跨平台带来一些困难,估计是 由 Skia 实现文本绘制会比较繁琐,才会带出如此一个设计的模式。。。 另外一个历史遗留产物,就是在 Windows 下的图形控件,还有一些是原生的,就 是说带有 HWND 那种传统的控件,这是 Chrome 身上不多的赶工期的痕迹,随着 时间的宽裕,这样的原生控件会被淘汰进历史的垃圾箱,而全部变为从 View 派生 的控件。。。 其实,对于 Chrome 这套控件架构我还没算摸得很熟悉,估计等到做一次插件之后 会了解的更透彻,因此,只说了点皮毛,聊表心意。。。 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 换成某个未来可能 出现的更好的渲染引擎。。。 重用 在《梦断代码》中,有一坨调侃重用的文字。他觉着软件重用的困难一方面来自于 场景本身很多变,很难设计出一套包罗万象的东西;另一方面来自于人,程序员总 是瞅着别人写的代码不顺眼,总喜欢自己写一套。。。 于是,解决重用这个问题也就只有两种,写最 NB 人见人服无所不能的代码,或者 是有很多很多 NB 代码共君任选。Google 无疑在这两个方面做得都不错, Map/Reduce,Big Table 之类的一套东西,强大到可以适合太多的场景,大大简 化了 N 多上层应用的开发。而对开源的利用使用,使得其可以随意挑一个巨人站到 他肩膀上跳舞,每看到这种场景,MS 估计都会气得拍着胸口吐血。。。 Google 本身在服务端的基础底层,有很深积累,随着 Chrome,Android 等等客 户端应用的开发,客户端的积累也逐步提升,也许,拥抱开源才是 MS 的正道?。。。 当你键入一个 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 的调用,从而完成了跨进程的一个调用, 反之亦然。。。 3. Chrome 的可扩展性 总所周知,firefox 通过三种方式进行自定义,插件、扩展和皮肤。其中,插件是使 得浏览器能用,不会出现一大块一大块的无法显示的区域;扩展是使得浏览器好用, 可以简单方便的进行功能的定制和个性化配置;皮肤是帮助浏览器变得好看,毕竟 罗卜白菜,给有所爱。。。 与之对比,来看 Chrome。Chrome 有了插件,有了皮肤,但是没有扩展。这就意 味着,你很难为 Chrome 定制一些特色的功能。目前,所有对 Chrome 的功能扩 展,都是通过书签抑或是修改内核来实现的。前者能力太弱,后者开发起来太麻烦, 容易出错不提,还必须要与时俱进,跟上版本的变化,并且还不能自由的选择或关 闭。因此,这都不是长远之计,Chrome 提供一套类似于 firefox 的扩展机制,也许 才是正道。据传说,Chrome 团队正在琢磨这件事,不知道最终会出来个怎么样的 结果,是尽力接近 firefox 降低移植成本,还是另立门户特立独行,我想可以拭目以 待一把。。。 在多进程模式下,Chrome 的插件还有一个问题,前面提到过,就是关于 UI 控件的。 由于 NPAPI 的标准,是允许插件创建 HWND 窗口的,这就使得当 Plugin 繁忙, 且 Browser 进程发起 HWND 的同步的时候,主进程被挂起,这个浏览器停滞。在 Render 进程中,解决这个问题的思路是控制权限,不然 Render 创建 HWND,到 了 Plugin 中,这招不能使用,只能够使用另一招,就是监管。不停的检查 Plugin 是否太繁忙,无法响应,一旦发现,立即杀死该 Plugin 及其所处的页面。这就好比 你想解决奶中有三氯氰胺的问题,要么控制奶源,不从奶站购买全部用自家的,要 么加强监管,提高检查力度防止隐患。两种策略的优缺点一眼便知,依照不同环境 采取不同策略即可。。。 总体说来,Chrome 的可扩展性着实一般,不过 Chrome 还处于 Beta 中,我们可 以继续期待。。。
还剩46页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

m3cc

贡献于2013-09-16

下载需要 10 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf