Chrome 的多程式架構
程式 & 執行緒
在談瀏覽器多程式架構之前,我們先聊聊程式和執行緒的概念。
程式是系統進行資源排程和分配的的基本單位,一個程式可以認為是一個程式的執行例項。啟動一個程式的時候,作業系統會為該程式建立一塊記憶體,用來存放程式碼、執行中的資料和一個執行任務的主執行緒,我們把這樣的一個執行環境叫程式。
執行緒是依附於程式的,是作業系統可識別的最小執行和排程單位,而程式中使用多執行緒並行處理能提升運算效率,多執行緒可以並行處理任務,但是執行緒是不能單獨存在的,它是由程式來啟動和管理的。我們最熟悉的 JS 的執行就是執行緒這個維度。
下面是程式和執行緒間的一些區別:
- 一個執行緒依附於一個程式,一個程式可以有多個執行緒;
- 執行緒之間共享程式中的資料。
- 程式中的任意一執行緒執行出錯,都會導致整個程式的崩潰,而程式之前不會相互影響。
- 當一個程式關閉之後,作業系統會回收程式所佔用的記憶體。
- 程式之間的內容相互隔離,只能通過 IPC 通訊。
我覺得知乎上一個用火車比喻程式和執行緒的例子比較形象:程式好比火車,執行緒好比車廂,一輛火車有多節車廂,不同的火車之間互不干擾,但是一節車廂失火會殃及多節車廂。
Chrome 架構
Chrome 瀏覽器包括:1 個 Browser 主程式、1 個 GPU 程式、1個 Utility 程式、多個 Renderer 程式和多個 Plugin 程式。
- Browser 主程式(瀏覽器程式):主要負責介面顯示、使用者互動、子程式管理,同時還包含網路請求和檔案訪問等。
- GPU 程式:與其他程式隔離處理 GPU 任務。
- Renderer 程式(渲染程式):核心任務是將 HTML、CSS 和 JavaScript 轉換為使用者可以與之互動的網頁,排版引擎 Blink 和 JavaScript 引擎 V8 都是執行在該程式中,預設情況下,Chrome 會為每個 Tab 標籤建立一個渲染程式。出於安全考慮,渲染程式都是執行在沙箱模式下。
- Plugin 程式:主要是負責外掛的執行,因外掛易崩潰,所以需要通過外掛程式來隔離,以保證外掛程式崩潰不會對瀏覽器和頁面造成影響。
瀏覽器渲染機制
渲染程式的核心工作是將 HTML、CSS 和 JavaScript 轉換為使用者可以與之互動的網頁。在這個工作過程中,輸入的 HTML 經過一些子階段,最後輸出畫素。按照渲染的時間順序,這些子階段大致可分為:構建 DOM 樹、計算樣式、佈局、分層、繪製、分塊、柵格化和合成。
構建 DOM 樹
當渲染程式開始接收 HTML 資料時,主執行緒開始解析 HTML 並將其轉換為瀏覽器能夠理解的 DOM 樹結構。
在 DOM 樹的解析過程中,如果遇到 img、css 或者 js 資源時,主執行緒會向 Browser 主程式的網路執行緒傳送請求以獲取對應的資源。
當解析的過程中遇到 <script>
標籤時,主執行緒會暫停 HTML 的解析,從而進行 js 程式碼的載入、解析和執行。因為 js 程式碼中可能涉及對頁面結構的修改,主執行緒必須等待 js 執行才能恢復對 HTML 文件的解析。因此我們可以通過在 <script>
標籤上加上 async 或者 defer 屬性來非同步載入執行 js 程式碼,避免 js 阻塞 HTML 的解析。
樣式計算
樣式計算的目的是為了計算出 DOM 節點中每個元素的具體樣式,在計算過程中需要遵守 CSS 的繼承和層疊兩個規則,這個階段大體可分為三步來完成:
- 把 CSS 轉換為瀏覽器能夠理解的結構:當渲染引擎接收到 CSS 文字時,會執行一個轉換操作,將 CSS 文字轉換為瀏覽器可以理解的結構——StyleSheets;
- 轉換樣式表中的屬性值,使其標準化:例如 rem 這些屬性,需要將所有值轉換為渲染引擎容易理解的、標準化的計算值;
- 計算出 DOM 樹中每個節點的具體樣式
在瀏覽器中,我們可以通過 Computed 皮膚檢視當前節點的 Computed Style。
佈局
有了 DOM 樹和 DOM 對應的 Computed Style 之後還不足以顯示頁面,接下來還需要計算出 DOM 樹中可見元素的幾何位置,這個計算過程叫做佈局。
Chrome 在佈局階段需要完成兩個任務:建立佈局樹和佈局計算。
- 建立佈局樹:瀏覽器會遍歷 DOM 樹中的所有可見節點,並把這些節點加到佈局樹中,而不可見的節點會被佈局樹忽略掉,如圖中 span 這個元素被設定為 dispaly:none,這個元素會被佈局樹忽略;
- 佈局計算:有了佈局樹後,瀏覽器會計算佈局節點的座標位置;
分層
有了佈局樹後,對於一些簡單頁面已經具備繪製條件了,但是對於我們現代的頁面來說,有很多複雜的效果,如一些複雜的 3D 轉換、 z-index 做 z 軸排序等,對於這些場景為了頁面展示的正確性,渲染引擎還會為特定的節點生成專用的圖層,並生成一棵對應的圖層樹。
需要注意的是,並不是每個節點都包含一個圖層,如果一個節點沒有對應的層,那麼這個節點就從屬於父節點所在的圖層。最終每一個節點都會直接或者間接地從屬於一個圖層。
通常滿足下面兩點便會提升為一個單獨的圖層:
- 擁有層疊上下文屬性的元素會被提升為單獨的一層。層疊上下文
- 需要剪裁的地方也會被建立為圖層(overflow)。
我們可以在瀏覽器的 layers 皮膚看到當前頁面的分層情況:
繪製
在確定了 DOM 樹、計算樣式以及佈局樹仍然不足以繪製頁面,這裡還需要有明確的繪製順序,在此過程中主執行緒會遍歷佈局樹並建立繪製記錄。
同樣,我們可以在瀏覽器的 layers 皮膚看到當前頁面對應圖層的繪製記錄:
光柵化(柵格化)
在確定了佈局樹並建立了圖層以及對應的繪製順序之後,主執行緒會將資訊提交給合成執行緒。合成執行緒會去光柵化每一個圖層。
由於我們瀏覽器的視口是有限的,但是頁面的長度可能很長,有些圖層可能超過視口很多,而使用者對頁面的感知是視口維度的,一次性渲染整個圖層未免有些浪費,因此合成執行緒會對圖層進行分塊處理。
有了圖塊之後,合成執行緒會將每一個圖塊傳送到光柵執行緒(Raster thread),光柵執行緒會光柵化每一個圖塊並存在 GPU 記憶體中。在這個過程中,合成執行緒會優先選擇視口內的圖塊提交給光柵執行緒。
合成和展示
光柵化完成後,合成執行緒會建立合成幀通過 IPC 通訊提交給瀏覽器程式。瀏覽器程式接收到指令後會將內容繪製在記憶體中並展示在螢幕上。
至此,從接收 HTML 資料到頁面的展示全流程就結束了。下面我們再結合瀏覽器的渲染流程看下什麼是重排、重繪和合成。
重排、重繪和直接合成
重排
當我們通過 js 或者 css 屬性更新了元素的幾何屬性,例如元素的寬度、高度,此時瀏覽器會重新觸釋出局並重新執行後面全部的渲染流程,因此,重排的開銷是最大的。
重繪
當我們通過 js 或者 css 更新元素的繪製屬性,例如元素的背景色、文字的顏色等,此時佈局和分層階段被省略,只執行後續的流程,因此重繪的開銷相比重排會小很多。
直接合成
為什麼我們為了避免重排和重繪而去採用 css3 的 transform 等屬性呢?因為此時整個主執行緒的流程會被全部跳過,執行後續的流程,而後續的流程交給了在執行執行緒、光柵執行緒和 GPU 程式上執行沒有佔據主執行緒的資源,因此效率是最高的。