淺析View工作原理

View是android os裏一個重要的組成部分,在開發過程中我們經常會用到的一些View組件就有TextView,Button,ListView等等。有些組件經過設計後會展示出更好的視覺效果。那麼我們會感到疑惑,一個組件肯定不會憑空產生,那麼我們在代碼使用了這些組件後它們又經歷了一個怎樣的過程才使得它們能夠在屏幕上展示出來呢?
首先,需要知道的是View的繪製流程是從ViewRootImpl這個類裏的 performTraversals方法開始,過程中經歷了measure, layout, draw三個流程,最終使得組件在屏幕上展現出來。其中,有三個主要的方法依次被執行:onMeasure, onLayout以及onDraw方法

一、onMeasure()

看到這個方法名,大概就會知道這個方法有什麼作用了,它主要負責視圖大小的測量。既然View的繪製流程是從ViewRootImpl這個類裏的 performTraversals方法開始,那我們可以看看這個方法的代碼,不難發現會有這三條語句:

int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
//...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

從上面可以看到performTraversals會調用performMeasure這個方法,而這個方法的參數來自於之前的getRootMeasureSpec方法,這兩個參數這分別用於確定視圖的寬度和高度的規格和大小。至於如何確定,則需要先了解一下MeasureSpec這個變量。
MeasureSpec是一個32位的int值,高2位代表SpecMode(測量模式),低30位代表SpecSize(測量規格)。其中SpecMode有三種類型:

  1. UNSPECIFIED
    父容器會View不會有任何限制,設置多大即爲多大,一般用不上。
  2. EXACTLY
    父容器已經檢測出View的精確大小,並且該大小即爲SpecSize。它對應於LayoutParams裏的macth_parent和精確數值兩種模式。
  3. AT_MOST
    父容器指定一個可用大小即SpecSize,View的大小不能超過這個數值。它對應於LayoutParams裏的wrap_content模式。

那接下里就需要了解一下childWidthMeasureSpec 與 childHeightMeasureSpec 是怎麼得來的:

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
        case ViewGroup.LayoutParams.MATCH_PARENT:
            //精確模式,大小即爲窗口大小
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            //最大模式,大小不確定,且不能超過窗口大小
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            //固定數值
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

對MeasureSpec有一定了解後,再繼續看看接下來的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);
        }
    }

這裏可以看到調用了View裏的measure方法,同樣也是上述兩個參數:

 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
          /**
          *  省略部分代碼
          */
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                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;
            }
         /**
          *  省略部分代碼
          */          
    }

注意到measure方法被final修飾了,因此我們並不能重寫measure這個方法。可以看到這時候又調用了onMeasure()這個方法:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

此處纔是真正開始測量View的地方,它會調用getDefaultSize這個方法來獲取視圖的大小:

public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        //從MeasureSpec中取出specMode與specSize
        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:
            //如果是AT_MOST和EXACTLY,都返回測量值
            result = specSize;
            break;
        }
        return result;
    }

通過getDefaultSize來獲取視圖的大小,然後通過setMeasuredDimension這個方法來設置大小,就完成了一個View的測量。但我們知道一個程序中往往不止一兩個View,因此需要遍歷的測量每個視圖的大小,而這個方法在ViewGroup中就可以找到了:

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        //view數組,子視圖
        final View[] children = mChildren;
        //遍歷子視圖,每個子視圖執行measure方法,完成整個ViewGroup的測量
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }


protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();
        //獲取MeasureSpec值
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
        //測量child View
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

二、onLayout()

測量過程完成,緊接着就到了佈局的過程了。這時候還是要回到ViewRootImpl裏的 performTraversals方法中,很容易會發現在performMeasure方法執行過後又執行了performLayout這個方法,以開始佈局視圖。而與measure過程類似,在執行performLayout時又調用了View的layout方法:

 host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

繼續看看layout方法:

/**
* @params l, t, r, b 對應四個頂點位置
*/
public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        //設置新位置
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //執行onLayout方法
            onLayout(changed, l, t, r, b);
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }

可以看到與measure過程如出一轍,又接着調用了onLayout方法,所以接着看看onLayout是怎麼實現的:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

點進去後卻發現裏面什麼都沒有!爲什麼?原因在於子視圖的位置是基於父視圖的,即由父視圖來執行子視圖的佈局。父視圖那便是ViewGroup,而ViewGroup卻是一個抽象方法,顯然,具體的實現方法會在繼承它的子類中實現,那麼有哪些子類呢?這顯然不陌生,我們常用的就有LinearLayout, RelativeLayout等等。那就挑個LinearLayout來看看它的onLayout方法:

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //通過以下兩個方法可以實現視圖的垂直佈局與橫向佈局
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }

由於每個佈局都有自己獨特的佈局方式,對於具體的佈局方法不做具體研究。
至此layout的過程也執行完畢。

三、onDraw()

通過performMeasure與performLayout這兩個過程,視圖的測量與佈局就已經完成了,剩下最後一步,就是把確定好的視圖按照規定的佈局畫出來,從而展示給用戶。那麼,performTraversals裏也能發現有performDraw這個方法。毫無疑問,performDraw方法裏又和其他兩個過程一樣調用了draw方法:

public void draw(Canvas canvas) {  
    if (ViewDebug.TRACE_HIERARCHY) {  
        ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);  
    }  
    final int privateFlags = mPrivateFlags;  
    final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&  
            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);  
    mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;  
    // Step 1, draw the background, if needed  
    int saveCount;  
    if (!dirtyOpaque) {  
        //設置背景
        final Drawable background = mBGDrawable;  
        if (background != null) {  
            final int scrollX = mScrollX;  
            final int scrollY = mScrollY;  
            if (mBackgroundSizeChanged) {  
                background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);  
                mBackgroundSizeChanged = false;  
            }  
            if ((scrollX | scrollY) == 0) {  
                //繪製背景
                background.draw(canvas);  
            } else {  
                canvas.translate(scrollX, scrollY);  
                background.draw(canvas);  
                canvas.translate(-scrollX, -scrollY);  
            }  
        }  
    }  
    final int viewFlags = mViewFlags;  
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;  
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;  
    if (!verticalEdges && !horizontalEdges) {  
        // Step 3, draw the content  繪製視圖內容
        if (!dirtyOpaque) onDraw(canvas);  
        // Step 4, draw the children  繪製子視圖
        dispatchDraw(canvas);  
        // Step 6, draw decorations (scrollbars)  繪製裝飾 如滾動條
        onDrawScrollBars(canvas);  
        // we're done...  
        return;  
    }  
} 

在上面的代碼中,我們也會注意到有兩個方法:onDraw和dispatchDraw,點進去發現這兩個方法都是空方法,顯然都要在ViewGroup去尋找這些方法。而對於每個不同的佈局,其實現方式的也會有所不同。

經過上述的三個過程,一個視圖也就能成功地顯示出來了!最後,可以畫個圖來總結一下來描述一下View的工作原理:
這裏寫圖片描述

發佈了58 篇原創文章 · 獲贊 45 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章