webkit源碼解讀系列-普及篇

前言

我們在做移動端的web開發的時候,無時無刻不在和瀏覽器打交道,而目前我們使用大多數手機瀏覽器都是基於webkit進行包裝完成的。在這種背景下,對我們開發人員來說,如果能更多的熟悉webkit的內部機制,瞭解基優缺點,那麼就能更好解決當前我們遇到的各種問題,包括,標準支持,兼容性,性能解決方案等。本文的目的就在於將通過對webkit的結構及相關解析流程上進行簡述與分解來幫助我們更好的理解這一開源框架。

Webkit是什麼?都有誰在用?

首先,引用webkit官方地話說就是“webkit並不是瀏覽器,而是一套針對web內容的渲染引擎”。Webkit所包含的WebCore排版引擎和JsCore引擎,均是從KDE的KHTML及KJS引擎衍生而來,但是webkitr 的優勢在於它的高效穩定,兼容性好,且源碼清晰,易於維護。

當前基於手機的瀏覽器中,大多數瀏覽器都是使用webkit做為渲染引擎,包括safari,qq, uc ,safari,chrome(js採用v8引擎)。特別是中國大多數的手機瀏覽器均是將webkit進行包裝山寨而成。除此之外,由於手機的本身內存及cpu等硬件的限制,導致基於手機的web實現,需要更多的考慮的兼容及性能問題。所以熟悉webkit對我們來說,也是一個門必選的功課。

Webkit的結構

webkit源碼解讀系列-普及篇


通過上圖我們可以看到,Webkit主要包括

  1. Webkit(向外部開放的API)

  2. Wekit2 為webkit提供的支持單獨進程模型的api層,將web內容的處理與應用程序分離,分別運行在不同的進程當中.

  3. Webcore(html,css,dom等元素的渲染引擎)

  4. JavaScriptCore(javascript解釋及執行引擎)

  5. Platform 主要是一些第三方的開源的類庫,包括網絡解析libcurl,圖片處理的libjpeg,libpng,xml處理libxml,libxslt,小型數據庫sqlite,除此之外還有一個二維的圖像處理cairo等.

  6. WTF主要是與操作系統本身的交互,調用操作系統接口為上層應用服務

Webkit的加載流程

webkit源碼解讀系列-普及篇


在頁面的加載過程中,首先,需要通過網絡請求加載頁面及圖片,腳本等附屬資源。然後由解釋器對頁面資源進行語法分析,最終生成一棵dom樹,在生成dom樹的同時,會對生成的dom節點通過render生成相應的render樹,遇到script時,也會通過javascriptcore對script進行解析執行.最終,再通過圖像處理模塊,生成可視的頁面. 在此節,我們將主要來著重介紹webcore中的渲染流程.

Webkit主要包含兩個加載管道,一個是用來加載documents到frames,另一個是用來加載附屬資源(如image,腳本).

  • FrameLoader 主要負責將documents加載到frames中,每當點擊一個超鏈接的時候,FrameLoader都會產生一個DocumentLoader,並將其狀態設置為”policy”,然後FrameLoader會等待webkit 客戶端做出一個如何來對待當前加載的決定。通常情況下,客戶端都會告訴FrameLoader,將當前的加載視為一個”navigation”. 一旦客戶告訴Frameloader,當前加載是一個“navigation”,FrameLoader就會將Document Loader的狀態更新為”provisional”,從而觸發網絡請求,並且等待一個是把當前的網絡請求視為下載還是新的document加載的決定.

Document Loader會創建一個叫做MainResourceLoader的對象.MainResourceLoader的主要工作就是與platform的網絡庫通信。之所以將MainResourceLoader從DcoumentLoader中分離出來主要有兩個原因:一是MainResourceLoader將Document Loader從複雜,繁瑣ResourceHandler返回的信息處理中隔離開來。二是將MainResourceLoader的生命週期從Document Loader的生命週期中脫離出來.

一旦加載系統從網絡中接收到了足夠的信息來做出判斷,認為當前的加載的資源就是一個document的呈現,FrameLoader就會將DocumentLoader狀態更新為“commited”。然後Frame就會將新的document展現出來.

  • DocLoader主要是用來加載圖片,腳本等附屬信息.我們下面以加載圖片為例來說明一下DocLoader的流程。

DocLoader首先會去查看cache裡是否有對當前請求圖片的緩存.如果有,那麼會立即從加載圖片(而且,為了加快效率,緩存的圖片一般都是已經經過解碼的)。反之,DocLoader會創建一個新的CacheImage對象來呈現圖片.CacheImage對象會要求Loader去發出網絡請求,同DocumentLoader,DocLoader會生成一個SubResourceLoader。這個對象與前文所提到的MainResourceLoader有相同的作用.

