Chromium學習1

轉載自http://blog.csdn.net/qq295445028/article/details/7993069的一系列文章

http://blog.csdn.net/qq295445028/article/details/7993048

Chromium的技術優點,需要去分析下來內容:

1.它是如何利用多進程(其實也會有多線程一起)做并發的,又是如何解決多進程間的一些問題的,比如進程間通信,進程的開銷;

2.做為一個后來者,它的擴展能力如何,如何去權衡對原有插件的兼容,提供怎么樣的一個插件模型;

3.它的整體框架是怎樣,有沒有很NB的架構思想;

4.它如何實現跨平臺的UI控件系統;

5.傳說中的V8,為啥那么快

Chromium的設計文檔

https://sites.google.com/a/chromium.org/dev/developers/design-documents

Chromium的多進程模式

https://sites.google.com/a/chromium.org/dev/developers/design-documents/multi-process-architecture

Chrome有一個主進程,稱為Browser進程,管理Chrome大部分的日常事務;

有很多Renderer進程,它們圈地而治,各管理一組站點的顯示和通信(Chrome在宣傳中一直宣稱一個tab對應一個進程,其實是很不確切的...),它們彼此互不搭理,只和老大說話,由老大負責權衡各方利益。它們和老大說話的渠道,稱做IPC(Inter-Process Communication),這是Google搭的一套進程間通信的機制。

Chrome的進程模型

Google在宣傳的時候一直都說,Chrome是one tab one

process的模式,其實,這只是為了宣傳起來方便如是說而已,基本等同廣告,實際療效,還要從代碼中來看。實際上,Chrome支持的進程模型遠比宣傳豐富,你可以參考一下這里,簡單的說,Chrome支持以下幾種進程模型:

?Process-per-site-instance:就是你打開一個網站,然后從這個網站鏈開的一系列網站都屬于一個進程。這是Chrome的默認模式。

?Process-per-site:同域名范疇的網站放在一個進程,比如www.google.com和www.google.com/bookmarks就屬于一個域名內(google有自己的判定機制),不論有沒有互相打開的關系,都算作是一個進程中。用命令行--process-per-site開啟。

?Process-per-tab:這個簡單,一個tab一個process,不論各個tab的站點有無聯系,就和宣傳的那樣。用--process-per-tab開啟。

?Single Process:這個很熟悉了吧,傳統瀏覽器的模式,沒有多進程只有多線程,用--single-process開啟。

大家可以用Shift+Esc觀察各模式下進程狀況,至少我是觀察失敗了(每種都和默認的一樣...),原因待跟蹤。。。

不論是Browser進程還是Renderer進程,都不只是光桿司令,它們都有一系列的線程為自己打理各種業務。對于Renderer進程,它們通常有兩個線程,一個是Main thread,它負責與老大進行聯系,有一些幕后黑手的意思;另一個是Render thread,它們負責頁面的渲染和交互,一看就知道是這個幫派的門臉級人物。相比之下,Browser進程既然是老大,小弟自然要多一些,除了大腦般的Main thread,和負責與各Renderer幫派通信的IO thread,其實還包括負責管文件的file thread,負責管數據庫的db thread等等

同一個進程內的線程,往往需要很多的協同工作,這一坨線程間的并發管理,是Chrome最出彩的地方之一了。

Chrome的線程模型走的是另一個路子,即,極力規避鎖的存在。換更精確的描述方式來說,Chrome的線程模型,將鎖限制了極小的范圍內(僅僅在將Task放入消息隊列的時候才存在),并且使得上層完全不需要關心鎖的問題(當然,前提是遵循它的編程模型,將函數用Task封裝并發送到合適的線程去執行),大大簡化了開發的邏輯。

不過,從實現來說,Chrome的線程模型并沒有什么神秘的地方,它用到了消息循環的手段。每一個Chrome的線程,入口函數都差不多,都是啟動一個消息循環(參見MessagePump類),等待并執行任務。而其中,唯一的差別在于,根據線程處理事務類別的不同,所起的消息循環有所不同。比如處理進程間通信的線程(注意,在Chrome中,這類線程都叫做IO線程)啟用的是MessagePumpForIO類,處理UI的線程用的是MessagePumpForUI類,一般的線程用到的是MessagePumpDefault類。不同的消息循環類,主要差異有兩個,一是消息循環中需要處理什么樣的消息和任務,第二個是循環流程(比如是死循環還是阻塞在某信號量上)。

