浏览器历史演进
浏览器的历史可以追溯到上世纪80年代,其中有两个人物扮演了举足轻重的作用。Berners-Lee,这位大神是W3C组织的理事,他在80年代后期90年代初发明了世界上第一个浏览器WorldWideWeb(后更名为Nexus),并在1991年公布了项目源码;Marc Andreessen,于1993年发布了真正有影响力的浏览器Mosaic,是网景浏览器的前生。
早期的浏览器功能也非常简单,大多数是用作显示静态文本,没有CSS带来样式,没有Javascript来做动态构建。而后浏览器快速发展,并迅速席卷全国,一大批公司想要渗透进去分一杯羹。网景,微软,Firefox,谷歌,一串科技公司进入,迎来这真正群雄割据的状态。而历史也是惊人的相似,最终胜出者寥寥无几。就像中国的三国时期,浏览器现在也形成了微软IE,谷歌Chrome,Firefox三足鼎立的格局。当然还包括Safari,因为最重要的内核框架就是来源于苹果公司提出并主导的开源项目:webkit。
普通用户可以将webkit理解为针对浏览器不同功能的标准规范,各个厂商可以基于规范自己实现对应的功能。说到浏览器内核,也不只有webkit一个。苹果safari和chrmoe用的是webkit,firefox用的是Gecko,IE用的是Trident。在移动端,safari 和android两大阵营占据了90%的移动设备份额。safari自不必说使用webkit,android被谷歌收购后也使用了相关的webkit技术。因此掌握webkit内核的设计理念基本上就可以在当今了解主流浏览器的内核设计理念。
虽然谷歌和苹果都使用了webkit内核,但是其实现还是不一样的,从技术层面来说,苹果偏保守,谷歌偏激进。在谷歌内部创建了名为chromium的项目用于不断尝试新技术,同时将稳定成熟版本以chrome的形式发布出来。
说了这么多最关键的几个知识点还没覆盖到,比如什么是浏览器内核,有什么作用?既然叫内核,说明其属于浏览器的核心功能,当我们脑补这样一个黑盒场景。用户在地址栏输入一个url地址,浏览器呈现出对应的图像。而这个一进一出的流程中的转换流程就是通过浏览器内核来实现。其涉及技术层面非常广泛,从底层往上包括操作系统对接、网络资源获取、资源类型解析、图像2D,3D渲染等等。是一个复杂且完整的架构,具体的细节会在下面讨论。
趣闻轶事之用户代理(user-agent):在开发移动端和PC端浏览器页面时,适配兼容是一个逃不开的问题。很多实践直接基于浏览器的user-agent字符串来匹配判断,但是有没有想过为什么这玩意这么长?其实这是个历史遗留问题。在早期浏览器群雄割据的年代,每个浏览器厂商都想把最新的技术引入自己的浏览器,用以招揽优秀的开发者来对自己的浏览器进行适配开发,所以user-agent就被各大浏览器厂商写的越来越长,变成如今的模样。
webkit整体架构
俗话说,一图胜千言,直接上webkit的架构图:
image.png
图中虚线框都是依赖浏览器厂商和操作系统可以动态变化的,比如你想怎么解析javascript脚本,用什么引擎随你便,默认是JavascriptCore;再比如你想用哪种方式去获取资源,这就是网络栈的功能,linux和windows等不同操作系统的实现可能就会很不一样。因此webkit可以按照不同的需求和环境编译成不同的版本,有针对移动端的,有针对windows的,等等。但是不变的就是webCore中定义的几个功能。CSS,SVG,HTML属于文件类型解析,布局,渲染树属于最终呈现,如何将解析结果映射到用户的屏幕上,都是有严格流程的。而且这一流程是动态更新的,资源可能会随着用户操作不断更新,因此会不断重复获取,渲染,更新,呈现等步骤。
接下来让我们从网页URL到DOM树的建立来看看是怎样的一个过程:
image.png
在这一过程中,从URL开始,借助加载器来获取不同的资源文件:HTML,CSS,JS,图片。然后调用对应的解释器来解释,最终统一汇总到DOM Tree中。
对web熟悉的人会问,那CSS的样式在哪里?当当当,看下面:
image.png
这属于DOM Tree的下一步了,上一步拿到DOM树,结合解析进来的CSS样式生成一个个的renderObject,同时还要掺杂复杂的布局计算,完了以后生成RenderLayer树,最终生成绘图上下文,这个上下文应该就可以理解为最终的图像啦。
大伙是否还记得之前说渲染是一个动态的过程,因此这么多的中间步骤,dom tree,render Tree等中间过程数据都要保存下来,所以说整个的实现还是非常复杂的。
资源加载
现在我们把视角聚焦到资源的获取这块,从用户输入一个URL开始其实经历了一连串的过程:
- 域名DNS解析:查询域名转换成ip地址,一般耗时几十毫秒
- TCP建立连接:一般也要几十毫秒的耗时,三次握手
- 客户端生成HTTP报文并发送
- 获取response报文并解析
5.解析文件,针对解析到的其他资源继续获取
所以从上面的流程看其实隐藏着很多细节。所谓天下武功,为快不破。因此用户体验是非常重要的,像一个页面加载几十秒直接会让用户奔溃,改用其他的浏览器。因此如果想着每个资源都去远程拉是非常愚蠢的。所以缓存策略的引入是必须的,那接着就会引入下一个问题,我怎么判断哪些资源需要缓存,缓存多久,啥时候需要重新拉取?对于第一个点,现代的浏览器会默认维护一个资源池,你可以理解为是一个资源url为key值的键值对映射。当然这个映射也是有限制,不可能无限大,因此就需要引入一种机制来判断缓存哪些,删去哪些,而webkit等内核常用的一种策略是LRU(Least Recent Used),翻译过来就是把那些不常用的资源给踢走,保留使用频率高的资源。
对于缓存多久,啥时候重新拉取,这就需要协议的帮忙了,Http协议中可以设定强缓存和协商缓存的机制,通过header的几个字段来表征:cache-control,if-modified-since。
安全: 现代浏览器对安全高度重视,http协议本身是不安全的,比如谷歌等公司大力推动https协议,同时将http网站标记为不安全的。
性能:这是资源加载的核心,这就是为什么会有chromium的多进程下载,缓存策略,本地缓存,会话缓存,DNS prefetch, TCP Preconnect, SPDY,HTTP2。
针对资源加载的性能问题,开发者需要注意的可以优化的点也有很多:
- 不要页面设置很多的重定向来增加域名解析时间;
- tcp建立连接很耗时间,因此减少不必要的分散资源,该整合的整合。采用HTTP2协议来实现多路复用是不错的选择;
- 资源本身大小控制下,该压缩的压缩;
- 对于小图片可以通过精灵图或是编译成base64字符串来加载
......
HTML文件解析&DOM模型
紧接着上面的流程,在获取完资源文件后,我们开始分析html文件,在webkit内部通过复杂的解析过程生成DOM树。现在就让我们深入探索下这一解析流程是如何进行的。
首先需要回答的问题就是什么是DOM结构,有什么功能?DOM的全称为Document Object Model。翻译过来为文档对象模型,也是属于标准规范的一种,经历了Level 1,Level 2, Level 3等几个版本的迭代。虽然我们常见的认识是从html生成dom,但是其使用范围其实比预想的要大的多,基本上所有标签化的文件格式都是可以生成dom的,例如xml。
宏观上来说,解析过程分为两步,首先是词法Token的分析处理,然后再是dom树的创建。这其中都涉及到很多类的调用。先来看下webkit是如何处理获取的html文件的,直接上图:
image.png
上图是一个粗的流程,从html带过来是字节流,结果解码后生成字符流。对于字符流的处理就是识别一个个的Token,然后生成对应节点,最终组合成DOM树。
词法Token分析调用的是HTMLTokenizer类,紧接着跟着一个安全机制,为了防止XSS(Cross Site Security)跨站攻击,使用XSSAudit的类来过滤不安全的词法,通过的就可以开始生成DOM节点了,节点生成由HTMLDocumentParse统一负责,其下面有个HTMLTreeBuilder类专门负责DOM Tree的生成。
说完解析过程,接下来再来看看DOM中的节点,在webkit中节点Node的类型非常多,但万变不离其宗,核心的类关系如下图所示:
image.png
基本类为Node,实现EventTarget接口和ScriptWrappable接口。前者实现Node节点的事件处理,后者完成Node节点JS的功能。而在下方有个抽象类ContainerNode,Element,文档和属性节点都是继承自ContainerNode。
最后补充一下Node节点的事件机制,也就是之前提到的EventTarget基类的原理。其包含两部分,自上而下的事件捕获(Event Capture)和自下而上的事件冒泡(Event Bubbling)。形象的如下图所示:
image.png
解码配置:在解析初期,就会获取html文件中的字符编码格式,一般是utf-8,然后解析成字节流。
有人会好奇HTMLTokenizer是如何解析的么?其实结构化文档(html, xml,etc)都是有开始标签(startTag)和结束标签(endTag),所以就很适合用栈来处理,而在webkit内部确实这样,使用HTMLElementStack类来处理。
影子DOM( Shallow DOM): 其定义是在渲染中出现而在dom中不出现的节点,可以形象的用namespace命名空间来类比。在某个节点下的影子节点在dom中是隐藏的,其目的很多是希望单独封装一部分dom结构,而不希望被外界的dom操作,样式传递所改变。而html5中定义了很多的音视频元素都是借助影子节点来实现的。
CSS解释器和样式布局
在DOM树创建完后,下一步就是将样式加载进来。在一个万物都是对象的程序世界,CSS的解析结果也是基于对象的,简称为CSSOM(CSS Object Model, CSS对象模型)。就像DOM一样,对象都会提供接口给JS来调用和控制。在最新的HTML5中,引入了document.stylesheets属性来展示和控制所有加载进来的样式数据,包括内部样式和link外部样式。但是虽然提供很多的样式控制功能,但从性能上来看还是不建议频繁的修改样式来触发页面重绘。下面就来解析下整个CSS的解析流程:
image.png
样式的解析主要通过CSSParser类和CSSGrammer类,最终生成StyleRule规则,而众多样式规则经过归集后统一保存到内部的document节点中,节点涉及的内部类如下图所示,这就是为什么我们可以通过document属性来操作元素的样式:
image.png
在样式属性解析完后接下来就要将每条规则作用到对应的dom节点中,从dom元素生成对应的renderObject,而匹配规则需要借助优先级来判断,CSS样式中的id样式,class样式,属性样式等等具有不同的优先级。基本的逻辑是描述的越具体的优先级越高,相同优先级时后定义的样式覆盖之前的样式。举个例子,在CSS内部ID属性的优先级为100,class属性为10,将所有优先级计算完成后按从高到低来渲染。
样式属性的归集计算还不是结束,下面是元素布局的计算。内部通过FrameView类来实现,RenderObject类继承Frame,而这个Frame就是CSS中所定义的线框模型(或称之为盒模型)。
image.png
dom元素通过递归方式调用renderObject的layout计算布局,因为父元素需要依赖子元素的大小尺寸来计算自己的布局宽度,而且不同的display方式也会影响布局。
布局是webkit中最终的计算设计,因为这直接影响到最终的呈现,因此一定会有对应的测试。而且不同的webkit迁移版本会有对应的测试集合。
渲染流程
在webkit的最后一步就是讲上面生成的RenderObject 树进行渲染,而在其中还得经过一步的转换,从RenderObject树再通过计算生成RenderLayer树。这个RenderLayer生成以后就可以借助2D和3D的渲染引擎来绘制了。
那我们现在聚焦下这个转换是如何实现的?这里其实有个很形象的比喻:PhotoShop的图层。没错,将一个个RenderLayer理解为一层层的图层是非常合理的,而实际上也是这么做的,最底层也就代表着RenderObject的根节点,在浏览器上就是视口viewport大小。
接下来只需要解释下如何分层,那么这个渲染逻辑也就很清楚了。在实际的webkit实现中是有一套规则的:包括document元素单独一层,h5中的video、canvas等新开一层,浮动元素又是一层。最终生成下下图描述的图层数据格式:
image.png
数据以layer来做归集,每层包含同级的元素尺寸和初始位置。同时从图上可以看到每个render的元素类型也是不尽相同,针对Block,Text文本,Table,Canvas都有针对性的类来专门处理,就像DOM中节点的类型一样。具体的类继承关系可以参考下图:
render object 相关类继承关系
硬件渲染有两种类型:依赖CPU的软件渲染和依赖GPU的硬件渲染。两种的工作方式不同,应用场景也不同。前者使用CPU做单次计算,适合2D的元素渲染;而后者会保留每个layer的计算结果,然后再进行合成(Composition),适合3D的渲染,覆盖html5中的CSS 3D和WebGL等技术。而很多浏览器同时支持两种模式的渲染,平衡性能和内存等硬件消耗。
至此,整个webkit内核的加载渲染流程就基本解释完了,但实际上本篇内容还只是很粗浅的介绍。更多细节需要查阅相关资料,欢迎交流讨论。
网友评论