Webkit 核心初探

lucifer發表於2020-08-12
  • 作者: 阿吉
  • 校對&整理: lucifer

當下瀏覽器核心主要有 Webkit、Blink 等。本文分析注意是自 2001 年 Webkit 從 KHTML 分離出去並開源後,各大瀏覽器廠商魔改 Webkit 的時期,這些魔改的核心最終以 Chromium 受眾最多而脫穎而出。本文就以 Chromium 瀏覽器架構為基礎,逐層探入進行剖析。

引子

這裡以一個面試中最常見的題目從 URL 輸入到瀏覽器渲染頁面發生了什麼?開始。

這個很常見的題目,涉及的知識非常廣泛。大家可先從瀏覽器監聽使用者輸入開始,瀏覽器解析 url 的部分,分析出應用層協議 是 HTTPS 還是 HTTP 來決定是否經過會話層 TLS 套接字,然後到 DNS 解析獲取 IP,建立 TCP 套接字池 以及 TCP 三次握手,資料封裝切片的過程,瀏覽器傳送請求獲取對應資料,如何解析 HTML,四次揮手等等等等。 這個回答理論上可以非常詳細,遠比我提到的多得多。

本文試圖從瀏覽器獲取資源開始探究 Webkit。如瀏覽器如何獲取資源,獲取資源時 Webkit 呼叫了哪些資源載入器(不同的資源使用不同的載入器),Webkit 如何解析 HTML 等入手。想要從前端工程師的角度弄明白這些問題,可以先暫時拋開 C++原始碼,從瀏覽器架構出發,做到大致瞭解。之後學有餘力的同學再去深入研究各個底層細節。

本文的路線循序漸進,從 Chromium 瀏覽器架構出發,到 Webkit 資源下載時對應的瀏覽器獲取對應資源如 HTML、CSS 等,再到 HTML 的解析,再到 JS 阻塞 DOM 解析而產生的 Webkit 優化 引出瀏覽器多執行緒架構,繼而出於安全性和穩定性的考慮引出瀏覽器多程式架構。

一. Chromium 瀏覽器架構

Chromium瀏覽器架構

(Chromium 瀏覽器架構)

我們通常說的瀏覽器核心,指的是渲染引擎。

WebCore 基本是共享的,只是在不同瀏覽器中使用 Webkit 的實現方式不同。它包含解析 HTML 生成 DOM、解析 CSS、渲染布局、資源載入器等等,用於載入和渲染網頁。

JS 解析可以使用 JSCore 或 V8 等 JS 引擎。我們熟悉的谷歌瀏覽器就是使用 V8。比如比較常見的有內建屬性 [[scope]] 就僅在 V8 內部使用,用於物件根據其向上索引自身不存在的屬性。而對外暴露的 API,如 __proto__ 也可用於更改原型鏈。實際上 __proto__ 並不是 ES 標準提供的,它是瀏覽器提供的(瀏覽器可以不提供,因此如果有瀏覽器不提供的話這也並不是 b ug)。

Webkit Ports 是不共享的部分。它包含視訊、音訊、圖片解碼、硬體加速、網路棧等等,常用於移植。

同時,瀏覽器是多程式多執行緒架構,稍後也會細入。

在解析 HTML 文件之前,需要先獲取資源,那麼資源的獲取在 Webkit 中應該如何進行呢?

二.Webkit 資源載入

HTTP 是超文字傳輸協議,超文字的含義即包含了文字、圖片、視訊、音訊等等。其對應的不同檔案格式,在 Webkit 中 需要呼叫不同的資源載入器,即 特定資源載入器。

而瀏覽器有四級快取,Disk Cache 是我們最常說的通過 HTTP Header 去控制的,比如強快取、協商快取。同時也有瀏覽器自帶的啟發式快取。而 Webkit 對應使用的載入器是資源快取機制的資源載入器 CachedResoureLoader 類。

如果每個資源載入器都實現自己的載入方法,則浪費記憶體空間,同時違背了單一職責的原則,因此可以抽象出一個共享類,即通用資源載入器 ResoureLoader 類。 Webkit 資源載入是使用了三類載入器:特定資源載入器,資源快取機制的資源載入器 CachedResoureLoader 和 通用資源載入器 ResoureLoader

既然說到了快取,那不妨多談一點。

資源既然快取了,那是如何命中的呢?答案是根據資源唯一性的特徵 URL。資源儲存是有一定有效期的,而這個有效期在 Webkit 中採用的就是 LRU 演算法。那什麼時候更新快取呢?答案是不同的快取型別對應不同的快取策略。我們知道快取多數是利用 HTTP 協議減少網路負載的,即強快取、協商快取。但是如果關閉快取了呢? 比如 HTTP/1.0 Pragma:no-cache 和 HTTP/1.1 Cache-Control: no-cache。此時,對於 Webkit 來說,它會清空全域性唯一的物件 MemoryCache 中的所有資源。

