當下瀏覽器內核主要有 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 瀏覽器架構)安全
咱們一般說的瀏覽器內核,指的是渲染引擎。網絡
WebCore 基本是共享的,只是在不一樣瀏覽器中使用 Webkit 的實現方式不一樣。它包含解析 HTML 生成 DOM、解析 CSS、渲染布局、資源加載器等等,用於加載和渲染網頁。前端工程師
JS 解析可使用 JSCore 或 V8 等 JS 引擎。咱們熟悉的谷歌瀏覽器就是使用 V8。好比比較常見的有內置屬性 [[scope]]
就僅在 V8 內部使用,用於對象根據其向上索引自身不存在的屬性。而對外暴露的 API,如 __proto__
也可用於更改原型鏈。實際上 __proto__
並非 ES 標準提供的,它是瀏覽器提供的(瀏覽器能夠不提供,所以若是有瀏覽器不提供的話這也並非 b ug)。多線程
Webkit Ports 是不共享的部分。它包含視頻、音頻、圖片解碼、硬件加速、網絡棧等等,經常使用於移植。架構
同時,瀏覽器是多進程多線程架構,稍後也會細入。
在解析 HTML 文檔以前,須要先獲取資源,那麼資源的獲取在 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 解析。這就是瀏覽器的多線程架構。
總結一下,多線程的好處就是,高響應度,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 的解析。以後咱們再談一下,對於瀏覽器來講,它擁有哪些進程呢?
對於 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 進程等等。下面讓咱們詳細看看各大進程。
注意:若是頁面有 iframe,它會造成影子節點,會運行在單獨的進程中。
咱們僅僅在圍繞 Chromium 瀏覽器來講上述進程,由於在移動端,畢竟手機廠商不少,各大廠商對瀏覽器進程的支持也不同。這其實也是咱們最多見的 H5 兼容性問題,好比 IOS margin-bottom
失效等等。再好比 H5 使用 video 標籤作直播,也在不一樣手機之間會存在問題。有的手機直播頁面跳出主進程再回來,就會黑屏。
以 Chromium 的 Android 版爲例子,不存在 GPU 進程,GPU 進程變成了 Browser 進程的線程。同時,Renderer 進程演變爲服務進程,同時被限制了最大數量。
爲了方便起見,咱們以 PC 端谷歌瀏覽器爲例子,打開任務管理器,查看當前瀏覽器中打開的網頁及其進程。
當前我打開了 14 個網頁,不太好容易觀察,但能夠從下圖中看到,只有一個 Browser 進程,即第 1 行。可是打開的網頁對應的 Renderer 進程,並不必定是一個網頁對應一個 Renderer 進程,這跟 Renderer 進程配置有關係。好比你看第 六、7 行是每一個標籤頁建立獨立 Renderer 進程,可是藍色光標所在的第 八、九、10 行是共用一個 Renderer 進程,這屬於爲每一個頁面建立一個 Renderer 進程。由於第 九、10 行打開的頁面是從第 8 行點擊連接打開的。第 2 行的 GPU 進程也清晰可見,以及第 三、四、5 行的插件進程。
關於,Renderer 進程和打開的網頁並不必定是一一對應的關係,下面咱們詳細說一下 Renderer 進程。當前只有四種多進程策略:
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 指出修改)
你們也能夠關注個人公衆號《腦洞前端》獲取更多更新鮮的前端硬核文章,帶你認識你不知道的前端。