Dom元素渲染流程

Domrender

在webkit中,dom元素會被解析成Dom Tree.每一個元素(包括文字)都會有一個樹上的節點對應.根節點就是document.

Dom tree中的每個結點都對應一個叫做Renderobject 的類,這個類的作用就是用來描述當前dom節點的樣式,如高寬border等。而Renderobject同樣會組成棵樹被稱為RenderTree。每當DomTree結點中的一個節點創建後,RenderTree中就會有一個相應的結節被創建。因此DomTree與RenderTree從始至終會保持相同的結構.

當前有一個問題,RenderObject只能決定單個Dom元素的樣式,但是沒辦法決定Dom元素的位

webkit源碼解讀系列-普及篇

置及排版.

因此每一個RenderObject 又分別直接或者通過其父節點間接對應著一個RenderLayer.

在同一個座標空間中RenderObject通常屬於同一個RenderLayer. RenderLayer節點和Render 節點不是一一對應的,而是一對多的關係。那麼在什麼情況下RenderObject節點需要建立新的RenderLayer節點呢?

(1)當前節點是DOM樹的根節點,也就是document

(2)顯式設置css位置

(3)有透明效果的對象

(4)節點有overflow, alpha或者反射等效果

(5)使用了css filter

(6)Canvas 2D和3D(WebGL)

(7)Video節點對應的RenderObject對象

一個RenderLayer建立後,其所在的RenderObject對象的所有後代均包含在該RenderLayer,除非這些後代需要建立自己的RenderLayer.而後代的RenderLayer的父親就是自己最近的祖先所在的不同的RenderLayer.

每個RenderLayer對應的Render節點內容均會繪製在該RenderLayer所對應的層次上(或者內部存儲結構上)。不同的RenderLayer可以共享同一個內部存儲結構,也可以有各自的後端存儲,這取決於不同的移植。在軟件渲染下,通常各個RenderLayer的內容都繪製在同一塊後端存儲上。在GPU硬件加速下,某些RenderLayer可能有自己獨立的後端存儲,而後通過合成器來把這些不同的後端合成在一起,最終形成網頁的可視化內容.

RenderLayer在創建RenderObject對象的時候,如果需要,也會同時被創建,當然也有可能在執行Javascript代碼時,更新頁面的樣式從而需要新創建一個RenderLayer.

RenderLayer的子節點會以降序的方式在兩個列表中存放,一個是存放zindex小於0或者低於當前節點的zindex的子節點,一個存放高於當前節點zindex的子節點.

Dom元素的顯示

Dom元素的顯示有兩種方式,一種是基於軟件策略,一種是基於硬件加速。默認情況下是使用軟件策略。我們將在下面介紹軟件策略(硬件策略較複雜,在此略去)

dom元素的rending是按照從後到前的順序進行的。首先,會判斷當前的renderlayer是否存在相互交叉的情況,如果有,那麼,會先找到zindex低於當前layer的列表,然後將其rending出來,再rending當前的layer,最後rending zindex大於當前layer的列表。

webkit源碼解讀系列-普及篇

Renderobject會根自己的所具有的信息通過GraphicContext繪製成bitmap存放到共享內存中.然後browser process會通過系統的windows api將bitmap繪製到指定的窗口或者tab上去.

JavaScriptCore(Javascript執行過程)

JavaScriptCore主要模塊

Assembler

對各種開發環境的集成,如arm,Macro.x86(區別)

BytecodeCompiler

主要說明如何生成字節碼,以及字節碼的編譯情況

Bytecode

詳細說明了字節碼的具體內容,比如字節碼類型以及具體的計算過程

Interpreter

解析當前執行的腳本文件

JIT

即時編譯(just-in-time compilation),又稱動態轉譯,是一種通過在運行時將字節碼翻譯為機器碼,從而改善字節碼編譯語言性能的技術。即時編譯前期的兩個運行時理論是字節碼編譯和動態編譯。該目錄提供的功能是對腳本文件進行及時編譯。

Parser

對腳本文件進行詞法和語法分析

Runtime

運行時

JavaScript的解釋

webkit源碼解讀系列-普及篇

JavaScript底層是通過c++來實現的。Js本身的面向對象性並不強,主要是依靠原型鏈來實現對象和繼承。

事實上每一個Javascript對象都對應著一個c++的對象。c++中對Javascript實現卻充分表現出來了對象及繼承的特點。c++有一個基類叫做JsObject,c++中所有對Javascript的實現對象,都繼承了JsObject.

