浏览器渲染机制(二)上

简介: 浏览器渲染机制(二)上

首先,回顾一下,上一篇文章的内容,我们首先介绍了content概念,知道了我们最终要的结果是显示在屏幕上的像素,了解了浏览器渲染的目标【html/css/js 转换到 正确的opengl调用来调整像素的样式】,了解了Style,了解了Layout,了解了Panit,现在所有的绘制操作已经记录在显示项列表(display items)中了,那么现在就可以开始本篇文章的阅读。


资料:docs.google.com/presentatio…


读前提要


因为浏览器渲染机制的两篇文章是基于前面资料中的ppt为出发点研究,所以文章的顺序没有按照完整的渲染顺序编写,读起来体验应该比较一般,文章中也可能存在或多或少的问题,但是我会在后续进行修正,并梳理出一篇清晰易懂的文章。


raster光栅化


显示项列表中的绘制操作由一个称为光栅化的进程执行。光栅化可以将显示项列表转换成颜色值的位图。生成的位图中每个单元都保存着这个位图的颜色值与透明度的编码(如下图FFFFFFF,其实就是RGBA的16进制表示)。


网络异常,图片无法展示
|


光栅化这个过程还会去解码嵌入在页面中的图像资源,绘制操作会引用压缩的数据(比如JPEG,PNG等等),而光栅化会调用适当的解码器对其进行适当的解压。


网络异常,图片无法展示
|


  • 过去的GPU只是作为一个内存,这些内存被OpenGL纹理对象引用,我们会将栅格化的位图放到主内存里,然后上传到GPU,用来分担内存压力。


  • 现在的GPU也可以执行生成位图的命令(“加速光栅化”),这属于硬件加速,但是无论是硬件光栅化还是软件光栅化,本质都是生成了某种内存中像素的位图。


网络异常,图片无法展示
|


现在仅仅是位图存储在了内存中,像素还没有显示在屏幕上。GPU光栅化并不是直接调用的GPU,而是通过Skia图形库(谷歌维护的2D图形库,在Android,Flutter,Chromium都有使用)。


Skia提供了某种抽象层,可以理解更加复杂的东西,比如贝塞尔曲线。Skia是开源的,装载在Chrome二进制文件中,而不是存在于一个单独的代码库中。当需要光栅化的显示项(display item)时,会先去调用SkCanvas上面的方法,他是Skia的调用入口,SkCanvas提供了Skia内部更多的抽象,在硬件加速的时候,它会构建另一个绘图操作缓冲区,然后对其进行刷新,在栅格化任务结束时,通过flush操作,我们获得了真正的GL指令,GL指令运行在GPU进程中。


网络异常,图片无法展示
|


Skia和GL指令可以运行在不同的进程中,也可以运行在同一个进程,于是产生了两种调用方式。


  1. In Process Raster
  2. Out of Process Raster


1.In Process Raster


老版本采用了这种方式,Skia是在渲染进程中执行的,他会生产GL调用指令,GPU有单独的GPU process,这种模式下Skia没办法直接进行渲染系统的调用。在初始化Skia的时候,会给它一个函数指针表(这个指针指向了GL API,但不是真正的OpenGL API,而是Chromium提供的代理)。下面的GpuChannelMsg_FlushCommandBuffers是一个命令缓冲区,会将函数指针表转换成真正的OpenGl API。


单独的GPU进程有利于隔离GL操作,提升稳定性和安全性,这种模式也成为沙箱机制(就是把不安全操作放在单独的进程去执行)。


(图中 GLES2后端映射到桌面的OpenGL 2.1)


网络异常,图片无法展示
|


2.Out of Process Raster


新版本把绘制的操作都放在了GPU进程里面,在GPU进程中去运行Skia,可以提升性能。

光栅化的绘制操作包装在GPU的命令缓冲区在发送到IPC通道(进程间通信方式)。


网络异常,图片无法展示
|


