CHROME源碼剖析 下《轉》

轉自 http://www.blogjava.net/xiaomage234/archive/2012/02/16/370123.html

【四】Chrome的UI繪製

1. Chrome的窗口控件

Chrome提供了自己的一個UI控件庫,相關文檔可以參見這裏。用Chrome自己的話來說,我覺得市面上的七葷八素的圖形控件庫都不好用,於是自己倒騰倒騰實現了一套。。。
廣告雖如此說,不過,Chrome的圖形控件結構,我還未發現有啥非常非常特別的地方。Chrome的窗口、按鈕、菜單之類的控件,都直接或間接派生自View,這個是控件基類。Chrome的View具有樹形結構,其內部有一個子View數組,由此構成一個控件常用的組合模式。。。

有一個比較特殊的View子類,叫做RootView,顧名思義,它是整個View控件樹的根,在Chrome中,一個正確的樹形的控件結構,必須由RootView作爲根。之所以要這樣設計,是因爲RootView有一個比較特殊的功能,那就是分發消息。。。

我們知道,一般的Windows控件,都有一個HWND,用與佔據一塊屏幕,捕獲系統消息。Chrome中的View只是保存控件相關信息和繪製控件,裏面沒有HWND句柄,因此不能夠捕獲系統消息。在Chrome中,完整的控件架構是這樣的,首先需要有一個ViewContainer,它裏面包含一個RootView。ViewContainer是一個抽象類,在Window中的一個子類是HWNDViewContainer,同時,HWNDViewContainer還是MessageLoopForUI::Observer的子類。如果你看過本文第一部分描述的線程通信的內容的話,你就應該還記得,Observer是用於監聽本線程內系統消息的東東。。。

當有系統消息進入此線程消息循環後,HWNDViewContainer會監聽到這個情況,如果和View相關的消息,它就會調用RootView的相關方法,傳遞給控件。在 RootView的內部,會遍歷整個控件樹上的控件,將消息傳遞給各個控件。當然,有的消息是可以獨佔的,比如鼠標移動發送在某個View所管轄的範圍內,它會告知RootView(通過方法的返回值…),這個消息我要了,那麼RootView會停止遍歷。。。

在設計的時候,View對消息的處理,採取的是大而全的接口模式。 就是說在View內部,提供了所有可能的消息處理接口,並提供了默認實現,所有子類只需要覆蓋自己需要的消息處理函數即可。如果對MFC的消息映射有了解 的話,可以知道兩者的區別。MFC在設計的時候,覺得無法提供大而全的接口,因爲消息總類實在太多,而且還是可擴展的,於是就有了消息映射着一套繁瑣的宏。但Chrome的圖形框架,顯然沒有做一個通用的Framework的打算,因此,可以採用這樣的策略,使得子類的派生變得簡單而自然。。。

每一個View的子類控件,比如Button之類的,會存儲一些數據,根據消息做一些行爲,並且繪製出自己。在Chrome中,畫圖的東西是ChromeCanvas這個類,在其內部,通過Skia和GDI實現繪製。 Skia是Android團隊開發的一個跨平臺的圖形引擎,在Chrome中負責除了文字之外,所有內容的繪製;而文字繪製的重擔,在Windows中交到了GDI的手上。這樣的設計會給跨平臺帶來一些困難,估計是由Skia實現文本繪製會比較繁瑣,纔會帶出如此一個設計的模式。。。

另外一個歷史遺留產物,就是在Windows下的圖形控件,還有一些是原生的,就是說帶有HWND那種傳統的控件,這是Chrome身上不多的趕工期的痕跡,隨着時間的寬裕,這樣的原生控件會被淘汰進歷史的垃圾箱,而全部變爲從View派生的控件。。。

其實,對於Chrome這套控件架構我還沒算摸得很熟悉,估計等到做一次插件之後會了解的更透徹,因此,只說了點皮毛,聊表心意。。。

2. Chrome的頁面加載和繪製

上面這些UI控件,都是用在窗口上的(比如瀏覽器的外框,菜單,對話框之類的…)。我們在瀏覽器中看到的大部分內容,是網頁頁面。頁面的繪製(繪製,就是把一個HTML文件變成一個活靈活現的頁 面展示的過程…),只有一半輪子是Chrome自己做的,還有一部分來自於WebKit,這個Apple打造的Web渲染器。。。

