Android O: View的绘制流程(二):测量
在前一篇博客Android O: View的繪制流程(一): 創建和加載中,?
我們分析了系統創建和加載View的過程,這部分內容完成了View繪制的前置工作。
本文開始分析View的測量的流程。
一、繪制流程的起點?
在分析View的測量的流程前,我們先來尋找一下界面繪制流程的起點。?
當Activity啟動時,會調用ActivityThread的handleLaunchActivity方法:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
我們跟進一下handleResumeActivity函數:
final void handleResumeActivity(IBinder token,boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {..........// 會回調Activity的onResume接口r = performResumeActivity(token, clearHide, reason);if (r != null) {final Activity a = r.activity;.........if (r.window == null && !a.mFinished && willBeVisible) {//之前已經創建出Activity對應的PhoneWindow和DecorView//將這些對象記錄到ActivityRecord中r.window = r.activity.getWindow();View decor = r.window.getDecorView();decor.setVisibility(View.INVISIBLE);//得到WindowManagerImplViewManager wm = a.getWindowManager();WindowManager.LayoutParams l = r.window.getAttributes();a.mDecor = decor;..........//如果Activity可見if (a.mVisibleFromClient) {if (!a.mWindowAdded) {a.mWindowAdded = true;//Activity的DecorView遞交給WindowManagerwm.addView(decor, l);} else {...........}}} else if (...) {.......}.......} else {.........} }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
從上述代碼可以看出,解析完XML對應的View后,?
最終將DecorView遞交給WindowManager。
我們跟進一下WindowManagerImpl中的addView函數:
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {applyDefaultToken(params);//實際上定義于WindowManagerGlobal中mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);}- 1
- 2
- 3
- 4
- 5
繼續跟進WindowManagerGlobal中的代碼:
public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {............final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;//進一步調整wparams.............ViewRootImpl root;View panelParentView = null;synchronized (mLock) {..........//創建出View對應的ViewRootroot = new ViewRootImpl(view.getContext(), display);view.setLayoutParams(wparams);mViews.add(view);mRoots.add(root);mParams.add(wparams);try {//關聯View和ViewRootImplroot.setView(view, wparams, panelParentView);} catch (RuntimeException e) {...........}} }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
至此我們知道了,WindowManager將DecorView和對應的ViewRootImpl關聯起來了。?
現在來一起看看ViewRootImpl的setView函數:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
容易看出ViewRootImpl與View關聯后,會調用requestLayout函數,?
該函數將開啟整個繪制流程。
眼見為實,我們來看看這個requestLayout函數:
public void requestLayout() {if (!mHandlingLayoutInLayoutRequest) {checkThread();mLayoutRequested = true;//繼續跟進scheduleTraversals();}}void scheduleTraversals() {//mTraversalScheduled用于限制繪制的次數if (!mTraversalScheduled) {mTraversalScheduled = true;...........//將mTraversalRunnable加入執行隊列mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);...........}}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
最后,我們來看看TraversalRunnable的實現:
final class TraversalRunnable implements Runnable {@Overridepublic void run() {doTraversal();}}- 1
- 2
- 3
- 4
- 5
- 6
TraversalRunnable在執行時,會調用doTraversal函數,對應代碼如下:
void doTraversal() {if (mTraversalScheduled) {mTraversalScheduled = false;..........//開始繪制了performTraversals();..........} }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
繪制的主要邏輯定義于ViewRootImpl的performTraversals中,?
該函數會遍歷整個視圖書,逐一繪制每個View。
performTraversals函數接近1000行左右且涉及較多瑣碎的細節,?
個人感覺沒有逐行解析的必要,因此我們主僅關注主要的邏輯。
實際上performTraversals的代碼流程可以大致分為三個階段,如下所示:
private void performTraversals() {.............// 測量階段int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);............performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);............// 布局階段performLayout(lp, mWidth, mHeight);............// 繪制階段performDraw(); }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
總結一下上述整個代碼的調用流程,大致如下所示:?
二、Measure階段?
將performTraversals劃分為三個階段后,整體的邏輯就可以簡化為下圖:?
現在我們來看看其中Measure階段的代碼。
2.1 MeasureSpec?
在分析測量的代碼前,我們先要了解一下MeasureSpec的概念。?
MeasureSpec是定義于View.java中的內部類,表示一個32位的整形值。?
它的高2位表示測量模式SpecMode,低30位表示在相應模式下的測量尺寸SpecSize。
目前SpecMode的取值可以為以下三種:
/*** 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;- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
如注釋所述:?
UNSPECIFIED?表示不指定測量模式,對應的場景是:?
父視圖沒有限制子試圖大小,子試圖可以是想要的任何尺寸。?
這種模式基本用不到。
EXACTLY?表示精確測量模式,對應的場景是:?
父視圖已經指定了子試圖的精確大小,此時測量值就是SpecSize的值。?
當視圖的layout_width或者layout_height指定為具體的數值,?
或指定為match_parent時,該模式生效。
AT_MOST?表示最大值模式,對應的場景是:?
當視圖的layout_width或layout_height指定為wrap_content時,?
子視圖的尺寸可以是不超過父視圖允許最大值的任何尺寸。
我們來看看前文代碼中的getRootMeasureSpec函數:
private static int getRootMeasureSpec(int windowSize, int rootDimension) {int measureSpec;switch (rootDimension) {case ViewGroup.LayoutParams.MATCH_PARENT:// Window can't resize. Force root view to be windowSize.measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);break;case ViewGroup.LayoutParams.WRAP_CONTENT:// Window can resize. Set max size for root view.measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);break;default:// Window wants to be an exact size. Force root view to be that size.measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);break;}return measureSpec; }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
從代碼來看,根據LayoutParams的參數,getRootMeasureSpec會得到對應模式的MeasureSpec。?
其中主要用到的還是EXACTLY和AT_MOST模式。
對于DecorView而言,它的MeasureSpec由窗口尺寸和其自身的LayoutParams共同決定;?
對于普通的View,它的MeasureSpec由父視圖的MeasureSpec和自身的LayoutParams共同決定。
2.2 measure?
了解完MeasureSpec后,我們來看看performMeasure函數:
- 1
- 2
- 3
- 4
- 5
- 6
我們跟進View的measure函數:
//參數為父ViewGroup對當前View的約束信息 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {//當前View為ViewGroup且設置為視覺邊界布局模式時,才返回trueboolean optical = isLayoutModeOptical(this);//當前View與父容器的模式不同時,需要調整MeasureSpecif (optical != isLayoutModeOptical(mParent)) {............widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);}// Suppress sign extension for the low bytes// 計算key值, 用于判斷是否有緩存及作為存儲鍵值long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);//判斷是否需要強制重新布局//例如View調用requestLayout時,會在mPrivateFlags添加該標記final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;// 以下其實就是判斷是否需要重新布局// Optimize layout by avoiding an extra EXACTLY pass when the view is// already measured as the correct size. In API 23 and below, this// extra pass is required to make LinearLayout re-distribute weight.final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec|| heightMeasureSpec != mOldHeightMeasureSpec;final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);final boolean needsLayout = specChanged&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);//強制重新布局或需要重新布局if (forceLayout || needsLayout) {// first clears the measured dimension flagmPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;//嘗試解析RTL相關的屬性resolveRtlPropertiesIfNeeded();//沒有forceLayout時,嘗試從緩存獲取int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);//獲取緩存失敗,或忽略緩存時,才開始測量if (cacheIndex < 0 || sIgnoreMeasureCache) {// measure ourselves, this should set the measured dimension flag backonMeasure(widthMeasureSpec, heightMeasureSpec);mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;} else {//命中緩存,直接從緩存取結果long value = mMeasureCache.valueAt(cacheIndex);// Casting a long to int drops the high 32 bits, no mask neededsetMeasuredDimensionRaw((int) (value >> 32), (int) value);mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;}...........}mOldWidthMeasureSpec = widthMeasureSpec;mOldHeightMeasureSpec = heightMeasureSpec;//存入緩存mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
從上面的代碼來看,當需要(包括強制)重新布局且不使用(包括無緩存)緩存數據時,?
才會調用onMeasure進行View的測量工作。
上述代碼的整體流程,大致如下圖所示:?
2.3 ViewGroup的onMeasure?
onMeasure函數一般會被View的子類覆蓋,因此對于DecorView而言,?
實際調用的應該是FrameLayout的onMeasure方法。
我們來跟進一下FrameLayout的onMeasure方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {//FrameLayout是ViewGroup的子類, 此處獲取子View的數量int count = getChildCount();//長或寬的SpecMode不為EXACTLY時, measureMatchParentChildren置為true//意味著ViewGroup的長或寬為wrap_contentfinal boolean measureMatchParentChildren =MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;mMatchParentChildren.clear();int maxHeight = 0;int maxWidth = 0;int childState = 0;//依次measure子Viewfor (int i = 0; i < count; i++) {final View child = getChildAt(i);//判斷能否measure該子Viewif (mMeasureAllChildren || child.getVisibility() != GONE) {//具體的測量函數measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);//不斷迭代出子View需要的最大寬度和最大高度final LayoutParams lp = (LayoutParams) child.getLayoutParams();maxWidth = Math.max(maxWidth,child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);maxHeight = Math.max(maxHeight,child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);//迭代childStatechildState = combineMeasuredStates(childState, child.getMeasuredState());if (measureMatchParentChildren) {if (lp.width == LayoutParams.MATCH_PARENT ||lp.height == LayoutParams.MATCH_PARENT) {//統計matchParentChildrenmMatchParentChildren.add(child);}}}}// Account for padding too// 最大寬度和高度需要疊加paddingmaxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();// Check against our minimum height and width// 需要判斷是否滿足設置的最小寬高的要求maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());// Check against our foreground's minimum height and width// 還需要滿足前景圖像的要求final Drawable drawable = getForeground();if (drawable != null) {maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());}//經過了以上一系列步驟后,我們就得到了ViewGroup的maxHeight和maxWidth的最終值//表示當前容器用這個尺寸就能夠正常顯示其所有子View//此處resolveSizeAndState根據數值、MeasureSpec和childState計算出最終的數值//然后用setMeasuredDimension保存到mMeasuredWidth與mMeasuredHeight成員變量 (定義于View.java)setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),resolveSizeAndState(maxHeight, heightMeasureSpec,childState << MEASURED_HEIGHT_STATE_SHIFT));//部分子View需要做最后的測量//當ViewGroup存在wrap_content的size(初始時,未明確定義大小)//且child View存在match_parent的size時(需要依賴父容器)//那么父容器計算完畢后,這類child view需要重新測量count = mMatchParentChildren.size();if (count > 1) {for (int i = 0; i < count; i++) {//根據父容器的參數生成新的約束條件............//重新測量child.measure(childWidthMeasureSpec, childHeightMeasureSpec);}} }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
從以上代碼的執行流程,我們可以看到,作為容器的ViewGroup,?
將通過measureChildWithMargins方法,對所有子View進行測量,?
然后才會計算自身的測量結果。
FrameLayout的onMeasure函數整體流程可以概括為下圖:?
2.4 measureChildWithMargins?
接下來,我們來看下ViewGroup的measureChildWithMargins方法:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
從上述代碼可以看出,ViewGroup會利用getChildMeasureSpec函數計算出子View的約束條件,?
然后再調用子View的measure函數。
我們看看getChildMeasureSpec函數:
// 從measureChildWithMargins函數,可以看出: // spec為父View的MeasureSpec // padding為父View在相應方向的已用尺寸, 加上父View的padding和子View的margin // childDimension為子View的LayoutParams相應方向的值 public static int getChildMeasureSpec(int spec, int padding, int childDimension) {int specMode = MeasureSpec.getMode(spec);int specSize = MeasureSpec.getSize(spec);//得到父View在相應方向上的可用大小int size = Math.max(0, specSize - padding);//保存最終結果int resultSize = 0;int resultMode = 0;switch (specMode) {// Parent has imposed an exact size on uscase MeasureSpec.EXACTLY:// 表示子View的LayoutParams指定了具體大小值(xx dp)if (childDimension >= 0) {resultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {// Child wants to be our size. So be it.// 子View為match_parentresultSize = size;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.WRAP_CONTENT) {// Child wants to determine its own size. It can't be// bigger than us.// 子View為wrap_contentresultSize = size;resultMode = MeasureSpec.AT_MOST;}break;// Parent has imposed a maximum size on uscase MeasureSpec.AT_MOST:if (childDimension >= 0) {// Child wants a specific size... so be itresultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {// Child wants to be our size, but our size is not fixed.// Constrain child to not be bigger than us.resultSize = size;resultMode = MeasureSpec.AT_MOST;} else if (childDimension == LayoutParams.WRAP_CONTENT) {// Child wants to determine its own size. It can't be// bigger than us.resultSize = size;resultMode = MeasureSpec.AT_MOST;}break;case MeasureSpec.UNSPECIFIED://不關注................}//noinspection ResourceTypereturn MeasureSpec.makeMeasureSpec(resultSize, resultMode); }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
上面的方法展現了根據父View的MeasureSpec和子View的LayoutParams生成子View的MeasureSpec的過程,?
從代碼可以看出:?
當子View指定了具體的大小時,resultSize就是指定的size,resultMode為EXACTLY,?
父View對其沒有影響;?
當子View指定為MATCH_PARENT時,resultSize為父View可用的size,?
resultMode與父View一致;?
當子View指定為WRAP_CONTENT時,resultSize為父View可用的size,?
resultMode為AT_MOST。
從前文我們知道,獲取完子View的MeasureSpec后,?
measureChildWithMargins就會調用子View的measure方法。?
對于ViewGroup及其子類而言,將重新遞歸調用ViewGroup的onMeasure方法;?
對于View及其子類而言,將調用View的onMeasure方法。
由于measureChildWithMargins會遞歸調用ViewGroup的onMeasure方法,?
可以看出整個View的測量順序是先序遍歷的,但最終計算結果時是后序遍歷的,?
即子View測量完畢后,才能得到父View的size。
2.5 View的onMeasure?
現在我們跟進一下View的onMeasure方法:
- 1
- 2
- 3
- 4
從代碼可以看出,普通View只需要完成自身的測量工作即可。?
View以getDefaultSize方法的返回值來作為測量結果,通過setMeasuredDimension方法進行設置。
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;// AT_MOST和EXACTLY這兩種情況都返回了SpecSize作為result// 自定義View直接繼承View類時,需要自己實現// 否則wrap_content就和match_parent效果一樣case MeasureSpec.AT_MOST:case MeasureSpec.EXACTLY:result = specSize;break;}return result;}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
三、總結?
至此,我們大致了解了View的測量流程。?
個人覺得重點在于了解MeasureSpec對測量過程的影響,?
同時知道測量的順序是先序遍歷,計算最終結果是后序遍歷即可。
此外,當父容器的寬或高為wrap_content,其子View的寬或高為match_parent時,?
父容器得到最終的寬、高后,需要重新測量這部分子View。
下一篇博客,我們將繼續關注View繪制的布局流程。
版權聲明:轉載請注明:http://blog.csdn.net/gaugamela/article 與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的Android O: View的绘制流程(二):测量的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android内存分析工具:Memory
- 下一篇: How to Build Your Ow