接下来就是去执行GL指令,GL指令一般是由底层的so库提供的,在Windows平台还会被转换成DirectX(微软的图形API,用于图形加速)


网络异常,图片无法展示
|


问题


现在我们从content 一点一点的讲到了如何转换到内存里面的像素,但是页面的呈现并不是一个静态的过程(页面滚动,js脚本执行,动画等等。。。),在发生变化的时候去重新运行整个管道代价是十分昂贵的。


那么我们如何去提高性能呢???


网络异常,图片无法展示
|


Compositing Update


在Layout操作完成之后,按理是去进行Paint,但是我们直接Paint代价是十分昂贵的,于是引入了一个图层合成加速的概念。


那么什么是图层合成加速?


图层合成加速是把整个页面按照一定规则划分成多个图层,在渲染的时候,只要操作必要的图层,其他的图层只要参与合成就好了,以这种方式提高了渲染的效率,完成这个工作的线程叫做:Compositor Thread(合成器线程),合成器线程还具备处理事件输入的能力,比如滚动事件,但是如果在js中进行了事件的注册和监听,它会把输入事件转发给主线程。


网络异常,图片无法展示
|


主线程把页面拆分成多个可以独立光栅化的层,并在另一个线程(合成器线程)中将这些层合并。


这使得某些RenderLayer拥有自己独立的缓存,它们被称作合成图层(Compositing Layer),内核会为这些RenderLayer创建对应的GraphicsLayer。


  • 拥有自己的GraphicsLayer的RenderLayer在绘制的时候会绘制在自己的缓存里面
  • 没有自己的GraphicsLayer的RenderLayer会向上查找父节点的GraphicsLayer,直到RootRenderLayer(他总是有自己的GraphicsLayer)为止,然后绘制在有GraphicsLayer的父节点的缓存里面。


网络异常,图片无法展示
|


这样就形成了与RenderLayer Tree对应的GraphicsLayer Tree。当Layer的内容发生变化时,只需要更新对应的GraphicsLayer即可,而单一缓存架构下,就会更新整个图层,会比较耗时。这样就提高了渲染的效率。但是过多的GraphicsLayer也会带来内存的消耗,虽然减少了不必要的绘制,但也可能因为内存问题导致整体的渲染性能下降。因而图层合成加速追求的是一个动态的平衡。


图层化的决策目前是由Blink来负责,根据DOM树生成一个图层树,并以DisplayList记录每个图层的内容。


了解了图层合成加速的概念以后,我们再来看看发生在Layout操作之后的Compositing update(合成更新),合成更新就是为特定的RenderLayer创建GraphicsLayer的过程,如下所示:


网络异常,图片无法展示
|


Prepaint


属性树:在描述属性的层次结构这一块,之前的方式是使用图层树的方式,如果父图层具有矩阵变换(平移、缩放或者透视)、裁剪或者特效(滤镜等),需要递归的应用到子节点,时间复杂度是O(图层数),这在极端情况下会有性能问题。


于是,为了提高性能,引入了属性树的概念,合成器提供了变换树,裁剪树,特效树等。每个图层都有若干节点id,分别对应不同属性树的矩阵变换节点、裁剪节点和特效节点。这样的时间复杂度就是O(要变化的节点),如下所示:


网络异常,图片无法展示
|


prepaint的过程其实就是构建属性树的过程。


网络异常,图片无法展示
|


commit


前面提到了我们在paint这个阶段前,多了两件事情要做,先把页面拆成了很多图层,还构建了属性树。paint阶段做了什么?paint阶段把绘制的操作放在了display item里面。

下面就来到了commit阶段,这个阶段会更新图层和属性树的副本到合成器线程。


网络异常,图片无法展示
|


Tiling


合成器线程接收到数据之后,不会立即开始合成,而是把图层进行分块,这里涉及到了一个叫做“分块渲染”的技术,分块渲染会将网页的缓存分成一块一块的,比如256*256的块,之后进行分块渲染。