之所以說是一半輪子來源於WebKit,是因爲WebKit本身包含兩部分主要內容,一部分是做Html渲染的,另一部分是做JavaScript解析的。在Chrome中,只有Html的渲染採用了WebKit的代碼,而在JavaScript上,重新搭建了一個NB哄哄的V8引擎。目標是,用WebKit + V8的強強聯手,打造一款上網衝浪的法拉利,從效果來看,還着實做的不錯。。。

不過,雖說Chrome和WebKit都是開源的,並聯手工作。但是,Chrome還是刻意的和WebKit保持了距離,爲其始亂終棄埋下了伏筆。Chrome在WebKit上封裝了一層,稱爲 WebKit Glue。Glue層中,大部分類型的結構和接口都和WebKit類似,Chrome中依託WebKit的組件,都只是調用WebKit Glue層的接口,而不是直接調用WebKit中的類型。按照Chrome自己文檔中的話來說,就是,雖然我們再用WebKit實現頁面的渲染,但通過這 個設計(加一個間接層…)已經從某種程度大大降低了與WebKit的耦合,使得可以很容易將WebKit換成某個未來可能出現的更好的渲染引擎。。。

重用

在《夢斷代碼》中,有一坨調侃重用的文字。他覺着軟件重用的困難一方面來自於場景本身很多變,很難設計出一套包羅萬象的東西;另一方面來自於人,程序員總是瞅着別人寫的代碼不順眼,總喜歡自己寫一套。。。

於是,解決重用這個問題也就只有兩種,寫最NB人見人服無所不能的代碼,或者是有很多很多NB代碼共君任選。Google無疑在這兩個方面做得都不錯,Map/Reduce,Big Table之類的一套東西,強大到可以適合太多的場景,大大簡化了N多上層應用的開發。而對開源的利用使用,使得其可以隨意挑一個巨人站到他肩膀上跳舞,每看到這種場景,MS估計都會氣得拍着胸口吐血。。。

Google本身在服務端的基礎底層,有很深積累,隨着Chrome,Android等等客戶端應用的開發,客戶端的積累也逐步提升,也許,擁抱開源纔是MS的正道?。。。

當你鍵入一個Url並敲下回車後,Chrome會在Browser進程中下載Url對應的頁面資源(包括Web頁面和Cookie),而不是直接將Url發送給Render進程讓它們自行下載(你會越來越發現,Render進程絕對是100%的名符其實,除了繪製,幾乎啥多餘的事情都不會幹的…)。與各個Render進程各自爲站,各自管好自己所需的資源相比,這種策略彷彿會增加大量的進程間通信。之所以採用,按照這篇文檔的 解釋,主要有三個優點,一個是避免子進程與網絡通信,從而將網絡通信的權限牢牢握在主進程手中,Render進程能力弱了,想造反幹壞事的可能性就降低了 (可以更好控制各個Render進程的權限…);另一個是有利於Cookie等持久化資源在不同頁面中的共享,否則在不同Render進程中傳遞 Cookie這樣的事情,做起來更麻煩;還有一點很重要的,是可以控制與網絡建立HTTP連接的數量,以Browser爲代表與網絡各方進行通信,各種優化策略都比較好開展(比如池化)。。。

當然,在Browser進程中進行統一的資源管理,也就意味着不再方便用WebKit進行資源下載(WebKit當然有此能力,不過再次被Chrome拋棄了…),而是依託WinHTTP來做的。WinHTTP在接受數據的過程中,會不停的把數據和相關的消息通過IPC,發送給負責繪製此頁面的Render進程中對應的RenderView。在這裏,路由消息中的那個ID值起了關鍵的作用,系統依照此ID,能夠準確的將相關的消息發送到相關的View頭上,這玩意發錯了地方還真不是和有人把錢錯到你賬戶上一樣,因爲錯收的進程基本上無福消受這個意外來客,輕者頁面顯示混亂,重者消化不良直接噎死。。。