通過上圖,我們可以看到有兩個很重要的屬性proto_和prop_. proto_用來指向一個JSObject對象,即指明,一個Javascript對象的原型是誰。而prop_則是一個鍵值對的列表,主要說明當前的Javascript對象有哪一些屬性。

針對prop_ 和proto_我們來舉個簡單的例子來說明它的作用。我們寫了如下一段js代碼來獲取testObject這個對象下的value屬性

testObject.value

那麼在runtime中,就會查找testObject對應的prop_中是否存在value這樣一個健值對,有的話返回。如果沒有那麼會根據testObject的proto_去找到它所對應的原型對象的prop_中是存在,直到查找完所有的proto_及對應的prop_.

事實上屬性表的實現比較複雜,除了對象具有一般的屬性外,還可以通過 ClassInfo來提供一些初始化信息及類型信息。解釋器會區分對象一般的屬性及ClassInfo提供的屬性。如上例,如果value是一般的javascript的屬性,那麼會直接去鍵值對列表中查找,如果是由ClassInfo提供,那麼會調用callRuntimeMethod作為統一入口去查找.

webkit源碼解讀系列-普及篇

JavaScriptDOM元素的交互

Webkit是通過綁定(binding)機制來實現Javascrpt和dom的互操作的.binding主要涉及到三方面:一是javascript對dom的操作,二是dom觸發javascript的執行,三是javascript的執行環境,即在創建語法樹之前,如何初始化。(代碼Webcore/bindings/下)

上邊我們說到javascript在c++中都有一個繼承自JsObject的對象.為了能讓javascript操作dom,那麼我們就要實現一個針對dom元素的並且繼承自Jsobject的對象,這個對象,不僅僅包括javascript的一般的屬性和方法,它仍然需要支持對dom元素api的調用從而來實現對dom 的操作(創建,訪問期屬性,調用成員函數)。那麼我們就稱這一類對象為”影子對象”,而且在其構造函數執行之時,我們需要初始化一些信息,包括對dom的document類暴露出來的接口.

實際上,在運行時,並不是每一個dom元素都會有一個影子對象,影子對象只是在需要時才會建立。比如,我們要用js操作一個input組件獲取其值,那麼只有在調用document.getElementById(“”)之時,影子對象才會被建立。而且影子對象,並沒有做較多的事情,它的主要作用是對dom 的document類的接口做了一層包裝和調用。所以影子對象並不會對性能產生較大影響,主要的還是在控制dom元素對象的數量上.

Javascript響應dom事件

Dom觸發javascript是通過事件監聽(eventlistener)來完成的.dom對象會給js的影子對象提供add或者 remove Eventlistener的接口,這個應該是我們都比較瞭解的。前面提到,在解釋時會對javascript建立一棵語法樹,其中監聽器就對應著其中的一棵子樹,然後只需要將此子樹的根節點註冊到對應的dom節點上就行了。

DOM中在執行javascript語句之前,創建JSDOMWindow對象,它和DOM中的DOMWindow對象相對應。從 DoM進入javascript解釋器的入口。創建過程做兩件事:一是初始化大量的基本類型的原型(prototype),前面提到的原型鏈機制,二是創建了一個全局javascript對象JSDOmWindow,從而可以使得解釋器能從javascript進入到dom

JavaScript的垃圾回收

當一旦有新的對象被new出來時,webkit都會做一次回收,來保證有足夠的內存分配給新的對象。

webkit的垃圾回收目前主要是使用引用計數器。即判斷當前對象有沒有被引用,如果沒有那麼清理此對象。但是webkit的垃圾回收基於引用計數做了一定的優化,主要包括:protect和mark。

有一些全局對象,有可能當前並沒有被引用,但是將來可能會,所以這部分對象不應該被回收,那麼我就要在回收前放到protect集合中,還有一種是正處於當前局部區域中的棧對象集合,他們同樣不應該回收。那麼我們會在回收前針對protect及棧集合中的對象做mark,將其標識為不可回收對象。

除此之外,javascript針對dom元素生成的一些對象,在binding 中提供了另一種機制來幫助mark的順利執行.binding中維護了一張dom元素與jsdom的映射表。只要表中的dom存在,那麼在回收時,都應該將相應的jsdom做mark.

總結

從上述的各點,我們可以看到webkit具有較清晰的接口及結構,並且具有較強的跨平臺性。而且根據上面的分析,我們也能針對Javascript及dom展示的性能進行一定優化。主要是減少資源的請求,減少dom節點的創建,充分利用緩存機制.


分享到:


相關文章: