View工作流程-相關學習
Android中的Activity是作為應用程式的載體存在的,它代表一個完整的使用者介面並提供了視窗進行檢視繪製。
- 在這裡,我們這裡所說的檢視繪製,實質上就是在對View及其子類進行操作。而View作為檢視控制元件的頂層父類,在本文中會對其進行詳細分析。我們以Android的UI層級繪製體系為切入點對View進行探究。

圖1 View的層級結構
Android的UI層級繪製體系如圖1所示
繪製體系中做了這些事情 |
---|
①當呼叫 Activity 的setContentView 方法後會呼叫PhoneWindow 類的setContentView方法(PhoneWindow是抽象類Windiw的實現類,Window用來描述Activity檢視最頂端的視窗的顯示內容和行為動作)。 |
②PhoneWindow類的setContentView方法中最終會生成一個DecorView物件(DectorView是是PhoneWindow的內部類,繼承自FrameLayout)。 |
③DecorView容器中包含根佈局,根佈局中包含一個id為content的FrameLayout佈局,Activity載入佈局的xml最後通過LayoutInflater將xml檔案中的內容解析成View層級體系,最後填加到id為content的FrameLayout佈局中。 |
至此,View最終就會顯示到手機螢幕上。
二、View的檢視繪製流程剖析
1、DecorView被載入到Window中
DecorView被載入到Window的過程中,WindowManager起到了關鍵性的作用,最後交給ViewRootImpl做詳細處理,通過如下的區域性ActivityThread的原始碼分析這一點可以得到印證(在這裡我只展示核心原始碼,詳細原始碼可以在程式碼中檢視)。
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) { ActivityClientRecord r = mActivities.get(token); ... //在這裡執行performResumeActivity的方法中會執行Activity的onResume()方法 r = performResumeActivity(token, clearHide, reason); ... if (r.window == null && !a.mFinished && willBeVisible) { //PhoneWindow在這裡獲取到 r.window = r.activity.getWindow(); //DecorView在這裡獲取到 View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); //獲取ViewManager物件,在這裡getWindowManager()實質上獲取的是ViewManager的子類物件WindowManager ViewManager wm = a.getWindowManager(); ... if (r.mPreserveWindow) { ... //獲取ViewRootImpl物件 ViewRootImpl impl = decor.getViewRootImpl(); ... } if (a.mVisibleFromClient) { if (!a.mWindowAdded) { a.mWindowAdded = true; //在這裡WindowManager將DecorView新增到PhoneWindow中 wm.addView(decor, l); } ... } ... } 複製程式碼
WindowManager將DecorView新增到PhoneWindow中,即addView()方法執行時將檢視新增的動作交給了ViewRoot,ViewRoot作為介面,其實現類ViewRootImpl具體實現了addView()方法,最後,檢視的具體繪製在performTraversals()中展開,如下圖2.1所示:

圖2.1 View繪製的程式碼層級分析
2、ViewRootImpl的performTraversals()方法完成具體的檢視繪製流程
在原始碼中ViewRootImpl中檢視具體繪製的流程如下:
private void performTraversals() { // cache mView since it is used so much below... //mView就是DecorView根佈局 final View host = mView; //在Step3 成員變數mAdded賦值為true,因此條件不成立 if (host == null || !mAdded) return; //是否正在遍歷 mIsInTraversal = true; //是否馬上繪製View mWillDrawSoon = true; ... //頂層檢視DecorView所需要視窗的寬度和高度 int desiredWindowWidth; int desiredWindowHeight; ... //在構造方法中mFirst已經設定為true,表示是否是第一次繪製DecorView if (mFirst) { mFullRedrawNeeded = true; mLayoutRequested = true; //如果視窗的型別是有狀態列的,那麼頂層檢視DecorView所需要視窗的寬度和高度就是除了狀態列 if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL || lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) { // NOTE -- system code, won't try to do compat mode. Point size = new Point(); mDisplay.getRealSize(size); desiredWindowWidth = size.x; desiredWindowHeight = size.y; } else {//否則頂層檢視DecorView所需要視窗的寬度和高度就是整個螢幕的寬高 DisplayMetrics packageMetrics = mView.getContext().getResources().getDisplayMetrics(); desiredWindowWidth = packageMetrics.widthPixels; desiredWindowHeight = packageMetrics.heightPixels; } } ... //獲得view寬高的測量規格,mWidth和mHeight表示視窗的寬高,lp.width//he和lp.height表示DecorView根佈局寬和高 int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); // Ask host how big it wants to be //執行測量操作 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); ... //執行佈局操作 performLayout(lp, desiredWindowWidth, desiredWindowHeight); ... //執行繪製操作 performDraw(); } 複製程式碼
該方法主要流程就體現了View繪製渲染的三個主要步驟,分別是測量,擺放,繪製三個階段。流程圖如下圖2.2所示:

圖2.2 View的繪製流程
接下來,我們對於 performMeasure()、performLayout()、 performDraw()完成具體拆解分析。實質上最後就需要定位到View的onMeasure()、onLayout()、onDraw()方法中。
三、MeasureSpec在View體系中的作用
1、MeasureSpec的作用
首先我們從performMeasure()入手分析,在上面的內容中,我們通過原始碼可以看到 performMeasure()方法中傳入了childWidthMeasureSpec、childHeightMeasureSpec兩個int型別的值,performMeasure方法的原始碼如下所示:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure"); try { mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } } 複製程式碼
這兩個值又傳遞到mView.measure(childWidthMeasureSpec, childHeightMeasureSpec)方法中,其中measure方法的核心原始碼如下:
boolean optical = isLayoutModeOptical(this); if (optical != isLayoutModeOptical(mParent)) { Insets insets = getOpticalInsets(); int oWidth= insets.left + insets.right; int oHeight = insets.top+ insets.bottom; //根據原有寬高計算獲取不同模式下的具體寬高值 widthMeasureSpec= MeasureSpec.adjust(widthMeasureSpec,optical ? -oWidth: oWidth); heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight); } ... if (forceLayout || needsLayout) { // first clears the measured dimension flag mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; resolveRtlPropertiesIfNeeded(); int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key); if (cacheIndex < 0 || sIgnoreMeasureCache) { // measure ourselves, this should set the measured dimension flag back //在該方法中子控制元件完成具體的測量 onMeasure(widthMeasureSpec, heightMeasureSpec); ... } ... } 複製程式碼
到這裡我們應該明確,childWidthMeasureSpec, childHeightMeasureSpec是MeasureSpec根據原有寬高計算獲取不同模式下的具體寬高值。
2、MeasureSpec剖析
MeasureSpec是View的內部類,內部封裝了View的規格尺寸,以及View的寬高資訊。在Measure的流程中,系統會將View的LayoutParams根據父容器是施加的規則轉換為MeasureSpec,然後在onMeasure()方法中具體確定控制元件的寬高資訊。原始碼及分析如下所示:
public static class MeasureSpec { //int型別佔4個位元組,其中高2位表示尺寸測量模式,低30位表示具體的寬高資訊 private static final int MODE_SHIFT = 30; private static final int MODE_MASK= 0x3 << MODE_SHIFT; /** @hide */ @IntDef({UNSPECIFIED, EXACTLY, AT_MOST}) @Retention(RetentionPolicy.SOURCE) public @interface MeasureSpecMode {} //如下所示是MeasureSpec中的三種模式:UNSPECIFIED、EXACTLY、AT_MOST /** * Measure specification mode: The parent has not imposed any constraint * on the child. It can be whatever size it wants. */ public static final int UNSPECIFIED = 0 << MODE_SHIFT; /** * Measure specification mode: The parent has determined an exact size * for the child. The child is going to be given those bounds regardless * of how big it wants to be. */ public static final int EXACTLY= 1 << MODE_SHIFT; /** * Measure specification mode: The child can be as large as it wants up * to the specified size. */ public static final int AT_MOST= 2 << MODE_SHIFT; //根據尺寸測量模式跟寬高具體確定控制元件的具體寬高 public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size, @MeasureSpecMode int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } /** * Like {@link #makeMeasureSpec(int, int)}, but any spec with a mode of UNSPECIFIED * will automatically get a size of 0. Older apps expect this. * * @hide internal use only for compatibility with system widgets and older apps */ public static int makeSafeMeasureSpec(int size, int mode) { if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) { return 0; } return makeMeasureSpec(size, mode); } //獲取尺寸模式 /** * Extracts the mode from the supplied measure specification. * * @param measureSpec the measure specification to extract the mode from * @return {@link android.view.View.MeasureSpec#UNSPECIFIED}, *{@link android.view.View.MeasureSpec#AT_MOST} or *{@link android.view.View.MeasureSpec#EXACTLY} */ @MeasureSpecMode public static int getMode(int measureSpec) { //noinspection ResourceType return (measureSpec & MODE_MASK); } //獲取寬高資訊 /** * Extracts the size from the supplied measure specification. * * @param measureSpec the measure specification to extract the size from * @return the size in pixels defined in the supplied measure specification */ public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } //將控制元件的尺寸模式、寬高資訊進行拆解檢視,並對不同模式下的寬高資訊進行不同的處理 static int adjust(int measureSpec, int delta) { final int mode = getMode(measureSpec); int size = getSize(measureSpec); if (mode == UNSPECIFIED) { // No need to adjust size for UNSPECIFIED mode. return makeMeasureSpec(size, UNSPECIFIED); } size += delta; if (size < 0) { Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size + ") spec: " + toString(measureSpec) + " delta: " + delta); size = 0; } return makeMeasureSpec(size, mode); } /** * Returns a String representation of the specified measure * specification. * * @param measureSpec the measure specification to convert to a String * @return a String with the following format: "MeasureSpec: MODE SIZE" */ public static String toString(int measureSpec) { int mode = getMode(measureSpec); int size = getSize(measureSpec); StringBuilder sb = new StringBuilder("MeasureSpec: "); if (mode == UNSPECIFIED) sb.append("UNSPECIFIED "); else if (mode == EXACTLY) sb.append("EXACTLY "); else if (mode == AT_MOST) sb.append("AT_MOST "); else sb.append(mode).append(" "); sb.append(size); return sb.toString(); } } 複製程式碼
MeasureSpec的常量中指定了兩種內容,一種為尺寸模式,一種為具體的寬高資訊。其中高2位表示尺寸測量模式,低30位表示具體的寬高資訊。
尺寸測量模式有如下三種:
尺寸測量模式的3種類型 |
---|
①UNSPECIFIED:未指定模式,父容器不限制View的大小,一般用於系統內部的測量 |
②AT_MOST:最大模式,對應於在xml檔案中指定控制元件大小為wrap_content屬性,子View的最終大小是父View指定的大小值,並且子View的大小不能大於這個值 |
③EXACTLY :精確模式,對應於在xml檔案中指定控制元件為match_parent屬性或者是具體的數值,父容器測量出View所需的具體大小 |
對於每一個View,都持有一個MeasureSpec,MeasureSpec儲存了該View的尺寸測量模式以及具體的寬高資訊,MeasureSpec受自身的LayoutParams和父容器的MeasureSpec共同影響。
四、View的Measure流程分析
1、View樹的Measure測量流程邏輯圖

2、View的Measure流程分析
那麼在上文3.1的分析中,我們能夠明確在measure方法中最後呼叫onMeasure()方法完成子View的具體測量,onMeasure()方法的原始碼如下所示:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } 複製程式碼
setMeasuredDimension()方法在onMeasure()中被呼叫,被用於儲存測繪的寬度、高度,而不這樣做的話會觸發測繪時的異常。
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { boolean optical = isLayoutModeOptical(this); if (optical != isLayoutModeOptical(mParent)) { Insets insets = getOpticalInsets(); int opticalWidth= insets.left + insets.right; int opticalHeight = insets.top+ insets.bottom; measuredWidth+= optical ? opticalWidth: -opticalWidth; measuredHeight += optical ? opticalHeight : -opticalHeight; } setMeasuredDimensionRaw(measuredWidth, measuredHeight); } 複製程式碼
在setMeasuredDimension()方法中傳入的是getDefaultSize(),接著分析getDefaultSize()中做了哪些操作:
public static int getDefaultSize(int size, int measureSpec) { int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break; } return result; } 複製程式碼
通過上文對MeasureSpec的分析,在這裡我們就能明確,getDefaultSize實質上就是根據測繪模式確定子View的具體大小,而對於自定義View而言,子View的寬高資訊不僅由自身決定,如果它被包裹在ViewGroup中就需要具體測量得到其精確值。
3、View的Measure過程中遇到的問題以及解決方案
View 的measure過程和Activity的生命週期方法不是同步執行的,因此無法保證Activity執行了onCreate、onStart、onResume時某個View已經測量完畢了。如果View還沒有測量完畢,那麼獲得的寬和高都是0。下面是3種解決該問題的方法:
①Activity/View的onWindowsChanged()方法
onWindowFocusChanged()方法表示 View 已經初始化完畢了,寬高已經準備好了,這個時候去獲取是沒問題的。這個方法會被呼叫多次,當Activity繼續執行或者暫停執行的時候,這個方法都會被呼叫,程式碼如下:
public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); if(hasWindowFocus){ int width=view.getMeasuredWidth(); int height=view.getMeasuredHeight(); } } 複製程式碼
②View.post(runnable)方法
通過post將一個 Runnable投遞到訊息佇列的尾部,然後等待Looper呼叫此runnable的時候View也已經初始化好了
@Override protected void onStart() { super.onStart(); view.post(new Runnable() { @Override public void run() { int width=view.getMeasuredWidth(); int height=view.getMeasuredHeight(); } }); } 複製程式碼
③ViewTreeObsever使用 ViewTreeObserver 的眾多回調方法可以完成這個功能,比如使用onGlobalLayoutListener 介面,當 View樹的狀態發生改變或者View樹內部的View的可見性發生改變時,onGlobalLayout 方法將被回撥。伴隨著View樹的變化,這個方法也會被多次呼叫。
@Override protected void onStart() { super.onStart(); ViewTreeObserver viewTreeObserver=view.getViewTreeObserver(); viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { view.getViewTreeObserver().removeOnGlobalLayoutListener(this); int width=view.getMeasuredWidth(); int height=view.getMeasuredHeight(); } }); } 複製程式碼
當然,在這裡你可以通過setMeasuredDimension()方法對子View的具體寬高以及測量模式進行指定。
五、View的layout流程分析
1、View樹的layout擺放流程邏輯圖