为什么要分块渲染?


  • .GPU合成通常是使用OpenGL ES贴图实现的,这时候的缓存实际就是纹理(GL Texture),很多GPU对纹理的大小是有限制的,比如长宽必须是2的幂次方,最大不能超过2048或者4096等。无法支持任意大小的缓存。
  • 分块缓存,方便浏览器使用统一的缓冲池来管理缓存。缓冲池的小块缓存由所有WebView共用,打开网页的时候向缓冲池申请小块缓存,关闭网页是这些缓存被回收。


tiling图块也是栅格化的基本单位,栅格化会根据图块与可见视口的距离安排优先顺序进行栅格化。离得近的会被优先栅格化,离得远的会降级栅格化的优先级。这些图块拼接在一起,就形成了一个图层,如下所示:


网络异常,图片无法展示
|


Activate


在Commit之后,Draw之前有一个Activate操作。Raster和Draw都发生在合成器线程里的Layer Tree上,但是我们知道Raster操作是异步的,有可能需要执行Draw操作的时候,Raster操作还没完成,这个时候就需要解决这个问题。


它将LayerTree分为了


  • PendingTree:负责接收commit,然后将Layer进行Raster操作
  • ActiveTree:会从这里取出光栅化好的Layer进行draw操作。


网络异常,图片无法展示
|


主线程的图层树由LayerTreeHost拥有,每个图层以递归的方式拥有其子图层。Pending树、Active树、Recycle树都是LayerTreeHostImpl拥有的实例。这些树被定义在cc/trees目录下。之所以称之为树,是因为早期它们是基于树结构实现的,目前的实现方式是列表。

相关实践学习
部署Stable Diffusion玩转AI绘画(GPU云服务器)
本实验通过在ECS上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。
相关文章
|
4月前
|
缓存 JavaScript 前端开发
浏览器渲染:理解页面加载的幕后工作
浏览器渲染:理解页面加载的幕后工作
|
2月前
|
JavaScript 前端开发 开发者
浏览器事件机制详解
浏览器事件机制详解
34 1
|
3月前
|
监控 前端开发 JavaScript
记录浏览器节能机制导致Websocket断连问题
近期,在使用WebSocket(WS)连接时遇到了频繁断连的问题,这种情况在单个用户上每天发生数百次。尽管利用了socket.io的自动重连机制能够在断连后迅速恢复连接,但这并不保证每一次重连都能成功接收WS消息。因此,我们进行了一些的排查和测试工作。
328 1
记录浏览器节能机制导致Websocket断连问题
|
2月前
|
存储 JavaScript 前端开发
在?聊聊浏览器事件循环机制
在?聊聊浏览器事件循环机制
26 0
|
3月前
|
移动开发 前端开发 JavaScript
浏览器端图表渲染技术SVG, VML HTML Canvas
浏览器端图表渲染技术SVG, VML HTML Canvas
28 0
|
4月前
|
前端开发 JavaScript 数据可视化
探索浏览器的内心世界:渲染机制的奥秘
探索浏览器的内心世界:渲染机制的奥秘
探索浏览器的内心世界:渲染机制的奥秘
|
4月前
|
消息中间件 JavaScript 前端开发
前端秘法进阶篇----这还是我们熟悉的浏览器吗?(浏览器的渲染原理)
前端秘法进阶篇----这还是我们熟悉的浏览器吗?(浏览器的渲染原理)
|
4月前
|
消息中间件 前端开发 Java
【面试题】前端必修-浏览器的渲染原理
【面试题】前端必修-浏览器的渲染原理
|
10月前
|
缓存 前端开发 UED
浏览器关于 Largest Contentful Paint (LCP) 的计算机制
浏览器关于 Largest Contentful Paint (LCP) 的计算机制
|
12月前
|
缓存 JavaScript API
关于现代浏览器的 back-and-forward 缓存机制
关于现代浏览器的 back-and-forward 缓存机制