1. 程式人生 > >Android View 繪製流程 與invalidate 和postInvalidate 分析--從原始碼角度

Android View 繪製流程 與invalidate 和postInvalidate 分析--從原始碼角度

整個View樹的繪製流程是在ViewRootImpl.java類的performTraversals()函式展開的,該函式做的執行過程可簡單概況為

 根之前狀態,判斷是否需要重新計算檢視大小(measure)、是否重新需要佈局檢視的位置(layout)、以及是否需要重繪

 (draw),所以整個View的繪製過程,總結為三步:

1、  Measure:測量View大小

2 、 Layout:對View進行佈局

3、  Draw:繪製View:View背景、View內容、View邊線、繪製子View(如果有);

 一、performTraversals() 函式觸發時機

在這裡提問下,為什麼View的繪製是從performTraversals() 函式開始,這個函式是在什麼時候觸發的呢?

  來通過原始碼來分析下到底是什麼原因,本文的原始碼是基於最新的原始碼6.0.1

  首先我們知道是通過 PhoneWindow的 setContentView 將View 加進來的,分析其原始碼如下:

@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
    // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature // before this happens. if (mContentParent == null) { installDecor(); } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { mContentParent.removeAllViews(); } if (hasFeature(FEATURE_CONTENT_TRANSITIONS
)) { view.setLayoutParams(params); final Scene newScene = new Scene(mContentParent, view); transitionTo(newScene); } else {
        mContentParent.addView(view, params);
}
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
}
}

然後呼叫到ViewGroup 的addView 方法:

/**
 * Adds a child view with the specified layout parameters.
 *
 * <p><strong>Note:</strong> do not invoke this method from
 * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},
 * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>
*
 * @param child the child view to add
 * @param params the layout parameters to set on the child
 */
public void addView(View child, LayoutParams params) {
    addView(child, -1, params);
}


/**
 * Adds a child view with the specified layout parameters.
 *
 * <p><strong>Note:</strong> do not invoke this method from
 * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},
 * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>
*
 * @param child the child view to add
 * @param index the position at which to add the child or -1 to add last
 * @param params the layout parameters to set on the child
 */
public void addView(View child, int index, LayoutParams params) {
    if (DBG) {
        System.out.println(this + " addView");
}

    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}

    // addViewInner() will call child.requestLayout() when setting the new LayoutParams
    // therefore, we call requestLayout() on ourselves before, so that the child's request
    // will be blocked at our level
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}

看見addView 的方法中調運invalidate方法,這不就真相大白了。

   當我們寫一個Activity時,我們一定會通過setContentView方法將我們要展示的介面傳入該方法,該方法會講通過addView追加到id為content的一個FrameLayout(ViewGroup)中,然後addView方法中通過調運invalidate(true)去通知觸發ViewRootImpl類的performTraversals()方法,至此遞迴繪製我們自定義的所有佈局。

最終會呼叫到ViewRootImpl的invalidate方法,從而呼叫 scheduleTraversals

void invalidate() {
    mDirty.set(0, 0, mWidth, mHeight);
    if (!mWillDrawSoon) {
        scheduleTraversals();
}
}

 進入到scheduleTraversals 放法中:

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
}
        notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

  來看看  mTraversalRunnable實現了什麼,其程式碼如下:

final class TraversalRunnable implements Runnable {
    @Override
public void run() {
        doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
}

        performTraversals();
        if (mProfile) {
            Debug.stopMethodTracing();
mProfile = false;
}
    }
}

   從上面程式碼可以看出 , scheduleTraversals 通過Handler的Runnable傳送一個非同步訊息,然後呼叫doTraversal方法,然後最終呼叫performTraversals()執行重繪。文章開頭背景知識介紹說過的,performTraversals就是整個View樹開始繪製的呼叫入口,所以說View調運invalidate方法的實質是層層上傳到父級,直到傳遞到ViewRootImpl後觸發了scheduleTraversals方法,從而執行performTraversals 進行重繪;

至此,對於為什麼View的繪製入口是在performTraversals 本文分析完成;

二、  View繪製三大過程的具體分析

接下來,來具體分析下View繪製的三大過程,即performTraversals 做了具體什麼事情,其原始碼如下:


private void performTravelsals(){
....
 int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
 int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height)
 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