RenderView接收到頁面信息,會一邊繪製一邊等待更多的資源到來,在用戶看來,所請求的頁面正在一點一點顯示出來。當然,如果是一個通知傳輸開始、傳輸結束這樣的消息,通過序列化到消息參數裏面,經由IPC發過來,代價還是可以承受的,但是,想資源內容這樣大段大段的字節流,如果通過消息發過來,浪費兩邊進程大量空間和時間,就不合適了。於是這裏用到了共享內存。Browser進程將下載到的資源寫到共享內存中,並將共享內存的句柄和共享區域的大小序列化在消息中發送給Render進程。Render進程拿到這個句柄,就可以通過它訪問到共享內存相關的區域,讀取信息並進行繪製。通過這樣的方式,即享用到了統一資源管理的優點,由避免了很高的進程通信開銷,左右逢源,好不快活。。。

3. Chrome頁面的消息響應

Render進程是一個嬌生慣養的進程,這一點從上面一段已經可以看出來了。它自己的資源它自己都不下載,而是由Browser進程來幫忙。不過Render進程也許比你想象的還要懶惰一些,它不但不自己下載資源,甚至,連自己的系統消息都不接收。。。

Render進程中不包含HWND,當你鼠標 在頁面上劃來劃去,點上點下,這些消息其實都發到了Browser進程,它們擁有頁面呈現部分的HWND。Browser會將這些消息轉手通過IPC發送 給對應的Render進程中的RenderView,很多時候WebKit會處理此類消息,當它發現出現了某種值得告訴Browser進程的事情,它會組個報回贈給Browser進程。舉個例子,你打開一個頁面,然後拿鼠標在頁面上亂晃。Browser這時候就像一個碎嘴大嬸,不厭其煩的告訴Render 進程,“鼠標動了,鼠標動了”。如果Render對這個信息無所謂,就會很無聊的應答着:“哦,哦”(發送一個回包…)。但是,當鼠標劃過鏈接的時候,矜持的Render進程坐不住了,會大聲告訴Browser進程:“換鼠標,換鼠標~~”,Browser聽到後,會將鼠標從箭頭狀換成手指狀,然後繼續以上過程。。。

比較麻煩的是Paint消息,重新繪製頁面是一個太頻繁發生的事情,不可能重繪一次就序列化一坨字節流過去。於是策略也很清楚了,就是依然用共享內存讀寫,用消息發句柄。在Render進程中,會有一個共享內存池(默認值爲2…),以size爲key,以共享內存爲值,簡單的先入先出淘汰算法,利用局部性的特徵,避免反覆的創建和銷燬共享內存 (這和資源傳遞不一樣,因爲資源傳遞可以開一塊固定大小的共享內存…)。Render進程從共享內存池中拿起一塊(二維字節數組…),就好像拿着一塊屏幕似的,拼了命往上繪製,爲了讓Render安心覺着有成就感,Browser會偷偷幫Render把這些內容繪製到屏幕上,造成Render進程直接繪製屏幕的假象。這可就苦了屏幕取詞的工具們,因爲在HWND上壓根就沒啥字符信息,全部就是一坨圖像而已,啥也取不着。於是Google金山詞霸, 網易有道詞霸各自發揮智慧,另闢蹊徑,也算是都利用Chrome做了一把廣告。。。
爲什麼不讓Render進程自己擁有HWND,自己管理自己的消息,既快捷又便利。在Chrome的官方Blog上,有一篇解釋的文章, 基本上是這個意思,速度是必須快的髮指的,但是爲了用戶響應,放棄一些速度是必要的,畢竟,沒有人喜歡總假死的瀏覽器。在Browser進程中,基本上是杜絕任何同步Render進程的工作,所有操作都是異步完成。因爲Render進程是不靠譜的,隨時可能犧牲掉,同步它們往往導致主進程停止響應,從而導致整個瀏覽器停下來甚至掛掉,這個代價是不可以容忍的。但是,Windows有一個惡習,喜歡往整個HWND繼承體系中發送同步消息(我不是很清楚這個狀況,有人能解釋麼?…),這時候,如果HWND在Render進程中,就務必會導致主進程與Render進程的同步,Chrome無法控制 Windows,於是,它們只能夠控制Render,把它們的HWND搬到主進程中,避免同步操作,換取用戶響應的速度。。。

4. 結論