Chrome中的Task

Chrome中的線程從實現層面來看沒有任何區別,它的區別只存在于職責層面,不同職責的線程,會處理不同的Task

Task就是一個類,一個包含了void Run()抽象方法的類(參見Task類...)。一個真實的任務,可以派生Task類,并實現其Run方法。每個MessagePump類中,會有一個MessagePump::Delegate的類的對象(MessagePump::Delegate的一個實現,請參見MessageLoop類...),在這個對象中,會維護若干個Task的隊列。當你期望,你的一個邏輯在某個線程內執行的時候,你可以派生一個Task,把你的邏輯封裝在Run方法中,然后實例一個對象,調用期望線程中的PostTask方法,將該Task對象放入到其Task隊列中去,等待執行。

Chrome中,線程模型是統一且唯一的,這就相當于有了一套標準,它需要滿足在各個線程上執行的幾十上百種任務的需求,因此,必須在靈活性和易用性上有良好的表現,這就是設計標準的難度。為了滿足這些需求,Chrome在底層庫上做了足夠的功夫:

它提供了一大套的模板封裝(參見task.h),可以將Task擺脫繼承結構、函數名、函數參數等限制(就是基于模板的偽function實現,想要更深入了解,建議直接看鼻祖《Modern C++》和它的Loki庫);

同時派生出CancelableTask、ReleaseTask、DeleteTask等子類,提供更為良好的默認實現;

在消息循環中,按邏輯的不同,將Task又分成即時處理的Task、延時處理的Task、Idle時處理的Task,滿足不同場景的需求;

Task派生自tracked_objects::Tracked,Tracked是為了實現多線程環境下的日志記錄、統計等功能,使得Task天生就有良好的可調試性和可統計性;

Chrome的多線程模型下,加鎖這個事情只發生在將Task放入某線程的任務隊列中,其他對任何數據的操作都不需要加鎖。如果你熟悉設計模式,你會發現這是一個Command模式,將創建與執行的環境相分離,在一個線程中創建行為,在另一個線程中執行行為。Command模式的優點在于,將實現操作與構造操作解耦,這就避免了鎖的問題,使得多線程與單線程編程模型統一起來,其次,Command還有一個優點,就是有利于命令的組合和擴展,在Chrome中,它有效統一了同步和異步處理的邏輯。。。

Command模式

Command模式,是一種看上去很酷的模式,傳統的面向對象編程,我們封裝的往往都是數據,在Command模式下,我們希望封裝的是行為。這件事在函數式編程中很正常,封裝一個函數作為參數,傳來傳去,稀疏平常的事兒;但在面向對象的編程中,我們需要通過繼承、模板、函數指針等手法,才能將其實現。。。

應用Command模式,我們是期望這個行為能到一個不同于它出生的環境中去執行,簡而言之,這是一種想生不想養的行為。我們做Undo/Redo的時候,會把在任一一個環境中創建的Command,放到一個隊列環境中去,供統一的調度;在Chrome中,也是如此,我們在一個線程環境中創建了Task,卻把它放到別的線程中去執行,這種寄居蟹似的生活方式,在很多場合都是有用武之地的。。。

在一般的多線程模型中,我們需要分清楚啥是同步啥是異步,在同步模式下,一切看上去和單線程沒啥區別,但同時也喪失了多線程的優勢(淪落成為多線程串行...)。而如果采用異步的模式,那寫起來就麻煩多了,你需要注冊回調,小心管理對象的生命周期,程序寫出來是嗷嗷惡心。在Chrome的多線程模型下,同步和異步的編程模型區別就不復存在了,如果是這樣一個場景:A線程需要B線程做一些事情,然后回到A線程繼續做一些事情;在Chrome下你可以這樣來做:生成一個Task,放到B線程的隊列中,在該Task的Run方法最后,會生成另一個Task,這個Task會放回到A的線程隊列,由A來執行。如此一來,同步異步,天下一統,都是Task傳來傳去,想不會,都難了。。。