...
 int desiredWindowWidth;
 int desiredWindowHeight;
 performLayout(lp, desiredWindowWidth, desiredWindowHeight);

...

 performDraw();



}

從上述原始碼分析,也驗證來了文章開頭分析的View 繪製過程的三大步的正確性;

2.1  Measure 過程分析

先來看看performMeasure  做了什麼事情,首先看程式碼中,該函式會去呼叫 View .measure 函式;
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);
}
}

measure 具體實現如下:
/**
 * <p>
* This is called to find out how big a view should be. The parent
 * supplies constraint information in the width and height parameters.
 * </p>
*
 * <p>
* The actual measurement work of a view is performed in
 * {@link #onMeasure(int, int)}, called by this method. Therefore, only
 * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
 * </p>
*
 *
 * @param widthMeasureSpec Horizontal space requirements as imposed by the
 *        parent
 * @param heightMeasureSpec Vertical space requirements as imposed by the
 *        parent
 *
 * @see #onMeasure(int, int)
 */
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    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);
}

    // Suppress sign extension for the low bytes
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
    if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
    if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
            widthMeasureSpec != mOldWidthMeasureSpec ||
            heightMeasureSpec != mOldHeightMeasureSpec) {

        // first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
        int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
                mMeasureCache.indexOfKey(key);
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            // measure ourselves, this should set the measured dimension flag back
onMeasure(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 needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}

        // flag not set, setMeasuredDimension() was not invoked, we raise
        // an exception to warn the developer
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
            throw new IllegalStateException("View with id " + getId() + ": "
+ getClass().getName() + "#onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}

        mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}

    mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
            (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}

從註釋資訊可以得出很多重要的資訊,告訴Measure方法為整個View樹計算實際的大小,然後設定實際的高和寬,每個View控制元件的實際寬高都是由父檢視和自身決定的,實際的測量是在onMeasure方法進行,所以在View的子類需要重寫onMeasure方法,這是因為measure方法是final的,不允許過載,所以View子類只能通過過載onMeasure來實現自己的測量邏輯。

   measure的兩個引數都是父View傳遞過來的,也就是代表了父view的規格,它由兩部分組成,高16位表示MODE,定義在MeasureSpec類(View的內部類)中,有三種類型,MeasureSpec.EXACTLY表示確定大小, MeasureSpec.AT_MOST表示最大大小, MeasureSpec.UNSPECIFIED不確定。低16位表示size,也就是父View的大小。對於系統Window類的DecorVIew物件Mode一般都為MeasureSpec.EXACTLY ,而size分別對應螢幕寬高,對於子View來說大小是由父View和子View共同決定的。

從程式碼可以看出measure方法最終回調了View的onMeasure方法,我們來看下View的onMeasure原始碼,如下:

/**
 * <p>
* Measure the view and its content to determine the measured width and the
 * measured height. This method is invoked by {@link #measure(int, int)} and
 * should be overridden by subclasses to provide accurate and efficient
 * measurement of their contents.
 * </p>
*
 * <p>
* <strong>CONTRACT:</strong> When overriding this method, you
 * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
 * measured width and height of this view. Failure to do so will trigger an
 * <code>IllegalStateException</code>, thrown by
 * {@link #measure(int, int)}. Calling the superclass'
 * {@link #onMeasure(int, int)} is a valid use.
 * </p>
*
 * <p>
* The base class implementation of measure defaults to the background size,
 * unless a larger size is allowed by the MeasureSpec. Subclasses should
 * override {@link #onMeasure(int, int)} to provide better measurements of
 * their content.
 * </p>
*
 * <p>
* If this method is overridden, it is the subclass's responsibility to make
 * sure the measured height and width are at least the view's minimum height
 * and width ({@link #getSuggestedMinimumHeight()} and
 * {@link #getSuggestedMinimumWidth()}).
 * </p>
*
 * @param widthMeasureSpec horizontal space requirements as imposed by the parent.
 *                         The requirements are encoded with
 *                         {@link android.view.View.MeasureSpec}.
 * @param heightMeasureSpec vertical space requirements as imposed by the parent.
 *                         The requirements are encoded with
 *                         {@link android.view.View.MeasureSpec}.
 *
 * @see #getMeasuredWidth()
 * @see #getMeasuredHeight()
 * @see #setMeasuredDimension(int, int)
 * @see #getSuggestedMinimumHeight()
 * @see #getSuggestedMinimumWidth()
 * @see android.view.View.MeasureSpec#getMode(int)
 * @see android.view.View.MeasureSpec#getSize(int)
 */
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的測量工作結束。

     View的預設大小,從程式碼可以分析得出:setMeasuredDimension傳入的引數都是通過getDefaultSize返回的,所以再來看下getDefaultSize方法原始碼,如下:

/**
 * Utility to return a default size. Uses the supplied size if the
 * MeasureSpec imposed no constraints. Will get larger if allowed
 * by the MeasureSpec.
 *
 * @param size Default size for this view
 * @param measureSpec Constraints imposed by the parent
 * @return The size this view should be.
 */
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;
}

