一個Android應用程序窗口里面包含了很多UI元素,這些UI元素是以樹形結構來組織的,即它們存在着父子關系,其中,子UI元素位於父UI元素里面,因此,在繪制一個Android應用程序窗口的UI之前,我們首先要確定它里面的各個子UI元素在父UI元素里面的大小以及位置。確定各個子UI元素在父UI元素里面的大小以及位置的過程又稱為測量過程和布局過程。因此,Android應用程序窗口的UI渲染過程可以分為測量、布局和繪制三個階段,如圖1所示:
從前面Android應用程序窗口(Activity)的視圖對象(View)的創建過程分析一文可以知道,Android應用程序窗口的頂層視圖是一個類型為DecorView的UI元素,而從前面Android應用程序窗口(Activity)的繪圖表面(Surface)的創建過程分析一文的Step 3又可以知道,這個頂層視圖最終是由ViewRoot類的成員函數performTraversals來啟動測量、布局和繪制操作的,這三個操作分別由DecorView類的成員函數measure和layout以及ViewRoot類的成員函數draw來實現的。
接下來,我們就分別從DecorView類的成員函數measure和layout以及ViewRoot類的成員函數draw開始,分析Android應用程序窗口的測量、布局和繪制過程。
1. Android應用程序窗口的測量過程
DecorView類的成員函數measure是從父類View繼承下來的,因此,我們就從View類的成員函數measure開始分析應用程序窗口的測量過程,如圖2所示:
參數child用來描述當前要測量大小的子視圖,參數parentWidthMeasureSpec和parentHeightMeasureSpec用來描述當前子視圖可以獲得的最大寬度和高度,參數widthUsed和heightUsed用來描述父窗口已經使用了的寬度和高度。ViewGroup類的成員函數measureChildWithMargins必須要綜合考慮上述參數,以及當前正在測量的子視圖child所設置的大小和Margin值,還有當前視圖容器所設置的Padding值,來得到當前正在測量的子視圖child的正確寬度childWidthMeasureSpec和高度childHeightMeasureSpec,這是通過調用ViewGroup類的另外一個成員函數getChildMeasureSpec來實現的。
得到了當前正在測量的子視圖child的正確寬度childWidthMeasureSpec和高度childHeightMeasureSpec之后,就可以調用它的成員函數measure來設置它的大小了,即執行前面Step 1的操作。注意,如果當前正在測量的子視圖child描述的也是一個視圖容器,那么它又會重復執行Step 2和Step 3(遞歸)的操作,直到它的所有子孫視圖的大小都測量完成為止。
至此,我們就分析完成Android應用程序窗口的測量過程了,接下來我們繼續分析Android應用程序窗口的布局過程。
2. Android應用程序窗口的布局過程
DecorView類的成員函數layout是從父類View繼承下來的,因此,我們就從View類的成員函數layout開始分析應用程序窗口的布局過程,如圖3所示:
View類的成員函數layout首先調用另外一個成員函數setFrame來設置當前視圖的位置以及大小。設置完成之后,如果當前視圖的大小或者位置與上次相比發生了變化,那么View類的成員函數setFrame的返回值changed就會等於true。在這種情況下, View類的成員函數layout就會繼續調用另外一個成員函數onLayout重新布局當前視圖的子視圖。
此外,如果此時View類的成員變量mPrivateFlags的LAYOUT_REQUIRED位不等於0,那么也表示當前視圖需要重新布局它的子視圖,因此,這時候View類的成員函數layout也會調用另外一個成員函數onLayout。
View類的成員函數layout最后還會將成員變量mPrivateFlags的FORCE_LAYOUT位設置為0,也是因為此時當前視圖及其子視圖的布局已經是最新的了。
FrameLayout類的成員函數onLayout通過一個for循環來布局當前視圖的每一個子視圖。如果一個子視圖child是可見的,那么FrameLayout類的成員函數onLayout就會根據當前視圖可以用來顯示子視圖的區域以及它所設置的gravity屬性來得到它在應用程序窗口中的左上角位置(childeLeft,childTop)。
當一個子視圖child在應用程序窗口中的左上角位置確定了之后,再結合它在前面的測量過程中所確定的寬度width和高度height,我們就可以完全地確定它在應用程序窗口中的布局了,即可以調用它的成員函數layout來設置它的位置和大小了,這剛好就是前面的Step 1所執行的操作。注意,如果當前正在布局的子視圖child描述的也是一個視圖容器,那么它又會重復執行Step 5的操作,直到它的所有子孫視圖都布局完成為止。
至此,我們就分析完成Android應用程序窗口的布局過程了,接下來我們繼續分析Android應用程序窗口的繪制過程。
3. Android應用程序窗口的繪制過程
ViewRoot類的成員函數draw首先會創建一塊畫布,接着再在畫布上繪制Android應用程序窗口的UI,最后再將畫布的內容交給SurfaceFlinger服務來渲染,這個過程如圖4所示:
這個函數定義在文件frameworks/base/core/java/android/view/ViewRoot.java中。
ViewRoot類的成員函數draw的執行流程如下所示:
1. 將成員變量mSurface所描述的應用程序窗口的繪圖表面保存在變量surface中,以便接下來可以通過變量surface來操作應用程序窗口的繪圖表面。
2. 調用成員變量mScroller所描述的一個Scroller對象的成員函數computeScrollOffset來計算應用程序窗口是否處於正在滾動的狀態中。如果是的話,那么得到的變量scrolling就會等於true,這時候調用成員變量mScroller所描述的一個Scroller對象的成員函數getCurrY就可以得到應用程序窗口在Y軸上的即時滾動位置yoff。
3. 成員變量mScrollY用來描述應用程序窗口下一次繪制時在Y軸上應該滾動到的位置,因此,如果應用程序窗口不是處於正在滾動的狀態,那么它在下一次繪制時,就應該直接將它在Y軸上的即時滾動位置yoff設置為mScrollY。
4. 成員變量mCurScrollY用來描述應用程序窗口上一次繪制時在Y軸上的滾動位置,如果它的值不等變量yoff的值,那么就表示應用程序窗口在Y軸上的滾動位置發生變化了,這時候就需要將變量yoff的值保存在成員變量mCurScrollY中,並且將參數fullRedrawNeeded的設置為true,表示要重新繪制應用程序窗口的所有區域。
5. 成員變量mAttachInfo所描述的一個AttachInfo對象的成員變量mScalingRequired表示應用程序窗口是否正在請求進行大小縮放,如果是的話,那么所請求的大小縮放因子就保存在這個AttachInfo對象的另外一個成員變量mApplicationScale中。函數將這兩個值保存在變量scalingRequired和appScale中,以便接下來可以使用。
6. 成員變量mDirty描述的是一個矩形區域,表示應用程序窗口的臟區域,即需要重新繪制的區域。函數將這個臟區域保存變量dirty中,以便接下來可以使用。
7. 成員變量mUseGL用來描述應用程序窗口是否直接使用OpenGL接口來繪制UI。當應用程序窗口的繪圖表面的內存類型等於WindowManager.LayoutParams.MEMORY_TYPE_GPU時,那么就表示它需要使用OpenGL接口來繪制UI,以便可以利用GPU來繪制UI。當應用程序窗口需要直接使用OpenGL接口來繪制UI時,另外一個成員變量mGlCanvas就表示應用程序窗口的繪圖表面所使用的畫布,這塊畫布同樣是通過OpenGL接口來創建的。
8. 當應用程序窗口需要直接使用OpenGL接口來繪制UI時,函數接下來就會將它的UI繪制在成員變量mGlCanvas所描述的一塊畫布上,這是通過調用成員變量mView所描述的一個類型為DecorView的頂層視圖的成員函數draw來實現的。注意,在繪制之前,還需要對畫布進行適當的轉換:A. 設置畫布在Y軸上的偏移值yoff,以便可以正確反映應用程序窗口的滾動狀態;B. 如果成員變量mTranslator的值不等於null,即它指向了一個Translator對象,那么就說明應用程序窗口運行在兼容模式下,這時候就需要相應對畫布進行變換,以便可以正確反映應用程序窗口的大小;C. 當變量scalingRequired的值等於true時,同樣說明應用程序窗口是運行在兼容模式下,這時候就需要修改畫布在兼容模式下的點密度,以便可以正確地反映應用程序窗口的分辨率,注意,這時候屏幕在兼容模式下的點密度保存在DisplayMetrics類的靜態成員變量DENSITY_DEVICE中。由於上述畫布的轉換操作只針對當前的這一次繪制操作有效,因此,函數就需要在繪制之后,調用畫布的成員函數save來保存它在轉換前的矩陣變換堆棧狀態,以便在繪制完成之后,可以調用畫布的成員函數restoreToCount來恢復之前的矩陣變換堆棧狀態。
9. 使用OpenGL接口來繪制完成UI后,如果變量scrolling的值等於true,即應用程序窗口是處於正在滾動的狀態,那么就意味着應用程序窗口接下來還需要馬上進行下一次重繪,而且是所有的區域都需要重繪,因此,函數接下來就會將成員變量mFullRedrawNeeded的值設置為true,並且調用另外一個成員函數scheduleTraversals來請求執行下一次的重繪操作。
10. 以下的步驟針適用於使用非OpenGL接口來繪制UI的情況,也是本文所要關注的重點。
11. 參數fullRedrawNeeded用來描述是否需要繪制應用程序窗口的所有區域。如果需要的話,那么就會將應用程序窗口的臟區域的大小設置為整個應用程序窗口的大小(0,0,mWidth,mHeight),其中,成員變量mWidth和mHeight表示應用程序窗口的寬度和高度。注意,如果應用程序窗口的大小被設置了一個縮放因子,即變量appScale的值不等於1,那么就需要將應用程序窗口的寬度mWidth和高度mHeight乘以這個縮放因子,然后才可以得到應用程序窗口的實際大小。
12. 經過前面的一系列計算之后,如果應用程序窗口的臟區域dirty不等於空,或者應用程序窗口在正處於動畫狀態,即成員變量mIsAnimating的值等於true,那么函數接下來就需要重新繪制應用程序窗口的UI了。在繪制之前,首先會調用用來描述應用程序窗口的繪圖表面的一個Surface對象surface的成員函數lockCanvas來創建一塊畫布canvas。有了這塊畫布之后,接下來就可以調用成員變量mView所描述的一個類型為DecorView的頂層視圖的成員函數draw來在上面繪制應用程序窗口的UI了。 與前面的第8步一樣,在繪制之前,還需要對畫布進行適當的A、B和C轉換,以及需要在繪制之后恢復畫布在繪制之前的矩陣變換堆棧狀態。
13. 繪制完成之后,應用程序窗口的UI就都體現在前面所創建的畫布canvas上了,因此,這時候就需要將它交給SurfaceFlinger服務來渲染,這是通過調用用來描述應用程序窗口的繪圖表面的一個Surface對象surface的成員函數unlockCanvasAndPost來實現的。
14. 在請求SurfaceFlinger服務渲染應用程序窗口的UI之后,函數同樣是需要判斷變量scrolling的值是否等於true。如果等於的話,那么就與前面的第9步一樣,函數需要將成員變量mFullRedrawNeeded的值設置為true,並且調用另外一個成員函數scheduleTraversals來請求執行下一次的重繪操作。
在本文中,我們只關注使用非OpenGL接口來繪制應用程序窗口的UI的步驟,其中,第12步和第13步是關鍵所在。第12步調用了Java層的Surface類的成員函數lockCanvas來為應用程序窗口的繪圖表面創建了一塊畫布,並且調用了DecorView類的成員函數draw來在這塊畫布上繪制了應用程序窗口的UI,而第13步調用了Java層的Surface類的成員函數unlockCanvasAndPost來將前面已經繪制了應用程序窗口UI的畫布交給SurfaceFlinger服務來渲染