Life of a Pixel Chromium浏览器内核渲染原理 学习笔记

Chromium浏览器内核中,来自前端的内容如何在最终转换成为屏幕上的各个像素点,也就是浏览器内核渲染过程,这是一个复杂的工程上的一系列步骤。对于这些复杂的步骤我们需要把握的方面包括该过程每个阶段的设计思想、数据模型、数据模型的交互。深入而仔细地理解上述内容,有利与我们阅读庞大源码中始终保持清醒找准定位。下面我将基于我对Life of a Pixel的理解仔细来讨论这个过程。 Life of a Pixel输入与输出 首先需要谈谈输入与输出,这一系列步骤的输入成为Web Content。它由一系列现有协议所定义的一套描述Web内容的文本主要组成,当然也包括其他引用内容。现有的协议(我们常称为编程语言),通常是HTML、CSS与JavaScript。这三者分别有定义了Web内容的结构内容、样式、逻辑。但这并非严格分工,在当下流行的前端设计思想(前后端分离)中JavaScript正在承担着越来越多的职责。JavaScript本身也正在不断地变得独立,近年来逐渐跳出浏览器这个平台来独立发挥其作用(node.js、react-vr)。 另一方面,谈谈输出。如何绘制屏幕上的像素,涉及到计算机图形学方面的理论与工程。在传统上,我们可以认为计算机向屏幕输出内容经历了以下步骤。应用软件将其想要表达的图形内容转换成为对操作系统与图形相关的函数库的调用(OpenGL、Direct X等),这些函数库通过操作系统中安装驱动程序及其他操作系统有关服务向硬件(GPU等)传送数据与指令,并操纵硬件的计算核心与存储器(GPU等)完成光栅化等步骤、最终将硬件(GPU等)存储器中的最终内容转化为向屏幕输出的信号。在这方面,由函数库(OpenGL)所提供的API都是比较低级的。 举个例子,原来我也做过OpenGL相关的编程,虽然现代OpenGL提供一些模型与协议(如管线等)来简化工作,但是其提供的模型以及依照模型设计的API带有明显的硬件气息。对于OpenGL来说,调用者要精确输入预设的或者利用GPU程序计算出的数据其所绘制内容的在几何坐标系下的位置、颜色以及绘制顺序。 由输入输出来大致推断,浏览器的工作是按照先行前端协议(前端编程语言)及其附属多媒体、数据等方面的内容精确理解前端开发者对于Web页面的描述,并通过数据的层层流动与转换计算推断出图形函数库所需的各类信息。现行前端协议的复杂性与兼容性与鲁棒性要求使得这一步骤的实现十分复杂与庞大。而对于性能与稳定的要求更提升了设计与实现上的难度。实现这一系列不仅仅是技术上,而且也是软件工程上的难题。 页面生命周期综述 由上述对于输入与输出的讨论,我们可以理解Chromium团队提出以下有关于页面生命周期。 基于Web Content经过若干步骤产生相关数据模型。 数据模型随时间、交互等因素的不断更新。 其中,对于第二点要求尽可能少地快速地修改由初由第一步骤产生的数据模型,尽可能降低计算成本。其原因在于,第一步骤产生数据模型所进行的相关计算与数据模型的交互对于现有平均性能水平来说依然十分昂贵,所以不断重复执行第一步骤在实际环境中不可取。 初步渲染步骤概论 对于上面提及过的在输入与输出之间的相关步骤,我们将在下面按照前后顺序进行有关论述。 DOM 对于HTML来说,其语法规则有着明显的树状特征。这使得利用HTML能够方便地描述出文档结构并附带部分内容。所以我们需要提取出其中地结构信息与内容信息。在这一步中,HTML文档解析器将解析HTML文档中的文本并转换其为DOM树。 有关于DOM树,不得不提的是JavaScript对于DOM树的操作能力,我认为这也是JavaScript的核心内容之一。JavaScript借由该能力能够对页面进行控制、更新与内容添加、更改、删除等操作,我认为这是前后端分离思想的技术基础。上述能力对DOM树的实际操作由V8引擎具体完成,该引擎实现了JavaScript的操作DOM树的API,使得JavaScript具备该能力。 CSS(style) CSS有着两方面的主要作用,筛选或者指定其作用的HTML标签,定义其所对应HTML标签的内容。对于我们来说,其本质在于筛选或者查找出DOM节点,并将样式信息与DOM节点对应起来。 其中有个问题需要注意,在CSS文件对于某一个或者几个DOM节点的样式的描述中,可能存在着未定义、重复定义、冲突、无效的样式定义。针对这个问题,Chromium团队引入了重计算(recalc)针对DOM树每一个节点计算所有对应的Computed Style。 Layout 有了上面两个步骤提供的信息,我们需要进一步的转换。将DOM节点与Compute Style一道转换为视觉几何结构(Layout)。在这一步中我们需要解决的问题包括文字、表格、布局等等元素的在页面最终位置、排布及大小。为了能够对这些信息进行有效计算并且整理,Chromium构造了Layout Tree。这个数据结构旨在容纳结构有关信息并且进行上述工作。 在代码中,以下结构描述了有关信息。 // A LayoutRect contains the information needed to generate a CGRect that may or // may not be flipped if positioned in RTL or LTR contexts. |boundingWidth| is // the width of the bounding coordinate space in which the resulting rect will // be used. |position| is used to describe the location of the resulting frame, // and |size| is the size of resulting frame. struct LayoutRect { CGFloat boundingWidth; LayoutRectPosition position; CGSize size; }; Layout树与DOM树的节点关系并非一对一,有一些情况下DOM节点并不需要有其对应的Layout对象或者其可以放入其他有关的Layout对象中(通常为父节点对应的Layout对象)。其中有一点正在变化的内容需要注意,legacy layout object与LayoutNG。LayoutNG的提出,是为了解决当前的Layout对象中输入输出及其他中间内容混杂并且在计算过程中父子节点相互引用的问题。原先的设计首先带来了节点数据有效性的确定的问题,正在计算的节点需要判断其引用的数据是其需要的最终状态,不然节点当前所计算出的数据依然可能要在稍后重新计算,这提升了算法设计的复杂度。与此对应,LayoutNG的输入与输出分离,而且输出一旦产生其状态就已经确定且不可以修改,结构清晰,所以在算法设计上相对简单有效率。 ...

九月 7, 2021