從上面得出:如果specMode等於AT_MOST或EXACTLY就返回specSize,這就是系統預設的規格;

   繼續看上面onMeasure方法,其中getDefaultSize引數的widthMeasureSpec和heightMeasureSpec都是由父View傳遞進來的;

   getSuggestedMinimumWidth與getSuggestedMinimumHeight都是View的方法,具體如下:

/**
 * Returns the suggested minimum width that the view should use. This
 * returns the maximum of the view's minimum width)
 * and the background's minimum width
 *  ({@link android.graphics.drawable.Drawable#getMinimumWidth()}).
 * <p>
* When being used in {@link #onMeasure(int, int)}, the caller should still
 * ensure the returned width is within the requirements of the parent.
 *
 * @return The suggested minimum width of the view.
 */
protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

/**
 * Returns the suggested minimum height that the view should use. This
 * returns the maximum of the view's minimum height
 * and the background's minimum height
 * ({@link android.graphics.drawable.Drawable#getMinimumHeight()}).
 * <p>
* When being used in {@link #onMeasure(int, int)}, the caller should still
 * ensure the returned height is within the requirements of the parent.
 *
 * @return The suggested minimum height of the view.
 */
protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

 通過上面程式碼得出:建議的最小寬度和高度都是由View的Background尺寸與通過設定View的miniXXX屬性共同決定的。

   至此,最基礎的元素View的measure過程就完成了。

   但是View實際是巢狀的,所以measure是遞迴傳遞的,從而每個View都需要measure。實際能夠巢狀的View一般都是ViewGroup的子類,所以在ViewGroup中定義了measureChildren, measureChild, measureChildWithMargins方法來對子檢視進行測量,measureChildren內部實質只是迴圈呼叫measureChild,measureChild和measureChildWithMargins的區別就是是否把margin和padding也作為子檢視的大小。

  以ViewGroup中稍微複雜的measureChildWithMargins方法來分析:

/**
 * Ask one of the children of this view to measure itself, taking into
 * account both the MeasureSpec requirements for this view and its padding
 * and margins. The child must have MarginLayoutParams The heavy lifting is
 * done in getChildMeasureSpec.
 *
 * @param child The child to measure
 * @param parentWidthMeasureSpec The width requirements for this view
 * @param widthUsed Extra space that has been used up by the parent
 *        horizontally (possibly by other children of the parent)
 * @param parentHeightMeasureSpec The height requirements for this view
 * @param heightUsed Extra space that has been used up by the parent
 *        vertically (possibly by other children of the parent)
 */
protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    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);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

/**
 * Does the hard part of measureChildren: figuring out the MeasureSpec to
 * pass to a particular child. This method figures out the right MeasureSpec
 * for one dimension (height or width) of one child view.
 *
 * The goal is to combine information from our MeasureSpec with the
 * LayoutParams of the child to get the best possible results. For example,
 * if the this view knows its size (because its MeasureSpec has a mode of
 * EXACTLY), and the child has indicated in its LayoutParams that it wants
 * to be the same size as the parent, the parent should ask the child to
 * layout given an exact size.
 *
 * @param spec The requirements for this view
 * @param padding The padding of this view for the current dimension and
 *        margins, if applicable
 * @param childDimension How big the child wants to be in the current
 *        dimension
 * @return a MeasureSpec integer for the child
 */
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
    int size = Math.max(0, specSize - padding);
    int resultSize = 0;
    int resultMode = 0;
    switch (specMode) {
    // Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} 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;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            // Child wants a specific size... so be it
resultSize = 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;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            // Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;