2、View的layout流程分析
layout 的作用是ViewGroup來確定子元素的位置,當 ViewGroup 的位置被確定後,在layout中會呼叫onLayout ,在onLayout中會遍歷所有的子元素並呼叫子元素的 layout 方法。
在程式碼中設定View的成員變數 mLeft,mTop,mRight,mBottom 的值,這幾個值是在螢幕上構成矩形區域的四個座標點,就是該View顯示的位置,不過這裡的具體位置都是相對與父檢視的位置而言,而 onLayout 方法則會確定所有子元素位置,ViewGroup在onLayout函式中通過呼叫其children的layout函式來設定子檢視相對與父檢視中的位置,具體位置由函式 layout 的引數決定。下面我們先看View的layout 方法(只展示關鍵性程式碼)如下:
/* *@param l view 左邊緣相對於父佈局左邊緣距離 *@param t view 上邊緣相對於父佈局上邊緣位置 *@param r view 右邊緣相對於父佈局左邊緣距離 *@param b view 下邊緣相對於父佈局上邊緣距離 */ public void layout(int l, int t, int r, int b) { ... //記錄 view 原始位置 int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; //呼叫 setFrame 方法 設定新的 mLeft、mTop、mBottom、mRight 值, //設定 View 本身四個頂點位置 //並返回 changed 用於判斷 view 佈局是否改變 boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); //第二步,如果 view 位置改變那麼呼叫 onLayout 方法設定子 view 位置 if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { //呼叫 onLayout onLayout(changed, l, t, r, b); ... } ... } 複製程式碼
六、View的draw流程分析
1、View樹的draw繪製流程邏輯圖

2、View的draw流程分析
在View的draw()方法的註釋中,說明了繪製流程中具體每一步的作用,原始碼中對於draw()方法的註釋如下,我們在這裡重點分析註釋中除第2、第5步外的其他步驟。
/* * Draw traversal performs several drawing steps which must be executed * in the appropriate order: * *1. Draw the background(繪製背景) *2. If necessary, save the canvas' layers to prepare for fading(如果需要的話,儲存畫布背景以展示漸變效果) *3. Draw view's content(繪製View的內容) *4. Draw children(繪製子View) *5. If necessary, draw the fading edges and restore layers(如果需要的話,繪製漸變邊緣並恢復畫布圖層。) *6. Draw decorations (scrollbars for instance)(繪製裝飾(例如滾動條scrollbar)) */ 複製程式碼
①View中的drawBackground()繪製背景
核心原始碼如下:
private void drawBackground(Canvas canvas) { final Drawable background = mBackground; if (background == null) { return; } ... final int scrollX = mScrollX; final int scrollY = mScrollY; if ((scrollX | scrollY) == 0) { background.draw(canvas); } else { canvas.translate(scrollX, scrollY); background.draw(canvas); canvas.translate(-scrollX, -scrollY); } } 複製程式碼
如果背景有偏移,實質上對畫布首先做偏移處理,然後在其上進行繪製。
②View內容的繪製
View內容的繪製原始碼如下所示:
protected void onDraw(Canvas canvas) { } 複製程式碼
該方法是空實現,就根據不同的內容進行不同的設定,自定義View中就需要重寫該方法加入我們自己的業務邏輯。
③子View的繪製
子View的繪製原始碼如下所示:
protected void dispatchDraw(Canvas canvas) { } 複製程式碼
該方法同樣為空實現,而對於ViewGroup而言對子View進行遍歷,並最終呼叫子View的onDraw方法進行繪製。
④裝飾繪製
裝飾繪製的原始碼如下所示(只展示核心原始碼):
public void onDrawForeground(Canvas canvas) { //繪製前景裝飾 onDrawScrollIndicators(canvas); onDrawScrollBars(canvas); ... foreground.draw(canvas); } 複製程式碼
很明顯,在這裡onDrawForeground()方法用於繪製例如ScrollBar等其他裝飾,並將它們顯示在檢視的最上層。
七、檢視重繪
1、requestLayout重新繪製檢視
子View呼叫requestLayout方法,會標記當前View及父容器,同時逐層向上提交,直到ViewRootImpl處理該事件,ViewRootImpl會呼叫三大流程,從measure開始,對於每一個含有標記位的view及其子View都會進行測量、佈局、繪製。
2、invalidate在UI執行緒中重新繪製檢視
當子View呼叫了invalidate方法後,會為該View新增一個標記位,同時不斷向父容器請求重新整理,父容器通過計算得出自身需要重繪的區域,直到傳遞到ViewRootImpl中,最終觸發performTraversals方法,進行開始View樹重繪流程(只繪製需要重繪的檢視)。
3、postInvalidate在非UI執行緒中重新繪製檢視
這個方法與invalidate方法的作用是一樣的,都是使View樹重繪,但兩者的使用條件不同,postInvalidate是在非UI執行緒中呼叫,invalidate則是在UI執行緒中呼叫。
- 總結一下 一般來說,如果View確定自身不再適合當前區域,比如說它的LayoutParams發生了改變,需要父佈局對其進行重新測量、擺放、繪製這三個流程,往往使用requestLayout。而invalidate則是重新整理當前View,使當前View進行重繪,不會進行測量、佈局流程,因此如果View只需要重繪而不需要測量,佈局的時候,使用invalidate方法往往比requestLayout方法更高效。
參考文章: 連結:https://www.jianshu.com/p/af266ff378c6