WebKit 资源加载机制
资源
HTML 支持的资源主要包括:HTML、JavaScript、CSS、图片、SVG、CSS Shader、视频、音频、字幕、字体、XSL样式表等
这些资源在 WebKit 中都有不同的类表示,公共基类是 CachedResource
。其中 HTML文本的类型为 MainResource
,对应的资源类型叫 CachedRawResource
类。
资源缓存
基本思想是建立一个缓存池,优先从缓存池中读取数据。这里的所说的缓存池是内存缓存。WebKit 从资源池中查找资源的关键词是URL,只要URL不同,就被认为是两个不同的资源
资源加载器
分为三种:
-
针对每种资源类型的特定加载器,仅加载某一种资源,并且没有公共基类,加载器属于它的调用者。如
ImageLoader
属于HTMLImageElement
-
缓存机制加载器
CachedResourceLoader
所有特定加载器都共享它来查找并插入缓存资源,属于 HTML 文档的对象 -
通用资源加载器
ResourceLoader
类,WebKit 需要从网络或者文件中获取资源的时候使用,只负责获取资源数据
过程
例子:有一个 “img” 元素,“src”是一个有效 URL 地址,当 HTML 解释器解析到该元素的该属性,WebKit 会创建一个 ImageLoader
对象来加载该资源,ImageLoader
创建一个缓存资源请求CachedResourceRequest
,并调用CachedResourceLoader
查找缓存资源,如果命中缓存则返回给调用者,如果没有命中则创建一个资源请求ResourceRequest
,并且调用通用资源加载器加载资源,具体到下面的 ResourceHandleInternal
,依赖于每个 WebKit 移植的实现策略。
存在默写资源会阻塞主线程渲染过程,当前的主线程渲染被阻塞时,WebKit 会启动另外一个线程去遍历后面的 HTML 网页,收集需要的资源 URL,并可以并发下载这些资源。
资源的生命周期
资源池使用 LRU(Least Recently Used 最近最少使用)算法,并且在这个基础上添加了协商缓存。如果命中缓存,那么发送一个 HTTP 请求给服务器,说明资源在本地的一些信息,如资源更新时间,服务器根据信息判断,如果没有更新,则返回状态码 304,那么直接使用原资源;否则下载最新资源。
Chromium 多进程资源加载
Renderer 进程在网页的加载过程中需要获取资源,但由于安全性(沙箱模型打开的时候,Renderer进程是没有权限获取资源的)和效率上(资源可以共享),Renderer 进程的资源获取实际上是通过进程间通信将任务交给 Browser 进程来完成。
网络栈
WebKit 的资源加载的优化其实是交由各个移植来实现的,WebCore 没有什么也别的基础设施,每个移植的网络实现是非常不一样的。
网络栈的基本组成
除了 HTTP 协议、DNS 解析等,还包含了 Chromium 为了减少网络时间而引入的新技术,例如 SPDY,QUIC
网络栈结构
-
URLRequest
类被调用时,会根据 URL 的 “scheme”(协议类型,如:“http://”,“file://”等) 来决定要创建什么类型的请求,Chromium 使用工厂模式处理不同类型的请求,例如 “http://” 类型则会使用URLRequestJobManager
创建一个URLRequestJob
类,具体使用哪个工厂则是一个责任链模式,优先判断是否是用户自定义的 “scheme” 。 - 当
URLRequestJob
被创建后,先从 Cookie 管理器中获取与该 URL 相关的信息,之后使用HttpTransactionFactory
对象创建HttpTransaction
对象开启一个 Http 连接的事务。如果请求对应的回复已经在磁盘缓存中,那么 Chromium 无需再建立HttpTransaction
-
HttpNetworkTransaction
使用HttpNetworkSession
类来管理会话。通过HttpStreamFactory
对象来建立 TCP Socket 连接。之后HttpStreamFactory
创建HttpStream
对象,来处理对象和网络之间数据的读写。 - Chromium 中与服务器建立连接的套接字是
SteamSocket
类,它是一个抽象类,再 POSIX 系统和 Windows 系统上有不同实现。
域名解析(DNS)
Chromium 使用 HostResolverImpl
类来解析域名,具体调用的是 “getaddrinfo()”,是一个阻塞式函数,所以使用单独的线程处理。为了考虑效率,使用 HostCache
类来保存解析后的域名,还有 DNS 预解析机制。
磁盘本地缓存
要求:
- 要有相应的机制来移除合适的缓存资源
- 浏览器崩溃时不破坏磁盘文件
- 可以通过同步或异步的方式高效快速的访问
- 避免同时存储相同的资源
- 操作一个项的时候不受其他请求影响
- 磁盘不支持多线程访问,磁盘的缓存操作要放到单独的线程
- 支持老版结构
实现上主要有两个类,Backend
(整个磁盘缓存) 和 Entry
(表中的表项)。至少需要一个索引文件和四个数据文件。索引文件用来索引,数据文件又称块文件。
索引文件:
包括一个索引头部和索引地址;头部用来表示该索引文件的信息(索引文件版本号、索引项数量、文件大小等信息);索引地址表保存各个表项对应的索引地址,直接将文件映射到内存地址。从内存地址可以找到数据文件,数据文件也是一个文件头加上后面的块文件,每个块的大小是固定的,当超过 512 字节的时候会为其分配多个块。但最多不超过四个,超过通常会用单独的文件存储。如果一个表项要分配四个块,那么是和块索引位置是对齐的(起始块的位置是4的倍数)
表项结构
分为两个部分,第一部分标记自己,包括元数据信息和自身内容。另一部分经常发生变动,主要为表项的回收算法服务,保存了回收算法所需的信息(LRU回收算法)。
高性能网络栈
DNS 预取和 TCP 预连接
一次 DNS 查询约 60~120ms,而 TCP 的三次握手也大约几十毫秒
DNS 预取:利用现有的 DNS 机制,提前解析网页中可能的连接。不是使用前面提到的 Chromium 网络栈,而是直接利用系统的域名解析机制,不会阻碍当前网络栈的工作,针对多个域名采取并行处理的方式,每个域名的解析由一个新线程处理,结束后退出。网页开发者可以显示指定哪些域名来让 Chromium 解析,使用方法:<link rel="dns-prefetch" href='"htttp://...">
。 用户地址栏也同理。
TCP 预链接:使用追踪技术获取用户从什么网页跳转到另一个网页,利用这些数据、和一些启发规则来 DNS 预取和 TCP 预链接,这对用户隐私是一个巨大挑战。
HTTP 管线化
HTTP1.1 开始增加了管线化技术,可以将多个 HTTP 请求一次性提交给服务器,无需等待服务器回复,因为它可能将多个HTTP 请求填充在一个 TCP 数据包内。能在高延迟的链接环境下有明显的性能提升。
局限性:需要通过永久连接完成,并且只有 GET 和 HEAD 等请求可以进行管线化。
SPDY
为了解决网络延迟和安全问题,根据 Google 官方数据,可以将网络加载时间减少 64%,HTTP2.0 将引入 SPDY 协议,将其作为基础来编写。
SPDY 协议是一种新的会话层协议,是一种栈式结构,被定义在 HTTP 协议和 TCP 协议之间,核心是多路复用,仅使用一个连接来传输一个网页中的众多资源。本质上并没有改变 HTTP 协议,只是将 HTTP 协议头通过 SPDY 来封装传输,数据传输方式也没有发生变化。所以比较容易部署,服务器只需要插入 SPDY 协议的解释层,从 SPDY 的消息头中获取各个资源的 HTTP 头即可。但 SPDY 必须建立在 SSL 层之上。
特征:
- 利用一个 TCP 连接来传输不限个数的资源请求的读写数据流,提高 TCP 连接的利用率,减少 TCP 连接的维护成本
- 可以调整这些资源请求的优先级
- 对请求使用压缩技术
- 可以尝试发送一些信息给浏览器,告诉浏览器后面可能需要哪些资源,更极端可以主动发送资源给浏览器。
QUIC
QUIC 是一种新的网络传输协议,目标是改进 UDP 数据协议的能力,解决传输层的传输效率,并提供数据的加密,SPDY 可以在 QUIC 上工作。