整個Chrome的UI架構,就是一個權責分配的問題。可以把Browser進程看成是一個類似於朱元璋般的勤勞皇帝(詳見《明朝那些事 一》…),把大多數的權利都牢牢把握在手中,這樣,雖然Browser很操勞,但是整體上的協調和同步,都進行的非常順暢。Render進程就是皇帝手下的傀儡宰相們,只負責自己的一畝三分地,聽從皇帝的調配即可。這這樣的環境下,Render進程的生死變得無足輕重,Render的死亡,只是少了一個繪製頁面的工具而已,其他一切如故。通過控制權力,換取天下太平,這招在coding界,同樣是一個不錯的策略,但是,唯一的意外來自於Plugin。 按照規範,Chrome的Plugin是可以創立窗口的(HWND),這必然導致同步問題,Chrome沒有辦法通過控制權力的方式解決這個問題,只能想些別的亡羊補牢的招來搞定。。。

【五】 Chrome的插件模型

1. NPAPI

爲了緊密的與各個開源瀏覽器團結起來,共同抗擊IE的壟斷,Chrome的插件,也遵循了NPAPI(Netscape Plugin Application Programming Interface)標準,支持這個標準的瀏覽器需要實現一組規定的API供插件調用,這組API形如NPN_XXX,比如NPN_GetURL,插件可以利用這些API進行二次開發。而NPAPI插件以一個Dll之類的作爲物理載體(windows下dll,linux下是so…)進行提供,裏面同樣也實現了一組規定的API。形式包括NP_XXX和NPP_XXX,NP_XXX是系統需要默認調用的方法,用於認知這個插件,比如NP_Initialize, 而NPP_XXX是用於插件完成一些實際功能,比如NPP_New。。。
所有的插件dll都需要放置在指定目錄下(根據操作系統的不同而不同…),每個插件可以處理一種或多種MIME格式的數據,比如application/pdf,說明該插件可以處理pdf相關的 文檔。在Chrome中鍵入about:plugins,可以查看當前Chrome中具有的插件信息。。。
NPAPI是一個很經典的插件方案,用dll進行注入,用協定的API進行通信,用字符串描述插件能力。 插件宿主(在這裏就是瀏覽器…),會根據能力描述,動態加載插件,並負責插件調用的流程和生命週期管理。而插件中,負責真實邏輯的處理,並可以構造 UI與用戶交流。以此類方式實現的插件系統,往往是處理的邏輯比較固定適用範圍一般(用API寫死了邏輯…),但可擴展性不錯(用字符串描述能力,可 無限擴展…)。。。
在Chrome中nphostapi.h中,定義了所有NPAPI相關的函數指針和結構,這個文件放置在glue目錄下,如果看過前面碰過的文章就知道,在WebKit內肯定也有一套相同的東西;在npapi.h/.cc中,提供了Chrome瀏覽器端的NPN_XXX系列函數的實現;每一個插件物理實例,用PluginLib類來表示,而每一個插件的邏輯實例,用PluginInstance類 來表示。這個概念牽強附會的可以用windows中的句柄來類比,當你想操作一個內核對象,你需要獲得一個內核對象的句柄,每個進程中的句柄肯定不相同, 但後面的內核對象卻是同一個,內核對象的生命週期通過句柄的計數來控制,有人用則或,無人用則死(當然這個類比相當的牽強,主要是想說明引用計數和邏輯與物理的關係,但一個關鍵性的區別在於,PluginLib與PluginInstance都是在一個進程內的,不能跨越進程邊界…)。在Chrome中,PluginLib負責加載和銷燬一個dll,拿到所有導出函數的函數指針,PluginInstance對這些東西進行了封裝,可以更好的來調用。。。

關於NPAPI的更多細節,Chrome並沒有提供任何文檔,但是,各個先驅的瀏覽器們都提供了大量豐富的文檔。比如,你可以到這裏,查看firefox中的NPAPI文檔,基本通用。。。

2. Chrome的多進程插件模型

Chrome的插件模型,與早先的瀏覽器的最大不同,是它採用了多進程的 方式,每一個插件,都有一個單獨的進程來承載(Shift + Esc打開Chrome進程管理器,可以看到現在已經加載的插件進程…)。當WebKit進行頁面渲染的時候,發現了未知的MIME類型數據,它會告 知給Browser進程,召喚它提供一個插件來解析。如果該插件還未加載,Browser會在指定目錄中搜尋出具有此實力的插件(如果沒有此類人才只能作罷…),併爲它創建一個進程,讓它負責所有的該插件相關的任務,然後建立起一個IPC通路,與它“保持通話”。這套流程一定不會太陌生,因爲它與 Render進程的創建大同小異換湯不換藥。。。

