公司内部同事翻译的一篇技术文章,很多干货,讲解了浏览器渲染的一些原理的东西,值得一读。
去年翻译了一篇google的文章:GPU Accelerated Compositing in Chrome,最近翻出来重看了一下,重新整理了下,原文很长,对原文进行了一些整理和删减,想看原文请戳上面链接~
以下均针对chrome
硬件(GPU)加速加速的是什么?
是层的合成(layer composite),我们常说的使用translate3D开启硬件加速,其实是使应用了translate3D的元素获得独立的GraphicsLayer,那拥有独立的GraphicsLayer有啥好处呢?简单来说:
所以说,在我们平常做动态效果时,会使用translate3D的hack,利用GPU的合成加速来获取更好的效果和性能。
下面详细讲述下chrome中硬件加速合成
传统方式下,浏览器依赖于CPU来渲染页面内容。而随着GPU硬件能力的不断发展(包括一些小型设备,比如手机的硬件能力也有很大提升),开始试图使用GPU的硬件能力来获得更好的性能和更少的电量消耗。使用GPU来渲染合成页面内容可以获得明显的速度的提升。
GPU在合成页面layer上的效率要比CPU高很多,尤其是涉及大量像素的绘制(drawing[1])和合成(compositing)操作。(GPU:人家设计出来本来就是做这些工作的好伐(lll¬ω¬))
比较基础的内容,已经了解,可以直接略过
Blink Rending 引擎(由Google和Opera Software开发的浏览器排版引擎)的源码十分繁多和复杂。为更好理解在chrome中GPU加速是如何工作的,我们首先需要了解Blink引擎是如何渲染页面的。
在Blink引擎中,页面内容是存储为由Node对象组成的树状结构,也就是DOM树。每一个HTML element元素都有一个Node对象与之对应,DOM树的根节点永远都是Document Node。
DOM树中得每个node节点都有一个对应的RenderObject对象。RenderObject存储在与DOM树相对应的树形结构中——Render Tree。RenderObject知道如何在屏幕上paint Node内容,当然为实现这一操作,RenderObject会调用其对应的GraphicsContext来执行必要的draw操作。其中,GraphicsContext就是负责将像素写入位图(bitmap)中,这些位图最终会展示在屏幕上。
PS:draw != paint,draw是将像素点绘制到屏幕上(将最终的屏幕画面放到屏幕上),而pain是生成元素呈现的像素,例如,一个有着灰色背景,有文字的元素,当浏览器paint它时,是决定哪些像素填充背景,哪些像素填充文字,然后浏览器将这些像素存入位图(bitmap)中。
每一个RenderObject都直接或间接(通过其父对象)的同一个RenderLayer相关联。
RenderLayer的作用就是保证页面元素以正确的顺序合成(composited),这样才能正确的展示元素的重叠以及半透明元素等等。会有一些情形,为一些特殊的RenderObjects创建一个新的RenderLayer。以下是常见的一定会新建RenderLayer的RenderObject:
对于上述特殊的RenderObjects,它对应于为它所创建的新的RenderLayer,而其他的RenderObjects与其最近的拥有RenderLayer的父级RenderObject,对应于同一个RenderLayer(有点拗口,就是说他本身没有,他就去他的父级找,直到找到的第一个)
RenderLayer也是一个树状的结构。根节点就是页面根元素所对应的RenderLayer,视觉上,每一个layer节点的后代都包含在父layer中。每一个RenderLayer的子layer保存在两个升序排列的列表中,分别是negZOrderList(负z-index的layers,即当前layer之下的layer)和posZOrderList(正z-index的layers,即当前layer之上的layer)
为利用合成器(compositor,下一章会介绍),满足如下条件的RenderLayers,会被认为是一个独立的合成层:
如果RenderLayer是一个合成层,那么它有属于它自己的单独的GraphicsLayer,否则它和它的最近的拥有GraphicsLayer的父layer共用一个GraphicsLayer。如同RenderObject和Renderlayer的关系一样。
每一个GraphicsLayer都有一个GraphicsContext,其对应的RenderLayer会paint进GraphicsContext中。合成器(compositor)最终会负责,将由GraphicsContext输出的位图(bitmap)合并成最终屏幕显示的图案。
所有的规则都会有漏洞。正如上面提到的,GraphicsLayers会消耗内存和其他资源(随着GraphicsLayer树的大小增长,会使CPU执行时间越来越长)。当一些RenderLayer同一个成为独立合成层的RenderLayer重叠时,就会产生大量的额外的合成层(上节合成层产生条件的最后一条),十分消耗资源。
我们把产生合成层的自身因素(比如有3D变换的层)称之为直接因素,将比如:合成层后代,同合成层重叠等条件成为简介因素。为了防止上述所说的“层爆炸”,当很多element覆盖在因直接因素产生的层之上时,浏览器的排版引擎,会将这些element的RenderLayers覆盖在这个直接因素产生的RenderLayer上,同时将他们压缩(squash)成一个“层”。这就防止了由覆盖引起的层爆炸。更多细节请看这里和这里
chrome通过WebLayer和cc layer(chrome compositor layer)实现GraphicsLayers。
总的来说,为rendering服务的有如下四种树形结构:
如下图所示:
之后文章中所说的层(layer)都代指cc layer(chrome compositor layer:GraphicsLayer在chrome中得实现)。合成器(Compositor)操作的是cc layer。
chrome合成器(Compositor)是一个软件库,用来管理GraphicsLayer树
回想一下,rendering有两个阶段:先是paint然后composite。合成器会在每个层的基础上执行一些额外的工作。比如,在层的位图合成前,合成器会负责对这些层的位图执行一些必要的transform(比如层的CSS tranform属性指定的变换)。另外,因为层的painting[2]和compositing解耦了,所以一个invalidating[3](需要repainting的)层只会重绘(repainting)自己的内容,然后所有层重新进行合成(recopositing)。
合成器还会负责将层合成绘制为最终的屏幕画面。
GPU是如何发挥作用的呢?合成器会使用GPU来执行它的合成绘制的步骤。这和老的软件渲染模型有显著的区别,在老的模型中,Render进程将页面内容的位图(bitmap)通过IPC(进程间通信)和共享内存的方式来传给浏览器进程去呈现。
而在硬件加速体系结构中,合成由GPU负责。合成器本质上也是使用GPU将层绘制成一个位图,最后输出到屏幕上。
合成是交由GPU完成的,那Render进程如何传递命令给GPU的呢?在chrome多进程模型中,有一个专门的进程负责这个任务:GPU进程。GPU进程的存在主要是因为安全原因。需要注意的是,android是个例外,Chrom会使用内置的GPU实现作为浏览器主进程的一个线程来运行。当然android上的GPU线程和其他平台的GPU进程的行为是一致的。
GPU进程体系有如下的好处:
具体的工作原理就不说了(自己也一知半解...),有一点需要注意,
一些较大的资源,比如位图,Render进程和GPU进程是通过共享内存传递的,也就是说合成层是会占用内存(显存)的,所以像translate3D这样的hack不要过度使用,一旦用了,就会产生一个合成层,就会占用内存(显存),浏览器是很智能,有一套生命周期和共享的管理机制,但也架不住滥用的,尤其是在手机上,大部分手机的显存和内存是共享的,占用过多,手机会变卡的,所以hack虽好,可不要滥用哦。
在chrome为渲染服务的有两个进程:Render进程和GPU进程。GPU进程负责Render进程和GPU之间的命令的传递,Render进程包括主线程和合成线程,在chrome中,paint和composite放在了合成线程中。
之前说过,合成器是一个软件库,它最初是运行在Renderer主线程中,然后转移到了自己的线程中(也就是所谓的合成线程)。
合成线程和主线程是相互独立的,合成线程持有一份主线程层树(layer tree)的副本,主线程和合成线程的层数是独立的,他们之间通过消息机制保持同步,这意味着主线程可以专注于运行javascript,而合成线程则专注于处理paint和composite
一些输入事件(主要是scroll)会首先从浏览器进程转移到合成线程,然后从合成线程转交给Renderer主线程。通过控制输入和输出,合成器可以保证对用户输入的第一时间进行视觉响应。
有趣的是,这种体系结构导致了javascript的touch事件回调中调用preventDefault()可以阻止滚动,而scroll事件的却不可以。如果javascript要取消touch事件,那么合成器在javascript回调(运行在主线程中)完成前是不会滚动页面的。而对于scroll事件,会先转移到合成线程,合成线程会立刻开始滚动,只是将一些诸如滚动偏移量的信息异步传给主线程,而不管主线程是否立刻处理滚动事件。
chrome在渲染时做了很多优化:
[1] drawing:将像素点绘制到屏幕上(将最终的屏幕画面放到屏幕上)的render阶段
[2] painting:RenderObjects调用GraphicsContext API生成相应的视觉展现的render阶段。生成元素呈现的像素,例如,一个有着灰色背景,有文字的元素,当浏览器paint它时,是决定哪些像素填充背景,哪些像素填充文字,然后浏览器将这些像素存入位图(bitmap)中。
[3] invalidation:标记为dirty的文档区域,一般表示该区域需要repainting。样式系统(style system)也有相同的概念。