用戶在閱讀網頁的時候,需要與網頁進行交互,經常使用的操作如滑動、捏合網頁,和點擊網頁中的鏈接等。這些交互操作也稱為用戶輸入事件,閱讀器需要對它們作出迅速的響應,例如及時更新網頁內容或打開新的網頁等。閱讀器能夠對用戶輸入事件作出迅速的響應是相當重要的,由于這關乎到用戶閱讀網頁時的體驗,特別是在用戶滑動和捏合網頁時。本文接下來就扼要介紹Chromium對用戶輸入事件的處理機制,和制定后續的學習計劃。
老羅的新浪微博:http://weibo.com/shengyangluo,歡迎關注!
在任何1個平臺上,用戶輸入事件都是由系統捕獲和分發的,1般就是分給給當前激活的利用程序。利用程序接下來又會根據本身的UI結構找到產生輸入事件的位置所對應的控件,然后將輸入事件交給該控件處理。在Android平臺上,用戶輸入事件的捕獲和分發進程可以參考前面Android利用程序鍵盤(Keyboard)消息處理機制分析1文。
閱讀器作為1個利用程序,它的用戶輸入事件一樣也是系統捕獲后分發給它處理的。從前面Chromium多進程架構扼要介紹和學習計劃這個系列的文章可以知道,基于Chromium的閱讀器是多進程架構的,它主要有Browser、Render和GPU3種進程。其中,Browser進程負責創建閱讀器的UI結構,Render負責加載網頁內容,GPU進程負責將網頁內容渲染在閱讀器的UI上。這些知識點可以參考Chromium硬件加速渲染機制基礎知識扼要介紹和學習計劃、Chromium網頁加載進程扼要介紹和學習計劃和Chromium網頁渲染機制扼要介紹和學習計劃這3個系列的文章。
以Chromium自帶的Content Shell APK為例,它的Browser進程創建的UI結構如圖1所示:
圖1 Content Shell APK的UI結構
Browser進程創建的閱讀器窗口由1個ContentShellActivity描寫。這個ContentShellActivity包括了1個ShellManager。這個ShellManager又包括了1個Shell。注意,這里的ShellManager和Shell都是屬于1個Layout元素。
1個Shell由1個Toolbar和1個ContentView組成。其中,Toolbar用來輸入URL,ContentView用來展現URL對應的網頁的內容。ContentView內部包括了1個SurfaceView,網頁的內容實際上是渲染在這個SurfaceView上的。
用戶與網頁進行交互時,所產生的輸入事件就會分發給ContentView處理。整體的處理進程如圖2所示:
圖2 用戶輸入事件處理進程
ContentView運行在Browser進程中,它接收到輸入事件后首先會在Browser進程進行處理,主要就是檢查手勢操作,例如連續的輸入事件是不是產生了滑動和捏合事件。接著Browser進程再將產生的輸入事件及其產生的手勢操作通過1個類型為InputMsg_HandleInputEvent的IPC消息發送給Render進程處理。
Browser進程發送給Render進程的InputMsg_HandleInputEvent消息,首先會被Compositor線程截獲。Compositor線程主要是檢查消息里面是不是包括了滑動和捏合手勢操作。如果包括的話,那末就將它們利用在CC Active Layer Tree中。其它的輸入事個將會轉發給Main線程處理。Main線程主要將輸入事件交給WebKit處理,也就是將它們利用在WebKit保護的DOM Tree中。關于Render進程中的Compositor線程和Main線程的詳細介紹,可以參考前面Chromium網頁渲染機制扼要介紹和學習計劃1文。
接下來我們以Touch事件為例,扼要描寫Browser進程和Render進程的Compositor線程、Main線程處理輸入事件的進程。
Browser進程處理Touch事件的進程如圖3所示:
圖3 Browser進程處理Touch事件的進程
當產生Touch事件時,ContentView類的成員函數onTouchEvent就會被Android系統調用。從前面Chromium硬件加速渲染的OpenGL上下文繪圖表面創建進程分析1文可以知道,Java層的每個ContentView對象在C++層都對應有1個ContentViewCore對象。當Java層的每個ContentView對象取得1個Touch事件以后,就會將該Touch事件分發給C++層與它對應的ContentViewCore對象處理。這是通過調用ContentViewCore類的成員函數OnTouchEvent實現的。
C++層的ContentViewCore對象取得從Java層傳遞過來的Touch事件以后,就會分別通過1個Gesture Dector和1個Scale Gesture Detector檢查這個Touch事件是不是產生了滑動(Scroll)和捏合(Pinch)手勢操作。如果產生了,那末它們就會打包在1個Gesture Packet中。這個Gesture Packet又進1步通過1個InputRouter對象封裝在1個InputMsg_HandleInputEvent消息中發送給Render進程處理。
注意,原始的Touch事件同時也會被上述InputRouter對象封裝在另外1個InputMsg_HandleInputEvent消息中發送給Render進程處理。這意味1個Touch事件在產生了手勢操作的情況下,Browser進程向Render進程發送了兩個InputMsg_HandleInputEvent消息。其中1個描寫的是1個手勢操作,另外1個描寫的是原始的輸入事件。
Render進程在啟動的時候,會注冊1個類型為InputEvent的Filter,用來攔截Browser進程發送過來的輸入事件消息,如圖4所示:
圖4 Compositor線程處理滑動和捏合手勢操作的進程
Filter攔截IPC消息的進程,可以參考前面Chromium的IPC消息發送、接收和分發機制分析1文。
InputEvent Filter攔截到Browser進程發送過來的InputMsg_HandleInputEvent消息以后,會分給Compositor線程處理。Compositor線程進1步檢查這個InputMsg_HandleInputEvent消息是不是是1個滑動手勢操作或捏合手勢操作。如果是的話,就將它們利用在CC Active Layer Tree中。否則的話,就將攔截到的InputMsg_HandleInputEvent消息轉給給Main線程處理。
從前面Chromium網頁渲染機制扼要介紹和學習計劃這個系列的文章可以知道,CC Active Layer Tree是通過激活CC Pending Layer Tree得到的,而CC Pending Layer Tree又是通過同步CC Layer Tree得到的。理論上說,Compositor線程應當將滑動和捏合手勢操作利用在CC Layer Tree上,然后再同步到CC Pending Layer Tree中,最后再將CC Pending Layer Tree激活為CC Active Layer Tree。這樣就能夠保證這3個Tree的狀態1致性。但是如果這樣做的話,會觸及到以下4個操作:
1. 重新繪制CC Layer Tree。
2. 將重新繪制的CC Layer Tree同步到CC Pending Layer Tree中。
3. 將CC Pending Layer Tree激活為CC Active Layer Tree中。
4. 對CC Active Layer Tree進行渲染。
這個處理進程太漫長了,達不到快速響利用戶輸入的目的。CC Active Layer Tree描寫的是用戶當前在屏幕上看到的網頁的內容,當用戶滑動或捏合網頁的時候,網頁的內容并沒有產生實質的變化,只不過是位置或縮放因子產生了變化,這完全可以通過當前使用的CC Active Layer Tree來處理。這樣就能夠省掉上述的前3個操作,因而就能夠快速發響利用戶輸入了。從這里我們也能夠看出,為何Chromium要用3個Tree來描寫同1個網頁了,實際上就是為了能夠快速地響利用戶輸入,提高網頁閱讀的用戶體驗。
但是,如果只將滑動和捏合手勢操作利用在CC Active Layer Tree上,就會致使它的狀態與CC Layer Tree不1致,這是不允許的。為了解決這個問題,Compsitor線程同時會要求調度器履行1個Commit操作。這個Commit操作將會觸發Main線程重新繪制CC Layer Tree。Main線程在繪制CC Layer Tree之前,會搜集之前利用在CC Active Layer Tree上的滑動和捏合操作,并且將它們利用在行將要繪制的CC Layer Tree之上。這樣就能夠保證它的狀態與CC Active Layer Tree保證1致了。注意,將滑動和捏合手勢操作利用在CC Active Layer Tree上和重新繪制CC Layer Tree分別產生在Compositor線程和Main線程中,它們是并發履行的。
對那些不是描寫手勢操作的InputMsg_HandleInputEvent消息,Compositor線程將會轉發給Main線程處理,如圖5所示:
圖5 Main線程處理Touch事件的進程
從前面Chromium網頁Frame Tree創建進程分析1文可以知道,Main線程在加載網頁的內容之前,會在WebKit里面創建1個WebViewImpl對象。我們可以將這個WebViewImpl對象理解為WebKit對外提供的1個接口。通過這個接口,Chromium可以操作WebKit為它所加載、解析和渲染的網頁。
Main線程接收到Compositor發送過來的InputMsg_HandleInputEvent消息就會調用上述WebViewImpl對象的成員函數handleInputEvent,用來通知WebKit對該InputMsg_HandleInputEvent消息描寫的輸入事件進行處理。在我們這個情形中,這個輸入事件即為1個Touch事件。
WebKit接收到Touch事件通知后,所做的第1件事情是做Hit Test,也就是檢測Touch事件產生在哪個網頁元素上,然后再將接收到的Touch事件交給該網頁元素處理。注意,這里說的網頁元素,指的是網頁DOM Tree中的節點,也就是DOM Node。1旦找到這個目標網頁元素,那末WebKit就會調用它的成員函數dispatchTouchEvent,讓它處應當前產生的Touch事件。
現在最重要的事情就是在DOM Tree中找到目標DOM Node。每個輸入事件都關聯有1個位置,所有在該位置上的DOM Node都多是目標DOM Node。如果輸入事件產生的位置沒有DOM Node,或只有1個DOM Node,那末事情就好辦。但是常常這個位置上有多個DOM Node,因此就需要將其中1個設置為目標DOM Node。1般來講,在Z軸上位置最高的DOM Node,就是要找的目標DOM Node。
DOM Tree記錄了每個DOM Node的Z-Index值。理論上說,通過這些Z-Index值,就能夠肯定1個輸入事件的目標DOM Node,也就是僅僅通過DOM Tree就能夠肯定1個輸入事件的目標DOM Node。但是從前面Chromium網頁Graphics Layer Tree創建進程分析1文可以知道,1個DOM Node的Z-Index值其實不能終究決定它在Z軸上的位置,還與這個DOM所在的Stacking Context的Z-Index值有關。例如,假定有兩個Stacking Context,SC1和SC2。其中SC1的Z-Index值等于0,SC2的Z-Index值等于1。SC1包括了1個DOM Node,它的Z-Index值等于9,SC2包括了1個DOM Node,它的Z-Index值等于6。終究的效果是Z-Index值為6的DOM Node位于Z-Index值為9的DOM Node之上。這是由于前者所在的Stacking Context的Z-Index值比后者所在的Stacking Context的Z-Index值大的原因。
WebKit保護的Render Layer Tree記錄了網頁所有的Stacking Context的信息,因此WebKit在做輸入事件的Hit Test時,不能通過DOM Tree來肯定目標DOM Node,而是要通過Render Layer Tree來肯定。
以上就是Chromium處理輸入事件的整體進程概述,它觸及到Browser進程,和Render進程中的Compositor線程和Main線程。Browser進程負責手勢操作檢測,Compositor線程將手勢操作利用在網頁的CC Active Layer Tree中,Main線程負責將輸入事件分發給WebKit處理。為了更好地理解Chromium的輸入事件處理機制,接下來我們將依照以下3個情形進行詳細分析:
1. Browser進程處理輸入事件的進程分析;
2. Compositor線程處理輸入事件的進程分析;
3. Main線程處理輸入事件的進程分析。
敬請關注!更多的信息也能夠關注老羅的新浪微博:http://weibo.com/shengyangluo。
下一篇 C++中enum的使用