Chromium加载网页的过程,需要Browser进程和Render进程协作完成。加载网页的过程由Browser进程发起,向服务器请求网页内容的过程也是由Browser进程完成。Render进程负责对下载回来的网页内容进行解析,解析之后得到一个DOM Tree。有了这个DOM Tree之后,Render进程就可以对网页进行渲染了。本文接下来就对上述过程涉及到的重要概念进行简要介绍以及制定学习计划。
第一个涉及到的重要概念是Chromium的模块划分及其层次关系,如图1所示:
图1 Chromium模块划分及其层次关系
这个图来自官方文档:How Chromium Displays Web Pages。从下往上看:
1. WebKit:网页渲染引擎层,定义在命令空间WebCore中。Port部分用来集成平台相关服务,例如资源加载和绘图服务。WebKit是一个平台无关的网页渲染引擎,但是用在具体的平台上时,需要由平台提供一些平台相关的实现,才能让WebKit跑起来。
2. WebKit glue:WebKit嵌入层,用来将WebKit类型转化为Chromium类型,定义在命令空间blink中。Chromium不直接访问WebKit接口,而是通过WebKit glue接口间接访问。WebKit glue的对象命名有一个特点,均是以Web为前缀。
3. Renderer/Renderer host:多进程嵌入层,定义在命令空间content中。其中,Renderer运行在Render进程中,Renderer host运行在Browser进程中。
4. WebContents:允许将一个HTML网页以多进程方式渲染到一个区域中,定义在命令空间content中。
5. Browser:代表一个浏览器窗口,它可以包含多个WebContents。
6. Tab Helpers:附加在WebContents上,用来增加WebContents的功能,例如显示InfoBar。
我们可以将第1层和第2层归结为WebKit层,第3层和第4层归结为Content层,第5层和第6层归结为浏览器层。如果以进程为边界,Tab Helpers、Browser、WebContents和Renderer host运行在Browser进程中,Renderer、WebKit glue和WebKit运行在Render进程中。
Content层是Chromium的核心模块,它实现了Chromium的多进程架构。Content层主要向外提供的接口是WebContents,浏览器层通过这个WebContents接口就可以将一个HTML网页渲染在一个区域上。例如,Chrome就是通过Content层提供的WebContents接口实现一个浏览器的。同样我们也可以通过Content层提供的WebContents接口实现一个与Chrome类似的浏览器,甚至我们也可以通过Content层提供的WebContents接口实现一个嵌入在应用程序的浏览器控件,例如Android 4.4的WebView。
在Content层中,一个用来渲染网页的区域称为RenderView。一个RenderView也称为一个RenderWidget。RenderView与RenderWidget的区别是,前者是描述的是一个用来显示网页内容的控件,后者描述的是一个可以接收用户输入的控件,但是它不一定是用来显示网页内容的。例如,点击网页上的选择框弹出来的窗口,是一个RenderWidget,它里面显示的内容与网页无关。
我们可以将RenderView和RenderWidget看作是一个接口。Browser进程和Render进程都需要实现这个接口。其中,Browser进程分别实现RenderView和RenderWidget接口的两个类是RenderViewHostImpl和RenderWidgetHostImpl,Render进程分别实现RenderView和RenderWidget接口的两个类是RenderViewImpl和RenderWidgetImpl。Browser进程的每一个RenderViewHostImpl对象和每一个RenderWidgetHostImpl对象在Render进程中都分别对应有一个RenderViewImpl对象和一个RenderWidgetImpl。RenderViewHostImpl对象与RenderViewImpl对象、RenderWidgetHostImpl对象与RenderWidgetImpl对象可以通过Browser进程与Render进程建立的IPC通道进行通信。
WebKit层的WebView和WebWidget相当于Content层的RenderView和RenderWidget。我们注意到,WebKit层还有一个称为WebFrame的对象。事实上,Content层也有一个类似的对象,称为RenderFrame。WebFrame和RenderFrame都是用来描述网页的。既然已经有了RenderView和RenderWidget,为什么还需要WebFrame和RenderFrame呢?这与Chromium的Out-of-Process iframes(OOPIFs)项目有关。关于Chromium的Out-of-Process iframes(OOPIFs)项目的详细信息,可以参考官方文档:Out-of-Process iframes (OOPIFs)。
当网页使用window.open在另外一个Tab打开一个新的网页,或者通过iframe标签嵌入另外一个网页时,源网页和目标网页有可能在同一个Render进程中,也有可能在不同的Render进程中,这取决于它们是否来自同一个站点。在HTML5规范中,源网页和目标网页组成了一个浏览上下文单元(Browsing Context)。当源网页和目标网页不在同一个Render进程时,源网页如何通过HTML5规范中的window.postMessage接口发消息给目标网页呢?如图2所示:
图2 HTML5的window.postMessage实现
在图2中,网页A通过window.open打开了网页B,它们分别在两个不同的Tab中,这两个Tab又是在不同的Render进程中。在Browser进程中,网页A和网页B分别对应有一个WebContents对象。也就是Browser进程会为每一个网页创建一个WebContents对象,如前面的图1所示。为了让网页A能够通过window.postMessage接口给网页B发送消息,负责渲染网页A的Render进程会为网页B创建一个代理对象,这个代理对象知道如何发送消息给网页B(通过Chromium的IPC消息发送、接收和分发机制分析一文分析的Routing机制)。同样,负责渲染网页B的Render进程也会为网页A创建一个代理对象,这个代理对象负责接收从网页A发送过来的消息,并且分发给网页B处理。注意,代理对象在图2中均通过虚线框表示。通过这种代理对象方式,就实现了不在同一个Render进程中的两个网页的相互通信。
Chromium是如何实现上述代理对象的呢?我们通过图3所示的例子进行说明,如下所示:
图3 在同一个Browsing Context中的网页
网页A在一个Tab显示,并且它通过iframe标签包含了网页B和网页C。网页C通过window.open在另外一个Tab中打开了另外一个网页C实例。新打开的网页C通过iframe标签包含了网页D,网页D又通过iframe标签包含了网页A的另外一个实例。
这时候Browser进程会分别为图3的两个Tab创建一个WebContents对象,如图4所示:
图4 RenderFrameHost/RenderFrameProxyHost
每一个WebContents对象都关联有一个Frame Tree。Frame Tree中的每一个Node代表一个网页。第一个Tab的Frame Tree包含有三个Node,分别代表网页A、B和C。第二个Tab的Frame Tree也包含有三个Node,分别代表网页C、D和A。
代表网页B的Node关联有一个RenderFrameHost对象和三个RenderFrameProxyHost对象,其中,RenderFrameHost对象描述的是网页B本身,另外三个RenderFrameProxyHosts对象描述的是网页A、C和D。也就是说,在Browser进程中,代理对象是通过RenderFrameProxyHost类描述的。
图3所示的网页A、B、C和D分别在不同的Render进程中渲染,如图5所示:
图5 RenderFrame/RenderFrameProxy
在负责渲染网页A的Render进程中,有两个RenderFrame对象,分别代表图3所示的两个网页A实例。负责渲染网页A的Render进程还包含有四个RenderFrameProxy对象,分别代表网页B、C和D。在负责渲染网页B、C和D的Render进程中,也有类似的RenderFrame对象和RenderFrameProxy对象。其中,RenderFrameProxy对象就是前面描述的代理对象。
每一个RenderFrame对象和RenderFrameProxy对象在图1所示的WebKit glue层中,分别对应有一个WebLocalFrame对象和WebRemoteFrame对象,如图6所示:
图6 WebLocalFrame/WebRemoteFrame
注意,WebLocalFrame类和WebRemoteFrame类都是从图1的所示的WebFrame类继承下来的,它们都是属于WebKit glue层的。前面我们提到,WebKit glue层是用来封装WebKit层的,WebLocalFrame对象和WebRemoteFrame对象封装的便是WebKit层中的LocalFrame和RemoteFrame对象。Chromium将WebKit glue层和WebKit层统称为Blink模块。这意味着在Blink模块中,前面描述的代理对象是通过WebRemoteFrame和RemoteFrame描述的。
从前面的分析我们就可以看到,在Chromium中,为什么一个网页既要使用RenderView、RenderWidget,又要使用RenderFrame、WebFrame来描述,它们的作用是不一样的,总结来说,就是:
1. RenderView描述的是一个用来显示网页内容的RenderWidget。
2. RenderWidget描述的是一个UI控件。
3. RenderFrame用来建立网页之间的消息通道。
在WebKit层中,每一个LocalFrame对象都关联有一个LocalDOMWindow对象,这个LocalDOMWindow对象又关联有一个Document对象。这个Document对象描述的就是一个要在当前Render进程进行加载和渲染的网页。
在WebKit层中,每一个RemoteFrame对象都关联有一个RemoteDOMWindow对象。但是由于RemoteFrame对象描述的是一个代理对象,因此它关联的RemoteDOMWindow对象是不关联有Document对象的,只是作为一个Place Holder存在当前Render进程中。
LocalDOMWindow对象和Document对象描述的就是HTML规范的DOM Window和Document,它们是在网页的加载和解析过程中创建的。其中,Document对象包含有一个DOM Tree,如图7所示:
图7 DOM Tree和Layer Tree
这个图来自官方文档:GPU Accelerated Compositing in Chrome。DOM Tree中的每一个Node对应的就是网页中的每一个HTML标签。每一个HTML标签都对应有一个Render Object,这些Render Object又形成一个Render Object Tree。Render Object知道如何绘制其描述的HTML标签。
具有相同坐标空间的Render Object都绘制在同一个Render Layer中,这些Render Layer又形成了一个Render Layer Tree,这意味着并不是所有的Render Object都有自己的Render Layer。Render Layer Tree的根节点一定对应有一个Render Layer,其余的子节点如果不需要使用单独的Render Layer,那么就会与父节点使用相同的Render Layer。因此,Render Object与Render Layer是多对一的关系。典型地,设置有透明属性、CSS Filter的Render Object有单独的Render Layer,标签canvas和video对应的Render Object也有单独的Render Layer。Layer是图形渲染引擎普遍使用的一个技术,是为了方便绘制一组具有某些相同属性的图形而生的,这样就不会为每一个图形都单独执行相同的绘图操作。
在图形渲染引擎中,Layer会对应有一个Backing Surface。在软件方式渲染中,Backing Surface就是一个内存Buffer。在硬件方式渲染中,Backing Surface就是一个FBO。为了减少Buffer和FBO开销。WebKit不会为每一个Render Layer都分配一个Backing Surface,而是让某些Render Layer共用同一个Backing Surface。这意味着Render Layer与Backing Surface是多对一的关系。在WebKit中,具有Backing Surface的Layer称为Graphics Layer,这些Graphics Layer又形成了一个Graphics Layer Tree。每一个Graphics Layer都关联有一个Graphics Context。这个Graphics Context是由Chromium提供给WebKit的,它知道如何绘制Render Layer的内容。Graphics Context绘制出来的内容最后会通过合成器(Compositor)渲染在屏幕上。
上面描述的Render Object Tree、Render Layer Tree和Graphics Layer都是与网页渲染相关的,是我们下一个系列的文章要分析的内容。在这个系列的文章中,我们主要是分析DOM Tree以及前面描述的Frame Tree。
Frame Tree由Browser进程创建,DOM Tree由Render进程创建。Frame Tree是在网页内容下载回来前就创建出来的,并且在后面网页的解析和导航时增加或者移除子节点。DOM Tree是在网页内容下载回来后进行创建的,并且是根据网页的内容进行创建的。当一个网页的DOM Tree创建出来之后,它的加载过程就完成了。
注意,网页内容下载是由Browser进程执行的。Browser进程再将下载回来的网页通过共享内存交给Render进程处理。为什么不直接由Render进程下载呢?这是为了安全起见,Render进程按照最小权限原则创建,它不具有网络访问权限,因此就不能从服务器上下载网页内容回来。
为了更好地理解网页加载过程,以及这个过程中涉及到的各种对象,接下来我们将按照以下三个情景分析网页的加载过程