最根本的缺陷,是鎖和條件變量不支持模塊化的編程。比如一個轉賬業務中,A賬戶扣了100元錢,B賬戶增加了100元,即使這兩個動作單獨用鎖保護維持其正確性,你也不能將兩個操作簡單的串在一起完成一個轉賬操作,你必須讓它們的鎖都暴露出來,重新設計一番。好好的兩個函數,愣是不能組在一起用,這就是鎖的最大悲哀;

通過這些缺點的描述,也就可以明白Chrome多線程模型的優點。它解決了鎖的最根本缺陷,即,支持模塊化的編程,你只需要維護對象和線程之間的職能關系即可,這個攤子,比之鎖的那個爛攤子,要簡化了太多。對于程序員來說,負擔一瞬間從泰山降成了鴻毛。。。

畢竟,在客戶端,不會和服務器一樣,存在超規模的并發處理任務,而只是需要盡可能的改善用戶體驗

Chrome的進程間通信

1.Chrome進程通信的基本模式

進程間通信,叫做IPC(Inter-Process Communication),在Chrome不多的文檔中,有一篇就是介紹這個的,在這里。Chrome最主要有三類進程,

一類是Browser主進程,我們一直尊稱它老人家為老大;

還有一類是各個Render進程,前面也提過了;

另外還有一類一直沒說過,是Plugin進程,每一個插件,在Chrome中都是以進程的形式呈現,等到后面說插件的時候再提罷了。

Render進程和Plugin進程都與老大保持進程間的通信,Render進程與Plugin進程之間也有彼此聯系的通路,唯獨是多個Render進程或多個Plugin進程直接,沒有互相聯系的途徑,全靠老大協調。

進程與進程間通信,需要仰仗操作系統的特性,能玩的花著實不多,在Chrome中,用到的就是有名管道(Named Pipe),只不過,它用一個IPC::Channel類,封裝了具體的實現細節。Channel可以有兩種工作模式,一種是Client,一種是Server,Server和Client分屬兩個進程,維系一個共同的管道名,Server負責創建該管道,Client會嘗試連接該管道,然后雙發往各自管道緩沖區中讀寫數據(在Chrome中,用的是二進制流,異步IO...),完成通信。。。

管道名字的協商。在Socket中,我們會事先約定好通信的端口,如果不按照這個端口進行訪問,走錯了門,會被直接亂棍打出門去的。與之類似,有名管道期望在兩個進程間游走,就需要拿一個兩個進程都能接受的進門暗號,這個就是有名管道的名字。在Chrome中(windows下...),有名管道的名字格式都是:\\.\pipe\chrome.ID。其中的ID,自然是要求獨一無二,比如:進程ID.實例地址.隨機數。通常,這個ID是由一個Process生成(往往是Browser

Process),然后在創建另一個進程的時候,作為命令行參數傳進去,從而完成名字的協商。。。

Channel中,有三個比較關鍵的角色,

一個是Message::Sender

一個是Channel::Listener

最后一個是MessageLoopForIO::Watcher

Channel本身派生自Sender和Watcher,身兼兩角,而Listener是一個抽象類,具體由Channel的使用者來實現。顧名思義,Sender就是發送消息的接口,Listener就是處理接收到消息的具體實現,但這個Watcher是啥?如果你覺得Watcher這東西看上去很眼熟的話,我會激動的熱淚盈眶的,沒錯,在前面(第一部分第一小節...)說消息循環的時候,從那個表中可以看到,IO線程(記住,在Chrome中,IO指的是網絡IO,*_*)的循環會處理注冊了的Watcher。其實Watcher很簡單,可以視為一個信號量和一個帶有OnObjectSignaled方法對象的對,當消息循環檢測到信號量開啟,它就會調用相應的OnObjectSignaled方法。。。

溫柔的消息循環

其實,Chrome的很多消息循環,也不是都那么霸道,也是會被阻塞在某些信號量或者某種場景上的,畢竟客戶端不是它家的服務器,CPU不能被全部歸在它家名下。。。

比如IO線程,當沒有消息來到,又沒有信號量被激活的時候,就會被阻塞,具體實現可以去看MessagePumpForIO的WaitForWork方法。。。

不過這種阻塞是集中式的,可隨時修改策略的,比起Channel直接阻塞在信號量上,停工的時間更短。。。

