1 概念
有分析到Activity中界面加載顯示的基本流程原理,記不記得最終分析結果就是下面的關係,id爲content的內容就是整個View樹的結構,所以對每個具體View對象的操作,其實就是個遞歸的實現。
整個View樹的繪圖流程是在ViewRootImpl類的performTraversals()方法開始的(在上一篇博客—-Android系統分析之Window的視圖對象的創建過程分析-的最後階段分析到),該函數做的執行過程主要是根據之前設置的狀態,判斷是否重新計算視圖大小(measure)、是否重新放置視圖的位置(layout)、以及是否重繪 (draw),其核心也就是通過判斷來選擇順序執行這三個方法中的哪個,如下:
private void performTraversals() {
......
//最外層的根視圖的widthMeasureSpec和heightMeasureSpec由來
//lp.width和lp.height在創建ViewGroup實例時等於MATCH_PARENT
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
......
performMeasure(childWidthMeasureSpec,childHeightMeasureSpec);
......
performLayout(lp, mWidth, mHeight);
......
performDraw();
......
}
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
//測Root View,基於它的佈局參數,計算出窗口中根視圖的度量標準
//上面傳入參數後這個函數走的是MATCH_PARENT,使用MeasureSpec.makeMeasureSpec方法
//組裝一個MeasureSpec,MeasureSpec的specMode等於EXACTLY,specSize等於windowSize,也就是爲何根視圖總是全屏的原因。
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;
......
}
return measureSpec;
}
其中的mView就是View對象。如下就是整個流程的大致流程圖:
2 View繪製流程第一步:遞歸measure源碼分析
2.1 measure源碼分析
(1)先看下View的measure方法源碼,如下:
//final方法,子類不可重寫
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
......
//回調onMeasure()方法
onMeasure(widthMeasureSpec, heightMeasureSpec);
......
}
爲整個View樹計算實際的大小,然後設置實際的高和寬,每個View控件的實際寬高都是由父視圖和自身決定的。實際的測量是在onMeasure方法進行,所以在View的子類需要重寫onMeasure方法,這是因爲measure方法是final的,不允許重載,所以View子類只能通過重載onMeasure來實現自己的測量邏輯。
在這裏可以看出measure方法最終回調了View的onMeasure方法,我們來看下View的onMeasure源碼,如下:
//View的onMeasure默認實現方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
測量的結果怎麼辦?onMeasure默認的實現僅僅調用了setMeasuredDimension,setMeasuredDimension函數是一個很關鍵的函數,它對View的成員變量mMeasuredWidth和mMeasuredHeight變量賦值,measure的主要目的就是對View樹中的每個View的mMeasuredWidth和mMeasuredHeight進行賦值,所以一旦這兩個變量被賦值意味着該View的測量工作結束。
對於非ViewGroup的View而言,通過調用上面默認的onMeasure即可完成View的測量。如果是ViewGroup就需要測量裏面的所有childview。
到此一次最基礎的元素View的measure過程就完成了。上面說了View實際是嵌套的,而且measure是遞歸傳遞的,所以每個View都需要measure。實際能夠嵌套的View一般都是ViewGroup的子類,所以在ViewGroup中定義了measureChildren, measureChild, measureChildWithMargins方法來對子視圖進行測量,measureChildren內部實質只是循環調用measureChild,measureChild和measureChildWithMargins的區別就是是否把margin和padding也作爲子視圖的大小。如下我們以ViewGroup中稍微複雜的measureChildWithMargins方法來分析:
//對父視圖提供的measureSpec參數結合自身的LayoutParams參數進行了調整,然後再來調用child.measure()方法
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
//獲取子視圖的LayoutParams
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//調整MeasureSpec
//通過這兩個參數以及子視圖本身的LayoutParams來共同決定子視圖的測量規格
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
//調運子View的measure方法,子View的measure中會回調子View的onMeasure方法
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
2.2 measure原理總結
通過上面分析可以看出measure過程主要就是從頂層父View向子View遞歸調用view.measure方法(measure中又回調onMeasure方法)的過程。具體measure核心主要有如下幾點:
- MeasureSpec(View的內部類)測量規格爲int型,值由高2位規格模式specMode和低30位具體尺寸specSize組成。其中specMode只有三種值:
MeasureSpec.EXACTLY //確定模式,父View希望子View的大小是確定的,由specSize決定;
MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值;
MeasureSpec.UNSPECIFIED //未指定模式,父View完全依據子View的設計值來決定;
- View的measure方法是final的,不允許重載,View子類只能重載onMeasure來完成自己的測量邏輯。
- 最頂層DecorView測量時的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法確定的(LayoutParams寬高參數均爲MATCH_PARENT,specMode是EXACTLY,specSize爲物理屏幕大小)。
- ViewGroup類提供了measureChild,measureChild和measureChildWithMargins方法,簡化了父子View的尺寸計算。
- 只要是ViewGroup的子類就必須要求LayoutParams繼承子MarginLayoutParams,否則無法使用layout_margin參數。
- View的佈局大小由父View和子View共同決定。
- 使用View的getMeasuredWidth()和getMeasuredHeight()方法來獲取View測量的寬高,必須保證這兩個方法在onMeasure流程之後被調用才能返回有效值。
3 View繪製流程第二步:遞歸layout源碼分析
3.1 layout源碼分析
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
//layout方法接收四個參數,這四個參數分別代表相對Parent的左、上、右、下座標。而且還可以看見左上都爲0,右下分別爲上面剛剛測量的width和height。
view.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
}
類似measure過程,lauout調運了onLayout方法。
3.2 layout原理總結
整個layout過程比較容易理解,從上面分析可以看出layout也是從頂層父View向子View的遞歸調用view.layout方法的過程,即父View根據上一步measure子View所得到的佈局大小和佈局參數,將子View放在合適的位置上。
具體layout核心主要有以下幾點:
- View.layout方法可被重載,ViewGroup.layout爲final的不可重載,ViewGroup.onLayout爲abstract的,子類必須重載實現自己的位置邏輯。
- measure操作完成後得到的是對每個View經測量過的measuredWidth和measuredHeight,layout操作完成之後得到的是對每個View進行位置分配後的mLeft、mTop、mRight、mBottom,這些值都是相對於父View來說的。
- 使用View的getWidth()和getHeight()方法來獲取View測量的寬高,必須保證這兩個方法在onLayout流程之後被調用才能返回有效值。
4 View繪製流程第三步:遞歸draw源碼分析
4.1 draw源碼分析
private void performDraw() {
draw(fullRedrawNeeded);
}
public void draw(Canvas canvas) {
......
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background,對View的背景進行繪製
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content,對View的內容進行繪製
* 4. Draw children,對當前View的所有子View進行繪製,如果當前的View沒有子View就不需要進行繪製
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance),對View的滾動條進行繪製
*/
// Step 1, draw the background, if needed
......
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
......
// Step 2, save the canvas' layers
......
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
......
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
......
if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, right, top + length, p);
}
......
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
......
}
5 View的invalidate和postInvalidate方法源碼分析
在上面分析View的三步繪製流程中,高頻率調用一個叫invalidate的方法,我們下面對此進行分析。View調用invalidate方法的實質是層層上傳到父級,直到傳遞到ViewRootImpl後觸發了scheduleTraversals方法,然後整個View樹開始重新按照上面分析的View繪製流程進行重繪任務。
5.1 區別
(1)invalidate():在主線程當中刷新;
(2)postInvalidate():在子線程當中刷新;其實最終調用的就是invalidate。原理依然是通過子線程向主線程發送消息這一機制。
5.2 invalidate方法源碼分析
(1)View的invalidate(invalidateInternal)方法
/**
* Mark the area defined by dirty as needing to be drawn. If the view is
* visible, {@link #onDraw(android.graphics.Canvas)} will be called at some point in the future.
* This must be called from a UI thread. To call from a non-UI thread, call
* {@link #postInvalidate()}.
* <b>WARNING:</b> In API 19 and below, this method may be destructive to {@code dirty}.
* @param dirty the rectangle representing the bounds of the dirty region
*/
//public,只能在UI Thread中使用,別的Thread用postInvalidate方法,View是可見的纔有效,回調onDraw方法,針對局部View
public void invalidate(Rect dirty) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
//實質還是調運invalidateInternal方法
invalidateInternal(dirty.left - scrollX, dirty.top - scrollY,
dirty.right - scrollX, dirty.bottom - scrollY, true, false);
}
/**
* Mark the area defined by the rect (l,t,r,b) as needing to be drawn. The
* coordinates of the dirty rect are relative to the view. If the view is
* visible, {@link #onDraw(android.graphics.Canvas)} will be called at some point in the future.
* This must be called from a UI thread. To call from a non-UI thread, call
* {@link #postInvalidate()}.
*/
//看見上面註釋沒有?public,只能在UI Thread中使用,別的Thread用postInvalidate方法,View是可見的纔有效,回調onDraw方法,針對局部View
public void invalidate(int l, int t, int r, int b) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
//實質還是調運invalidateInternal方法
invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false);
}
/**
* Invalidate the whole view. If the view is visible,
* {@link #onDraw(android.graphics.Canvas)} will be called at some point in the future.
* This must be called from a UI thread. To call from a non-UI thread, call
*/
//public,只能在UI Thread中使用,別的Thread用postInvalidate方法,View是可見的纔有效,回調onDraw方法,針對整個View
public void invalidate() {
//invalidate的實質還是調運invalidateInternal方法
invalidate(true);
}
/**
* This is where the invalidate() work actually happens. A full invalidate()
* causes the drawing cache to be invalidated, but this function can be
* called with invalidateCache set to false to skip that invalidation step
* for cases that do not need it (for example, a component that remains at
* the same dimensions with the same content).
*
* @param invalidateCache Whether the drawing cache for this view should be
* invalidated as well. This is usually true for a full
* invalidate, but may be set to false if the View's contents or
* dimensions have not changed.
*/
//default的權限,只能在UI Thread中使用,別的Thread用postInvalidate方法,View是可見的纔有效,回調onDraw方法,針對整個View
void invalidate(boolean invalidateCache) {
//實質還是調運invalidateInternal方法
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
//!!!!!!看見沒有,這是所有invalidate的終極調用方法!!!!!!
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
......
// Propagate the damage rectangle to the parent view.
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
//設置刷新區域
damage.set(l, t, r, b);
//傳遞調運Parent--ViewGroup的invalidateChild方法
p.invalidateChild(this, damage);
}
......
}
(2)ViewGroup的invalidateChild方法
View的invalidate(invalidateInternal)方法實質是將要刷新區域直接傳遞給了父ViewGroup的invalidateChild方法,在invalidate中,調用父View的invalidateChild,這是一個從當前向上級父View回溯的過程,每一層的父View都將自己的顯示區域與傳入的刷新Rect做交集 。所以我們看下ViewGroup的invalidateChild方法,源碼如下:
public final void invalidateChild(View child, final Rect dirty) {
ViewParent parent = this;
final AttachInfo attachInfo = mAttachInfo;
......
do {
......
if (parent instanceof ViewRootImpl) {
}
//循環層層上級調用,直到ViewRootImpl會返回null
parent = parent.invalidateChildInParent(location, dirty);
......
} while (parent != null);
}
(3)ViewRootImpl的invalidateChildInParent方法
這個過程最後傳遞到ViewRootImpl的invalidateChildInParent方法結束,所以我們看下ViewRootImpl的invalidateChildInParent方法,如下:
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
//invalidate()
invalidate();
return null;
}
這個ViewRootImpl類的invalidateChildInParent方法直接返回了null,也就是上面ViewGroup中說的,層層上級傳遞到ViewRootImpl的invalidateChildInParent方法結束了那個do while循環。
void invalidate() {
mDirty.set(0, 0, mWidth, mHeight);
if (!mWillDrawSoon) {
//scheduleTraversals()
scheduleTraversals();
}
}
scheduleTraversals會通過Handler的Runnable發送一個異步消息,調運doTraversal方法,然後最終調用performTraversals()執行重繪。
5.3 postInvalidate方法源碼分析
(1)View的postInvalidate()方法
public void postInvalidate() {
//postInvalidateDelayed()
postInvalidateDelayed(0);
}
public void postInvalidateDelayed(long delayMilliseconds) {
final AttachInfo attachInfo = mAttachInfo;
//核心,實質就是調運了ViewRootImpl.dispatchInvalidateDelayed方法
if (attachInfo != null) {
attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
}
}
(2)ViewRootImpl類的dispatchInvalidateDelayed方法
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}
通過ViewRootImpl類的Handler發送了一條MSG_INVALIDATE消息,繼續追蹤這條消息的處理可以發現,實質就是又在UI Thread中調運了View的invalidate();方法。
public void handleMessage(Message msg) {
......
switch (msg.what) {
case MSG_INVALIDATE:
//invalidate()
((View) msg.obj).invalidate();
break;
......
}
......
}
5.4 invalidate與postInvalidate方法總結
invalidate系列方法請求重繪View樹(也就是draw方法),如果View大小沒有發生變化就不會調用layout過程,並且只繪製那些“需要重繪的”View,也就是哪個View(View只繪製該View,ViewGroup繪製整個ViewGroup)請求invalidate系列方法,就繪製該View。
(1)依據上面對View的invalidate分析我總結繪製如下流程圖:
(2)依據上面對View的postInvalidate分析我總結繪製如下流程圖:
(3)常見的引起invalidate方法操作的原因主要有:
- 直接調用invalidate方法.請求重新draw,但只會繪製調用者本身。
- 觸發setSelection方法。請求重新draw,但只會繪製調用者本身。
- 觸發setVisibility方法。 當View可視狀態在INVISIBLE轉換VISIBLE時會間接調用invalidate方法,繼而繪製該View。當View的可視狀態在INVISIBLE\VISIBLE 轉換爲GONE狀態時會間接調用requestLayout和invalidate方法,同時由於View樹大小發生了變化,所以會請求measure過程以及draw過程,同樣只繪製需要“重新繪製”的視圖。
- 觸發setEnabled方法。請求重新draw,但不會重新繪製任何View包括該調用者本身。
- 觸發requestFocus方法。請求View樹的draw過程,只繪製“需要重繪”的View。
6 相關問答題
6.1 requestlayout, onlayout, onDraw, drawChild區別與聯繫
(1)requestLayout()方法:會導致調用Measure()方法和layout(),將會根據標誌位判斷是否需要onDraw();
(2)onLayout():擺放viewGroup裏面的子控件;
(3)onDraw():繪製視圖本身;(ViewGroup還需要繪製裏面的所有子控件)
(4)drawChild(): 重新回調每一個子視圖的draw方法,child.draw(canvas, this, drawingTime);
6.2 LinearLayout對比RelativeLayout(實質是性能對比)
(1)RelativeLayout會對子View做兩次measure。這是爲什麼呢?首先RelativeLayout中子View的排列方式是基於彼此的依賴關係,而這個依賴關係可能和佈局中View的順序並不相同,在確定每個子View的位置的時候,就需要先給所有的子View排序一下。又因爲RelativeLayout允許A,B 2個子View,橫向上B依賴A,縱向上A依賴B。所以需要橫向縱向分別進行一次排序測量。
(2)LinearLayout,如果不使用weight屬性,LinearLayout會在當前方向上進行一次measure的過程,如果使用weight屬性,LinearLayout會避開設置過weight屬性的view做第一次measure,完了再對設置過weight屬性的view做第二次measure。由此可見,weight屬性對性能是有影響的,而且本身有大坑,請注意避讓。
(3)參考鏈接:Android中RelativeLayout和LinearLayout性能分析