資源載入器內容先到這裡。瀏覽器架構是多程式多執行緒的,其實多執行緒可以直接體現在資源載入的過程中,在 JS 阻塞 DOM 解析中發揮作用,下面我們詳細講解一下。

三.瀏覽器架構

瀏覽器是多程式多執行緒架構。

對於瀏覽器來講,從網路獲取資源是非常耗時的。從資源是否阻塞渲染的角度,對瀏覽器而言資源僅分為兩類:阻塞渲染如 JS 和 不阻塞渲染如圖片。

我們都知道 JS 阻塞 DOM 解析,反之亦然。然而對於阻塞,Webkit 不會傻傻等著浪費時間,它在內部做了優化:啟動另一個執行緒,去遍歷後續的 HTML 文件,收集需要的資源 URL,併發下載資源。最常見的比如<script async><script defer>,其 JS 資源下載和 DOM 解析是並行的,JS 下載並不會阻塞 DOM 解析。這就是瀏覽器的多執行緒架構。

JS async defer

總結一下,多執行緒的好處就是,高響應度,UI 執行緒不會被耗時操作阻塞而完全阻塞瀏覽器程式。

關於多執行緒,有 GUI 渲染執行緒,負責解析 HTML、CSS、渲染和佈局等等,呼叫 WebCore 的功能。JS 引擎執行緒,負責解析 JS 指令碼,呼叫 JSCore 或 V8。我們都知道 JS 阻塞 DOM 解析,這是因為 Webkit 設計上 GUI 渲染執行緒和 JS 引擎執行緒的執行是互斥的。如果二者不互斥,假設 JS 引擎執行緒清空了 DOM 樹,在 JS 引擎執行緒清空的過程中 GUI 渲染執行緒仍繼續渲染頁面,這就造成了資源的浪費。更嚴重的,還可能發生各種多執行緒問題,比如髒資料等。

另外我們常說的 JS 操作 DOM 消耗效能,其實有一部分指的就是 JS 引擎執行緒和 GUI 渲染執行緒之間的通訊,執行緒之間比較消耗效能。

除此之外還有別的執行緒,比如事件觸發執行緒,負責當一個事件被觸發時將其新增到待處理佇列的隊尾。

值得注意的是,多啟動的執行緒,僅僅是收集後續資源的 URL,執行緒並不會去下載資源。該執行緒會把下載的資源 URL 送給 Browser 程式,Browser 程式呼叫網路棧去下載對應的資源,返回資源交由 Renderer 程式進行渲染,Renderer 程式將最終的渲染結果返回 Browser 程式,由 Browser 程式進行最終呈現。這就是瀏覽器的多程式架構。

多程式載入資源的過程是如何的呢?我們上面說到的 HTML 文件在瀏覽器的渲染,是交由 Renderer 程式的。Renderer 程式在解析 HTML 的過程中,已蒐集到所有的資源 URL,如 link CSS、Img src 等等。但出於安全性和效率的角度考慮,Renderer 程式並不能直接下載資源,它需要通過程式間通訊將 URL 交由 Browser 程式,Browser 程式有許可權呼叫 URLRequest 類從網路或本地獲取資源。

近年來,對於有的瀏覽器,網路棧由 Browser 程式中的一個模組,變成一個單獨的程式。

同時,多程式的好處遠遠不止安全這一項,即沙箱模型。還有單個網頁或者第三方外掛的崩潰,並不會影響到瀏覽器的穩定性。資源載入完成,對於 Webkit 而言,它需要呼叫 WebCore 對資源進行解析。那麼我們先看下 HTML 的解析。之後我們再談一下,對於瀏覽器來說,它擁有哪些程式呢?

四.HTML 解析

對於 Webkit 而言,將解析半結構化的 HTML 生成 DOM,但是對於 CSS 樣式表的解析,嚴格意義 CSSOM 並不是樹,而是一個對映表集合。我們可以通過 document.styleSheets 來獲取樣式表的有序集合來操作 CSSOM。對於 CSS,Webkit 也有對應的優化策略---ComputedStyle。ComputedStyle 就是如果多個元素的樣式可以不經過計算就確認相等,那麼就僅會進行一次樣式計算,其餘元素僅共享該 ComputedStyle。

共享 ComputedStyle 原則:

(1) TagName 和 Class 屬性必須一樣。

(2)不能有 Style。

(3)不能有 sibling selector。

(4)mappedAttribute 必須相等。

對於 DOM 和 CSSOM,大家說的合成的 render 樹在 Webkit 而言是不存在的,在 Webkit 內部生成的是 RenderObject,在它的節點在建立的同時,會根據層次結構建立 RenderLayer 樹,同時構建一個虛擬的繪圖上下文,生成視覺化影像。這四個內部表示結構會一直存在,直到網頁被銷燬。

RenderLayer 在瀏覽器控制檯中 Layers 功能卡中可以看到當前網頁的圖層分層。圖層涉及到顯式和隱式,如 scale()、z-index 等。層的優點之一是隻重繪當前層而不影響其他層,這也是 Webkit 做的優化之一。同時 V8 引擎也做了一些優化,比如說隱藏類、優化回退、內聯快取等等。