2.進程間的跨線程通信和同步通信

在Chrome中,任何底層的數據都是線程非安全的,在每一個進程中,只能有一個線程來負責操作Channel,這個線程叫做IO線程。但是有時候(其實是大部分時候...),我們需要從非IO線程與別的進程相通信,這該如何是好?如果,你有看過我前面寫的線程模型,你一定可以想到,做法很簡單,先將對Channel的操作放到Task中,將此Task放到IO線程隊列里,讓IO線程來處理即可。當然,由于這種事情發生的太頻繁,每次都人肉做一次頗為繁瑣,于是有一個代理類,叫做ChannelProxy,來幫助你完成這一切。。。

從接口上看,ChannelProxy的接口和Channel沒有大的區別(否則就不叫Proxy了...),你可以像用Channel一樣,用ChannelProxy來Send你的消息,ChannelProxy會辛勤的幫你完成剩余的封裝Task等工作。不僅如此,ChannelProxy還青出于藍勝于藍,在這個層面上做了更多的事情,比如:發送同步消息。。。

不過能發送同步消息的類不是ChannelProxy,而是它的子類,SyncChannel。在Channel那里,所有的消息都是異步的(在Windows中,也叫Overlapped...),其本身也不支持同步邏輯。為了實現同步,SyncChannel并沒有另造輪子,而只是在Channel的層面上加了一個等待操作。當ChannelProxy的Send操作返回后,SyncChannel會把自己阻塞在一組信號量上,等待回包,直到永遠或超時。從外表上看同步和異步沒有什么區別,但在使用上還是要小心,在UI線程中使用同步消息,是容易被發指的。。

3.Chrome中的IPC消息格式

說了半天,還有一個大頭沒有提過,那就是消息包。如果說,多線程模式下,對數據的訪問開銷來自于鎖,那么在多進程模式下,大部分的額外開銷都來自于進程間的消息拆裝和傳遞。不論怎么樣的模式,只要進程不同,消息的打包,序列化,反序列化,組包,都是不可避免的工作。。。

在Chrome中,IPC之間的通信消息,都是派生自IPC::Message類的。對于消息而言,序列化和反序列化是必須要支持的,Message的基類Pickle,就是干這個活的。Pickle提供了一組的接口,可以接受int,char,等等各種數據的輸入,但是在Pickle內部,所有的一切都沒有區別,都轉化成了一坨二進制流這個二進制流是32位齊位的,比如你只傳了一個bool,也是最少占32位的,同時,Pickle的流是有自增邏輯的(就是說它會先開一個Buffer,如果滿了的話,會加倍這個Buffer...),使其可以無限擴展。Pickle本身不維護任何二進制流邏輯上的信息,這個任務交到了上級處理(后面會有說到...),但Pickle會為二進制流添加一個頭信息,這個里面會存放流的長度,Message在繼承Pickle的時候,擴展了這個頭的定義,其中,黃色部分是包頭,定長96個bit,綠色部分是包體,二進制流,由payload_size指明長度。從大小上看這個包是很精簡的了,除了routing位在消息不為路由消息的時候會有所浪費。消息本身在有名管道中是按照二進制流進行傳輸的(有名管道可以傳輸兩種類型的字符流,分別是二進制流和消息流...),因此由payload_size

+ 96bits,就可以確定是否收了一個完整的包。。。

消息的序列化

前不久讀了Google Protocol Buffers的源碼,是用在服務器端,用做內部機器通信協議的標準、代碼生成工具和框架。它主要的思想是揉合了key/value的內容到二進制中,幫助生成更為靈活可靠的二進制協議。。。

在Chrome中,沒有使用這套東西,而是用到了純二進制流作為消息序列化的方式。我想這是由于應用場景不同使然。在服務端,我們更關心協議的穩定性,可擴展性,并且,涉及到的協議種類很多。但在一個Chrome中,消息的格式很統一,這方面沒有擴展性和靈活性的需求,而在序列化上,雖然key/value的方式很好很強大,但是在Chrome中需要的不是靈活性而是精簡性,因此寧可不用Protocol Buffers造好的輪子,而是另立爐灶,花了好一把力氣提供了一套純二進制的消息機制。