Plugin進程與Render進程最大的區別在於,Render需要與Browser進程大量通信,因爲它的HWND歸Browser老大掌管着,相關所有內容都需要通信完成。但Plugin不需要與Browser頻繁聯繫,它大部分的通信都是與Render進程發生的。如果Plugin與Render之間的通信,還需要走Browser中轉一下,這就顯得有些脫褲子放屁了,雖然Browser是大頭,但不是冤大頭,它不會幹這種吃力不討好的事情。他只是做了一回Render與Plugin間的媒婆而已。當Plugin與Browser建立好了IPC通路後,它會讓Render建立一個新IPC通路,用以與Plugin通信,IPC的有名管道名,經由Browser通知給Plugin。完成名字協商後,Render與Plugin的通信關係就建立好了,它們之間就可以直接進行通信了。。。

整個通信模式,可以看這裏。這是一個很標準的代理模式的應用,稍有了解的都可以跳過我後面會做的一段羅嗦的描述,一看官方文檔中的圖便能知曉。在Render進程端,WebPluginImpl是WebPlugin的一個子類,WebPlugin是供Webkit進行調用的一個接口,利用依賴倒置,實現了擴展。在Plugin進程端,實現了一個WebPluginDelegateImpl類,該類會調用PluginInstance的相關接口實現真實的插件功能。這樣的話,只需要WebPluginImpl調用 WebPluginDelegateImpl中的相應方法,就可以實現功能。但問題是WebPluginImpl與 WebPluginDelegateImpl天各一方各處於一個進程,很顯然,這裏需要一個代理模式。這裏沿用了COM的架構,Delegate + Stub + Proxy。WebPluginImpl調用代理WebPluginDelegateProxy,該代理會將調用轉換成消息,通過IPC發送給Plugin進程,在Plugin端,通過WebPluginDelegateStub監聽消息,並轉換成對真實WebPluginDelegateImpl的調用,從而完成了跨進程的一個調用,反之亦然。。。

3. Chrome的可擴展性

總所周知,firefox通過三種方式進行自定義,插件、擴展和皮膚。其中,插件是使得瀏覽器能用,不會出現一大塊一大塊的無法顯示的區域;擴展是使得瀏覽器好用,可以簡單方便的進行功能的定製和個性化配置;皮膚是幫助瀏覽器變得好看,畢竟羅卜白菜,給有所愛。。。

與之對比,來看Chrome。Chrome有了插件,有了皮膚,但是沒有擴展。這就意味着,你很難爲Chrome定製一些特色的功能。目前,所有對Chrome的功能擴展,都是通過書籤抑或是修改內核來實現的。前者能力太弱,後者開發起來太麻煩,容易出錯不提,還必須要與時俱進,跟上版本的變化,並且還不能自由的選擇或關閉。因此,這都不是長遠之 計,Chrome提供一套類似於firefox的擴展機制,也許纔是正道。據傳說,Chrome團隊正在琢磨這件事,不知道最終會出來個怎麼樣的結果,是盡力接近firefox降低移植成本,還是另立門戶特立獨行,我想可以拭目以待一把。。。

在多進程模式下,Chrome的插件還有一個問題,前面提到過,就是關於UI控件的。由於NPAPI的標準,是允許插件創建HWND窗口的,這就使得當Plugin繁忙,且Browser進程發起 HWND的同步的時候,主進程被掛起,這個瀏覽器停滯。在Render進程中,解決這個問題的思路是控制權限,不然Render創建HWND,到了 Plugin中,這招不能使用,只能夠使用另一招,就是監管。不停的檢查Plugin是否太繁忙,無法響應,一旦發現,立即殺死該Plugin及其所處的頁面。這就好比你想解決奶中有三氯氰胺的問題,要麼控制奶源,不從奶站購買全部用自家的,要麼加強監管,提高檢查力度防止隱患。兩種策略的優缺點一眼便 知,依照不同環境採取不同策略即可。。。

總體說來,Chrome的可擴展性着實一般,不過Chrome還處於Beta中,我們可以繼續期待。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章