五.瀏覽器程式

瀏覽器程式包括 Browser 程式、Renderer 程式、GPU 程式、NPAPI 外掛程式、Pepper 程式等等。下面讓我們詳細看看各大程式。

  • Browser 程式:瀏覽器的主程式,有且僅有一個,它是程式祖先。負責頁面的顯示和管理、其他程式的管理。
  • Renderer 程式:網頁的渲染程式,可有多個,和網頁數量不一定是一一對應關係。它負責網頁的渲染,Webkit 的渲染工作就是在這裡完成的。
  • GPU 程式:最多一個。僅當 GPU 硬體加速被開啟時建立。它負責 3D 繪製。
  • NPAPI 程式:為 NPAPI 型別的外掛而建立。其建立的基本原則是每種型別的外掛都只會被建立一次,僅當使用時被建立,可被共享。
  • Pepper 程式:同 NPAPI 程式,不同的是 它為 Pepper 外掛而建立的程式。
注意:如果頁面有 iframe,它會形成影子節點,會執行在單獨的程式中。

我們僅僅在圍繞 Chromium 瀏覽器來說上述程式,因為在移動端,畢竟手機廠商很多,各大廠商對瀏覽器程式的支援也不一樣。這其實也是我們最常見的 H5 相容性問題,比如 IOS margin-bottom 失效等等。再比如 H5 使用 video 標籤做直播,也在不同手機之間會存在問題。有的手機直播頁面跳出主程式再回來,就會黑屏。

以 Chromium 的 Android 版為例子,不存在 GPU 程式,GPU 程式變成了 Browser 程式的執行緒。同時,Renderer 程式演變為服務程式,同時被限制了最大數量。

為了方便起見,我們以 PC 端谷歌瀏覽器為例子,開啟工作管理員,檢視當前瀏覽器中開啟的網頁及其程式。

開啟瀏覽器工作管理員

當前我開啟了 14 個網頁,不太好容易觀察,但可以從下圖中看到,只有一個 Browser 程式,即第 1 行。但是開啟的網頁對應的 Renderer 程式,並不一定是一個網頁對應一個 Renderer 程式,這跟 Renderer 程式配置有關係。比如你看第 6、7 行是每個標籤頁建立獨立 Renderer 程式,但是藍色游標所在的第 8、9、10 行是共用一個 Renderer 程式,這屬於為每個頁面建立一個 Renderer 程式。因為第 9、10 行開啟的頁面是從第 8 行點選連結開啟的。第 2 行的 GPU 程式也清晰可見,以及第 3、4、5 行的外掛程式。

瀏覽器程式

關於,Renderer 程式和開啟的網頁並不一定是一一對應的關係,下面我們詳細說一下 Renderer 程式。當前只有四種多程式策略:

  1. Process-per-site-instance: 為每個頁面單獨建立一個程式,從某個網站開啟的一系列網站都屬於同一個程式。這是瀏覽器的預設項。上圖中的藍色游標就是這種情況。
  2. Process-per-site:同一個域的頁面共享一個程式。
  3. Process-per-tab:為每個標籤頁建立一個獨立的程式。比如上圖第 6、7 行。
  4. Single process:所有的渲染工作作為多個執行緒都在 Browser 程式中進行。這個基本不會用到的。

Single process 突然讓我聯想到零幾年的時候,那會 IE 應該還是單程式瀏覽器。單程式就是指所有的功能模組全部執行在一個程式,就類似於 Single process。那會玩 4399 如果一個網頁卡死了,沒響應,點關閉等一會,整個瀏覽器就崩潰了,得重新開啟。所以多程式架構是有利於瀏覽器的穩定性的。雖然當下瀏覽器架構為多程式架構,但如果 Renderer 程式配置為 Process-per-site-instance,也可能會出現由於單個頁面卡死而導致所有頁面崩潰的情況。

故瀏覽器多程式架構綜上所述,好處有三:

(1)單個網頁的崩潰不會影響這個瀏覽器的穩定性。

(2)第三方外掛的崩潰不會影響瀏覽器的穩定性。

(3)沙箱模型提供了安全保障。

總結

Webkit 使用三類資源載入器去下載對應的資源,並存入快取池中,對於 HTML 文件的解析,在阻塞時呼叫另一個執行緒去收集後續資源的 URL,將其傳送給 Browser 程式,Browser 程式呼叫網路棧去下載對應的本地或網路資源,返回給 Renderer 程式進行渲染,Renderer 程式將最終渲染結果(一系列的合成幀)傳送給 Browser 程式,Browser 程式將這些合成幀傳送給 GPU 從而顯示在螢幕上。
(文中有部分不嚴謹的地方,已由 lucifer 指出修改)

大家也可以關注我的公眾號《腦洞前端》獲取更多更新鮮的前端硬核文章,帶你認識你不知道的前端。

相關文章