從邏輯上來看,IPC消息分成兩類,一類是路由消息(routed

message),還有一類是控制消息(control message)。路由消息是私密的有目的地的,系統會依照路由信息將消息安全的傳遞到目的地,不容它人窺視;控制消息就是一個廣播消息,誰想聽等能夠聽得到。。。

1.定義IPC消息

一個標準的IPC消息定義應該是類似于這樣的,你需要從Message(或者其他子類)派生出一個子類,該子類有一個獨一無二的ID值,該子類接受一個參數,你需要對這個參數進行序列化。兩個麻煩的地方看的很清楚,如何生成獨一無二的ID值?如何更方便的對任何參數可以自動的序列化?。。。

在Chrome中,解決這兩個問題的答案,就是宏+模板。Chrome為每個消息安排了一種ID規格,用一個16bits的值來表示,高4位標識一個Channel,低12位標識一個消息的子id,也就是說,最多可以有16種Channel存在不同的進程之間,每一種Channel上可以定義4k的消息。目前,Chrome已經用掉了8種Channel(如果A、B進程需要雙向通信,在Chrome中,這是兩種不同的Channel,需要定義不同的消息,也就是說,一種雙向的進程通信關系,需要耗費兩個Channel種類...),他們已經覺得,16bits的ID格式不夠用了,在將來的某一天,估計就被擴展成了32bits的。Chrome是這么來定義消息ID的,用一個枚舉類,讓它從高到低往下走,就像這樣:

enum SomeChannel_MsgType

{

SomeChannelStart = 5 << 12,

SomeChannelPreStart = (5 << 12) - 1,

Msg1,

Msg2,

Msg3,

...

MsgN,

SomeChannelEnd

};

這是一個類型為5的Channel的消息ID聲明,由于指明了最開始的兩個值,所以后續枚舉的值會依次遞減,如此,只要維護Channel類型的唯一性,就可以維護所有消息ID的唯一性了(當然,前提是不能超過消息上限...)。

定義一個ID還不夠,你還需要定義一個使用該消息ID的Message子類。這個步驟不但繁瑣,最重要的,是違反了DIY原則,為了添加一個消息,你需要在兩個地方開工干活,是可忍孰不可忍,于是Google祭出了宏這顆原子彈,需要定義消息,格式如下:

IPC_BEGIN_MESSAGES(PluginProcess, 3)

IPC_MESSAGE_CONTROL2(PluginProcessMsg_CreateChannel,

int /* process_id */,

HANDLE /* renderer handle */)

IPC_MESSAGE_CONTROL1(PluginProcessMsg_ShutdownResponse,

bool /* ok to shutdown */)

IPC_MESSAGE_CONTROL1(PluginProcessMsg_PluginMessage,

std::vector/* opaque data */)

IPC_MESSAGE_CONTROL0(PluginProcessMsg_BrowserShutdown)

IPC_END_MESSAGES(PluginProcess)

多次展開宏的技巧

這是Chrome中用到的一個技巧,定義一次宏,展開多段代碼,我孤陋寡聞,第一次見,一個類似的例子,如下:

首先,定義一個macro.h,里面放置宏的定義:

#undef SUPER_MACRO

#if

defined(FIRST_TIME)

#undef FIRST_TIME

#define

SUPER_MACRO(label, type) \

enum IDs { \

label##__ID = 10 \

};

#elif

defined(SECOND_TIME)

#undef SECOND_TIME

#define

SUPER_MACRO(label, type) \

class TestClass \

{\ };

這是Chrome中,定義PluginProcess消息的宏,如果你想添加一條消息,只需要添加一條類似與IPC_MESSAGE_CONTROL0東東即可,這說明它是一個控制消息,參數為0個。你基本上可以這樣理解,IPC_BEGIN_MESSAGES就相當于完成了一個枚舉開始的聲明,然后中間的每一條,都會在枚舉里面增加一個ID,并聲明一個子類。這個一宏兩吃,可以參看ipc_message_macros.h,或者看下面一宏兩吃的一個舉例。。。

此外,當接收到消息后,你還需要處理消息。接收消息的函數,是IPC::Channel::Listener子類的OnMessageReceived函數。在這個函數中,會放置一坨的宏,

