1. Chrome的窗口控件
Chrome提供了自己的一個(gè)UI控件庫(kù),相關(guān)文檔可以參見(jiàn)
這里。用Chrome自己的話來(lái)說(shuō),我覺(jué)得市面上的七葷八素的圖形控件庫(kù)都不好用,于是自己倒騰倒騰實(shí)現(xiàn)了一套。。。
廣告雖如此說(shuō),不過(guò),Chrome的圖形控件結(jié)構(gòu),我還未發(fā)現(xiàn)有啥非常非常特別的地方。Chrome的窗口、按鈕、菜單之類的控件,都直接或間接派生自View,這個(gè)是控件基類。Chrome的View具有樹(shù)形結(jié)構(gòu),其內(nèi)部有一個(gè)子View數(shù)組,由此構(gòu)成一個(gè)控件常用的組合模式。。。
有一個(gè)比較特殊的View子類,叫做RootView,顧名思義,它是整個(gè)View控件樹(shù)的根,在Chrome中,一個(gè)正確的樹(shù)形的控件結(jié)構(gòu),必須由RootView作為根。之所以要這樣設(shè)計(jì),是因?yàn)镽ootView有一個(gè)比較特殊的功能,那就是分發(fā)消息。。。
我們知道,一般的Windows控件,都有一個(gè)HWND,用與占據(jù)一塊屏幕,捕獲系統(tǒng)消息。Chrome中的View只是保存控件相關(guān)信息和繪制控件,里面沒(méi)有HWND句柄,因此不能夠捕獲系統(tǒng)消息。在Chrome中,完整的控件架構(gòu)是這樣的,首先需要有一個(gè)ViewContainer,它里面包含一個(gè)RootView。ViewContainer是一個(gè)抽象類,在Window中的一個(gè)子類是HWNDViewContainer,同時(shí),HWNDViewContainer還是MessageLoopForUI::Observer的子類。如果你看過(guò)本文第一部分描述的線程通信的內(nèi)容的話,你就應(yīng)該還記得,Observer是用于監(jiān)聽(tīng)本線程內(nèi)系統(tǒng)消息的東東。。。
當(dāng)有系統(tǒng)消息進(jìn)入此線程消息循環(huán)后,HWNDViewContainer會(huì)監(jiān)聽(tīng)到這個(gè)情況,如果和View相關(guān)的消息,它就會(huì)調(diào)用RootView的相關(guān)方法,傳遞給控件。在RootView的內(nèi)部,會(huì)遍歷整個(gè)控件樹(shù)上的控件,將消息傳遞給各個(gè)控件。當(dāng)然,有的消息是可以獨(dú)占的,比如鼠標(biāo)移動(dòng)發(fā)送在某個(gè)View所管轄的范圍內(nèi),它會(huì)告知RootView(通過(guò)方法的返回值...),這個(gè)消息我要了,那么RootView會(huì)停止遍歷。。。
在設(shè)計(jì)的時(shí)候,View對(duì)消息的處理,采取的是大而全的接口模式。就是說(shuō)在View內(nèi)部,提供了所有可能的消息處理接口,并提供了默認(rèn)實(shí)現(xiàn),所有子類只需要覆蓋自己需要的消息處理函數(shù)即可。如果對(duì)MFC的消息映射有了解的話,可以知道兩者的區(qū)別。MFC在設(shè)計(jì)的時(shí)候,覺(jué)得無(wú)法提供大而全的接口,因?yàn)橄⒖傤悓?shí)在太多,而且還是可擴(kuò)展的,于是就有了消息映射著一套繁瑣的宏。但Chrome的圖形框架,顯然沒(méi)有做一個(gè)通用的Framework的打算,因此,可以采用這樣的策略,使得子類的派生變得簡(jiǎn)單而自然。。。
每一個(gè)View的子類控件,比如Button之類的,會(huì)存儲(chǔ)一些數(shù)據(jù),根據(jù)消息做一些行為,并且繪制出自己。在Chrome中,畫圖的東西是ChromeCanvas這個(gè)類,在其內(nèi)部,通過(guò)Skia和GDI實(shí)現(xiàn)繪制。Skia是Android團(tuán)隊(duì)開(kāi)發(fā)的一個(gè)跨平臺(tái)的圖形引擎,在Chrome中負(fù)責(zé)除了文字之外,所有內(nèi)容的繪制;而文字繪制的重?fù)?dān),在Windows中交到了GDI的手上。這樣的設(shè)計(jì)會(huì)給跨平臺(tái)帶來(lái)一些困難,估計(jì)是由Skia實(shí)現(xiàn)文本繪制會(huì)比較繁瑣,才會(huì)帶出如此一個(gè)設(shè)計(jì)的模式。。。
另外一個(gè)歷史遺留產(chǎn)物,就是在Windows下的圖形控件,還有一些是原生的,就是說(shuō)帶有HWND那種傳統(tǒng)的控件,這是Chrome身上不多的趕工期的痕跡,隨著時(shí)間的寬裕,這樣的原生控件會(huì)被淘汰進(jìn)歷史的垃圾箱,而全部變?yōu)閺腣iew派生的控件。。。
其實(shí),對(duì)于Chrome這套控件架構(gòu)我還沒(méi)算摸得很熟悉,估計(jì)等到做一次插件之后會(huì)了解的更透徹,因此,只說(shuō)了點(diǎn)皮毛,聊表心意。。。
2. Chrome的頁(yè)面加載和繪制
上面這些UI控件,都是用在窗口上的(比如瀏覽器的外框,菜單,對(duì)話框之類的...)。我們?cè)跒g覽器中看到的大部分內(nèi)容,是網(wǎng)頁(yè)頁(yè)面。頁(yè)面的繪制(繪制,就是把一個(gè)HTML文件變成一個(gè)活靈活現(xiàn)的頁(yè)面展示的過(guò)程...),只有一半輪子是Chrome自己做的,還有一部分來(lái)自于WebKit,這個(gè)Apple打造的Web渲染器。。。
之所以說(shuō)是一半輪子來(lái)源于WebKit,是因?yàn)閃ebKit本身包含兩部分主要內(nèi)容,一部分是做Html渲染的,另一部分是做JavaScript解析的。在Chrome中,只有Html的渲染采用了WebKit的代碼,而在JavaScript上,重新搭建了一個(gè)NB哄哄的V8引擎。目標(biāo)是,用WebKit + V8的強(qiáng)強(qiáng)聯(lián)手,打造一款上網(wǎng)沖浪的法拉利,從效果來(lái)看,還著實(shí)做的不錯(cuò)。。。
不過(guò),雖說(shuō)Chrome和WebKit都是開(kāi)源的,并聯(lián)手工作。但是,Chrome還是刻意的和WebKit保持了距離,為其始亂終棄埋下了伏筆。Chrome在WebKit上封裝了一層,稱為WebKit Glue。Glue層中,大部分類型的結(jié)構(gòu)和接口都和WebKit類似,Chrome中依托WebKit的組件,都只是調(diào)用WebKit Glue層的接口,而不是直接調(diào)用WebKit中的類型。按照Chrome自己文檔中的話來(lái)說(shuō),就是,雖然我們?cè)儆肳ebKit實(shí)現(xiàn)頁(yè)面的渲染,但通過(guò)這個(gè)設(shè)計(jì)(加一個(gè)間接層...)已經(jīng)從某種程度大大降低了與WebKit的耦合,使得可以很容易將WebKit換成某個(gè)未來(lái)可能出現(xiàn)的更好的渲染引擎。。。
重用 在《夢(mèng)斷代碼》中,有一坨調(diào)侃重用的文字。他覺(jué)著軟件重用的困難一方面來(lái)自于場(chǎng)景本身很多變,很難設(shè)計(jì)出一套包羅萬(wàn)象的東西;另一方面來(lái)自于人,程序員總是瞅著別人寫的代碼不順眼,總喜歡自己寫一套。。。 于是,解決重用這個(gè)問(wèn)題也就只有兩種,寫最NB人見(jiàn)人服無(wú)所不能的代碼,或者是有很多很多NB代碼共君任選。Google無(wú)疑在這兩個(gè)方面做得都不錯(cuò),Map/Reduce,Big Table之類的一套東西,強(qiáng)大到可以適合太多的場(chǎng)景,大大簡(jiǎn)化了N多上層應(yīng)用的開(kāi)發(fā)。而對(duì)開(kāi)源的利用使用,使得其可以隨意挑一個(gè)巨人站到他肩膀上跳舞,每看到這種場(chǎng)景,MS估計(jì)都會(huì)氣得拍著胸口吐血。。。 Google本身在服務(wù)端的基礎(chǔ)底層,有很深積累,隨著Chrome,Android等等客戶端應(yīng)用的開(kāi)發(fā),客戶端的積累也逐步提升,也許,擁抱開(kāi)源才是MS的正道?。。。 |
當(dāng)你鍵入一個(gè)Url并敲下回車后,
Chrome會(huì)在Browser進(jìn)程中下載Url對(duì)應(yīng)的頁(yè)面資源(包括Web頁(yè)面和Cookie),而不是直接將Url發(fā)送給Render進(jìn)程讓它們自行下載(你會(huì)越來(lái)越發(fā)現(xiàn),Render進(jìn)程絕對(duì)是100%的名符其實(shí),除了繪制,幾乎啥多余的事情都不會(huì)干的...)。與各個(gè)Render進(jìn)程各自為站,各自管好自己所需的資源相比,這種策略仿佛會(huì)增加大量的進(jìn)程間通信。之所以采用,按照
這篇文檔的解釋,主要有三個(gè)優(yōu)點(diǎn),一個(gè)是避免子進(jìn)程與網(wǎng)絡(luò)通信,從而將網(wǎng)絡(luò)通信的權(quán)限牢牢握在主進(jìn)程手中,Render進(jìn)程能力弱了,想造反干壞事的可能性就降低了(可以更好控制各個(gè)Render進(jìn)程的權(quán)限...);另一個(gè)是有利于Cookie等持久化資源在不同頁(yè)面中的共享,否則在不同Render進(jìn)程中傳遞Cookie這樣的事情,做起來(lái)更麻煩;還有一點(diǎn)很重要的,是可以控制與網(wǎng)絡(luò)建立HTTP連接的數(shù)量,以Browser為代表與網(wǎng)絡(luò)各方進(jìn)行通信,各種優(yōu)化策略都比較好開(kāi)展(比如池化)。。。
當(dāng)然,在Browser進(jìn)程中進(jìn)行統(tǒng)一的資源管理,也就意味著不再方便用WebKit進(jìn)行資源下載(WebKit當(dāng)然有此能力,不過(guò)再次被Chrome拋棄了...),而是依托WinHTTP來(lái)做的。WinHTTP在接受數(shù)據(jù)的過(guò)程中,會(huì)不停的把數(shù)據(jù)和相關(guān)的消息通過(guò)IPC,發(fā)送給負(fù)責(zé)繪制此頁(yè)面的Render進(jìn)程中對(duì)應(yīng)的RenderView。在這里,路由消息中的那個(gè)ID值起了關(guān)鍵的作用,系統(tǒng)依照此ID,能夠準(zhǔn)確的將相關(guān)的消息發(fā)送到相關(guān)的View頭上,這玩意發(fā)錯(cuò)了地方還真不是和有人把錢錯(cuò)到你賬戶上一樣,因?yàn)殄e(cuò)收的進(jìn)程基本上無(wú)福消受這個(gè)意外來(lái)客,輕者頁(yè)面顯示混亂,重者消化不良直接噎死。。。
RenderView接收到頁(yè)面信息,會(huì)一邊繪制一邊等待更多的資源到來(lái),在用戶看來(lái),所請(qǐng)求的頁(yè)面正在一點(diǎn)一點(diǎn)顯示出來(lái)。當(dāng)然,如果是一個(gè)通知傳輸開(kāi)始、傳輸結(jié)束這樣的消息,通過(guò)序列化到消息參數(shù)里面,經(jīng)由IPC發(fā)過(guò)來(lái),代價(jià)還是可以承受的,但是,想資源內(nèi)容這樣大段大段的字節(jié)流,如果通過(guò)消息發(fā)過(guò)來(lái),浪費(fèi)兩邊進(jìn)程大量空間和時(shí)間,就不合適了。于是這里用到了共享內(nèi)存。Browser進(jìn)程將下載到的資源寫到共享內(nèi)存中,并將共享內(nèi)存的句柄和共享區(qū)域的大小序列化在消息中發(fā)送給Render進(jìn)程。Render進(jìn)程拿到這個(gè)句柄,就可以通過(guò)它訪問(wèn)到共享內(nèi)存相關(guān)的區(qū)域,讀取信息并進(jìn)行繪制。通過(guò)這樣的方式,即享用到了統(tǒng)一資源管理的優(yōu)點(diǎn),由避免了很高的進(jìn)程通信開(kāi)銷,左右逢源,好不快活。。。
3. Chrome頁(yè)面的消息響應(yīng)
Render進(jìn)程是一個(gè)嬌生慣養(yǎng)的進(jìn)程,這一點(diǎn)從上面一段已經(jīng)可以看出來(lái)了。它自己的資源它自己都不下載,而是由Browser進(jìn)程來(lái)幫忙。不過(guò)Render進(jìn)程也許比你想象的還要懶惰一些,它不但不自己下載資源,甚至,連自己的系統(tǒng)消息都不接收。。。
Render進(jìn)程中不包含HWND,當(dāng)你鼠標(biāo)在頁(yè)面上劃來(lái)劃去,點(diǎn)上點(diǎn)下,這些消息其實(shí)都發(fā)到了Browser進(jìn)程,它們擁有頁(yè)面呈現(xiàn)部分的HWND。Browser會(huì)將這些消息轉(zhuǎn)手通過(guò)IPC發(fā)送給對(duì)應(yīng)的Render進(jìn)程中的RenderView,很多時(shí)候WebKit會(huì)處理此類消息,當(dāng)它發(fā)現(xiàn)出現(xiàn)了某種值得告訴Browser進(jìn)程的事情,它會(huì)組個(gè)報(bào)回贈(zèng)給Browser進(jìn)程。舉個(gè)例子,你打開(kāi)一個(gè)頁(yè)面,然后拿鼠標(biāo)在頁(yè)面上亂晃。Browser這時(shí)候就像一個(gè)碎嘴大嬸,不厭其煩的告訴Render進(jìn)程,“鼠標(biāo)動(dòng)了,鼠標(biāo)動(dòng)了”。如果Render對(duì)這個(gè)信息無(wú)所謂,就會(huì)很無(wú)聊的應(yīng)答著:“哦,哦”(發(fā)送一個(gè)回包...)。但是,當(dāng)鼠標(biāo)劃過(guò)鏈接的時(shí)候,矜持的Render進(jìn)程坐不住了,會(huì)大聲告訴Browser進(jìn)程:“換鼠標(biāo),換鼠標(biāo)~~”,Browser聽(tīng)到后,會(huì)將鼠標(biāo)從箭頭狀換成手指狀,然后繼續(xù)以上過(guò)程。。。
比較麻煩的是Paint消息,重新繪制頁(yè)面是一個(gè)太頻繁發(fā)生的事情,不可能重繪一次就序列化一坨字節(jié)流過(guò)去。于是策略也很清楚了,就是依然用共享內(nèi)存讀寫,用消息發(fā)句柄。在Render進(jìn)程中,會(huì)有一個(gè)共享內(nèi)存池(默認(rèn)值為2...),以size為key,以共享內(nèi)存為值,簡(jiǎn)單的先入先出淘汰算法,利用局部性的特征,避免反復(fù)的創(chuàng)建和銷毀共享內(nèi)存(這和資源傳遞不一樣,因?yàn)橘Y源傳遞可以開(kāi)一塊固定大小的共享內(nèi)存...)。Render進(jìn)程從共享內(nèi)存池中拿起一塊(二維字節(jié)數(shù)組...),就好像拿著一塊屏幕似的,拼了命往上繪制,為了讓Render安心覺(jué)著有成就感,Browser會(huì)偷偷幫Render把這些內(nèi)容繪制到屏幕上,造成Render進(jìn)程直接繪制屏幕的假象。這可就苦了屏幕取詞的工具們,因?yàn)樵贖WND上壓根就沒(méi)啥字符信息,全部就是一坨圖像而已,啥也取不著。于是Google金山詞霸,網(wǎng)易有道詞霸各自發(fā)揮智慧,另辟蹊徑,也算是都利用Chrome做了一把廣告。。。
為什么不讓Render進(jìn)程自己擁有HWND,自己管理自己的消息,既快捷又便利。在Chrome的官方Blog上,有一篇
解釋的文章,基本上是這個(gè)意思,速度是必須快的發(fā)指的,但是為了用戶響應(yīng),放棄一些速度是必要的,畢竟,沒(méi)有人喜歡總假死的瀏覽器。在Browser進(jìn)程中,基本上是杜絕任何同步Render進(jìn)程的工作,所有操作都是異步完成。因?yàn)镽ender進(jìn)程是不靠譜的,隨時(shí)可能犧牲掉,同步它們往往導(dǎo)致主進(jìn)程停止響應(yīng),從而導(dǎo)致整個(gè)瀏覽器停下來(lái)甚至掛掉,這個(gè)代價(jià)是不可以容忍的。但是,Windows有一個(gè)惡習(xí),喜歡往整個(gè)HWND繼承體系中發(fā)送同步消息(我不是很清楚這個(gè)狀況,有人能解釋么?...),這時(shí)候,如果HWND在Render進(jìn)程中,就務(wù)必會(huì)導(dǎo)致主進(jìn)程與Render進(jìn)程的同步,Chrome無(wú)法控制Windows,于是,它們只能夠控制Render,把它們的HWND搬到主進(jìn)程中,避免同步操作,換取用戶響應(yīng)的速度。。。
4. 結(jié)論
整個(gè)Chrome的UI架構(gòu),就是一個(gè)權(quán)責(zé)分配的問(wèn)題??梢园袯rowser進(jìn)程看成是一個(gè)類似于朱元璋般的勤勞皇帝(詳見(jiàn)《明朝那些事 一》...),把大多數(shù)的權(quán)利都牢牢把握在手中,這樣,雖然Browser很操勞,但是整體上的協(xié)調(diào)和同步,都進(jìn)行的非常順暢。Render進(jìn)程就是皇帝手下的傀儡宰相們,只負(fù)責(zé)自己的一畝三分地,聽(tīng)從皇帝的調(diào)配即可。這這樣的環(huán)境下,Render進(jìn)程的生死變得無(wú)足輕重,Render的死亡,只是少了一個(gè)繪制頁(yè)面的工具而已,其他一切如故。通過(guò)控制權(quán)力,換取天下太平,這招在coding界,同樣是一個(gè)不錯(cuò)的策略,但是,唯一的意外來(lái)自于Plugin。按照規(guī)范,Chrome的Plugin是可以創(chuàng)立窗口的(HWND),這必然導(dǎo)致同步問(wèn)題,Chrome沒(méi)有辦法通過(guò)控制權(quán)力的方式解決這個(gè)問(wèn)題,只能想些別的亡羊補(bǔ)牢的招來(lái)搞定。。。