IPC_BEGIN_MESSAGE_MAP_EX(RenderProcessHost,

msg, msg_is_ok)

IPC_MESSAGE_HANDLER(ViewHostMsg_PageContents,OnPageContents)

IPC_MESSAGE_HANDLER(ViewHostMsg_UpdatedCacheStats,

OnUpdatedCacheStats)

IPC_MESSAGE_UNHANDLED_ERROR()

IPC_END_MESSAGE_MAP_EX()

這個東西很簡單,展開后基本可以視為一個Switch循環,判斷消息ID,然后將消息,傳遞給對應的函數。

通過宏的手段,可以解決消息類聲明和消息的分發問題,但是自動的序列化還不能支持(所謂自動的序列化,就是不論你是什么類型的參數,幾個參數,都可以直接序列化,不需要另寫代碼...)。在C++這種語言中,所謂自動的序列化,自動的類型識別,自動的XXX,往往都是通過模板來實現的。這些所謂的自動化,其實就是通過事前的大量人肉勞作,和模板自動遞推來實現的,如果說.Net或Java中的自動序列化是過山軌道,這就是那挑夫的驕子,雖然最后都是兩腿不動到了山頂,這底下費得力氣真是天壤之別啊。具體實現技巧,有興趣的看看《STL源碼剖析》,或者是《C++新思維》,或者Chrome中的ipc_message_utils.h,這要說清楚實在不是一兩句的事情。。。

總之通過宏和模板,你可以很簡單的聲明一個消息,這個消息可以傳入各式各樣的參數(這里用到了夸張的修辭手法,其實,只要是模板實現的自動化,永遠都是有限制的,在Chrome的模板實現中,參數數量不要超過5個,類型需要是基本類型、STL容器等,在不BT的場合,應該夠用了...),你可以調用Channel、ChannelProxy、SyncChannel之類的Send方法,將消息發送給其他進程,并且,實現一個Listener類,用Message Map來分發消息給對應的處理函數。如此,整個IPC體系搭建完成。。。

Chrome的進程模型

1.基本的進程結構

Chrome是一個多進程的架構,不過所有的進程都會由老大,Browser進程來管理,走的是集中化管理的路子。在Browser進程中,有xxxProcessHost,每一個host,都對應著一個Process,比如RenderProcessHost對應著RenderProcess,PluginProcessHost對應著PluginProcess,有多少個host的實例,就有多少個進程在運行。。。

這是一個比較典型的代理模式,Browser對Host的操作,都會被Host封裝成IPC消息,傳遞給對應的Process來處理,對于大部分上層的類,也就隔離了多進程細節。。。

2.Render進程

前面說了,一個Process一個tab,只是廣告用語,實際上,每一個web頁面內容(包括在tab中的和在彈出窗口中的...),在Chrome中,用RenderView表示一個web頁面,每一個RenderView可以寄宿在任一一個RenderProcess中,它只是依托RenderProcess幫助它進行通信。每一個RenderProcess進程都可以有1到N個RenderView實例。。。

Chrome支持不同的進程模型,可以一個tab一個進程,一個site instance一個進程等等。但基本模式都是一致的,當需要創建一個新的RenderView的時候,Chrome會嘗試進行選擇或者是創建進程。比如,在one site one

process的模式下,如果存在此site,就會選擇一個已有的RenderProcessHost,讓它管理這個新的RenderView,否則,會創建一個RenderProcessHost(同時也就創建了一個Process),把RenderView交給它。。。

在默認的one site instance one process的模式中,Chrome會為每個新的site instance創建一個進程(從一個頁面鏈開來的頁面,屬于同一個site instance),但,Render進程總數是有個上限的。這個上限,根據內存大小的不同而異,比如,在我的機器上(2G內存),最多可以容納20個Render進程,當達到這個上限后,你再開新的網站,Chrome會隨機為你選擇一個已有的進程,把這個網站對應的RenderView給扔進去。。。

Chrome并沒有像我YY的一樣做啥進程池之類的特殊機制,而是簡單的履行有就創建、沒有就銷毀的策略

3.進程開銷控制算法

Chrome沒有在進程創建和銷毀上做功夫,但是當進程運行起來后,還是做了一些工作的。。。

節約工作首先從CPU耗時上做起,優先級越高的進程中的線程,越容易被調度,從而耗費CPU時間,于是,當一個頁面不再直接面對用戶的時候,Chrome會將它的進程優先級切到Below Normal的級別,反之,則切回Normal級別。通過這個步驟,小節約了一把時間。。。

當然這只是一道開胃小菜,滿漢全席是控制進程的工作集大小,以達到降低進程實際內存消耗的目的(Chrome為了體現它對內存的節約,用了“更為精確”的內存消耗計算方法...)。提到這一點,Chrome頗為自豪,在文檔中,順著道把單進程的模式鄙視了一下,基本意思是:在多進程的模式下,各個頁面實際占用的內存數量,更容易被控制,而在單進程的模式下,幾乎是不能作出控制的,所以,很多時候,多進程模式耗費的內存,是會小于多線程模式的。這個說法靠不靠譜,大家心里都有譜,就不多說了。。。

具體說來,Chrome對進程工作集的控制算法還是比較簡單的。首先,在進程啟動的時候,需要指明進程工作的內存環境,是高內存,低內存,還是中等內存,默認模式下,是中等內存(我以為Chrome會動態計算的,沒想到竟然是啟動時指定...)。在高內存模式,不存在對工作集的調整,使勁用就完事了;在低內存的模式下,調整也很簡單,一旦一個進程不再有頁面面對觀眾了,嘗試釋放其所有工作集。相比來說,中等模式下,算法相對復雜一些,當一個進程從直接面對觀眾,淪落到切換到后臺的悲慘命運,其工作集會縮減,算法為:TargetWorkingSetSize = (LastWorkingSet/2 + CurrentWorkingSet) /2;其中,TargetWorkingSetSize指的是預期降到的工作集大小,CurrentWorkingSet指的是進程當前的工作集(在Chrome中,工作集的大小,包含私有的和可共享的兩部分內存,而不包含已經共享了的內存空間...),LastWorkingSet,等于上一次的CurrentWorkingSet除以DampingFactor,默認的DampingFactor為2。而反之,當一個進程從幕后走向臺前,它的工作集會被放大為LastWorkingSet *?DampingFactor * 2,了解過LastWorkingSet的含義,你已經知道,這就是將工作集放大兩倍的另類版寫法。。。

ChromeRender進程工作集調整,除了發生在tab切換(或新頁面建立)的時候,還會發生在整個Chromeidle事件觸發后。Chrome有個計時器,統計Chrome空閑的時長,當時長超過30s后(此工作會反復進行...),Chrome會做一系列工作,其中就包括,調整進程的工作集。被調整的進程,不僅僅是Render進程,還包括Plugin進程和Browser進程,換句話描述,就是所有Chrome進程。。。

Chrome的UI繪制

1.Chrome的窗口控件

Chrome提供了自己的一個UI控件庫。Chrome的窗口、按鈕、菜單之類的控件,都直接或間接派生自View,這個是控件基類。ChromeView具有樹形結構,其內部有一個子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內部,提供了所有可能的消息處理接口,并提供了默認實現,所有子類只需要覆蓋自己需要的消息處理函數即可。

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

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換成某個未來可能出現的更好的渲染引擎。。。

當你鍵入一個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的調用,從而完成了跨進程的一個調用,反之亦然。。。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,335評論 25 708
  • 又來到了一個老生常談的問題,應用層軟件開發的程序員要不要了解和深入學習操作系統呢? 今天就這個問題開始,來談談操...
    tangsl閱讀 4,172評論 0 23
  • 學習要點: 1.HTML5 文檔結構2.文檔結構解析3.元素標簽探討 主講教師:李炎恢 本章主要先從 HTML5 ...
    愛上小媳婦閱讀 264評論 0 2
  • 圖一至三是之前在網上看到的圖片,就臨摹下來,很抱歉,由于時間太久,不記得原作者是誰如果大家見過原畫,歡迎告訴我。圖...
    予安安閱讀 403評論 2 6
  • 今天看了不少的關于產品思維的文章,甚有感悟。但是還是覺得一句話說的非常對,做產品就像游泳,你掌握了很多理論知識,只...
    魚太咸丶閱讀 303評論 0 1