安卓 View 的工作原理

在Android的知識體系中,View扮演著很重要的角色,簡單來理解,View 是 Android 在視覺上的呈現。在界面上 Android 提供了一套 GUI庫,里面有很多控件,但是很多時候我們并不滿足于系統提供的控件,因為這樣就意味這應用界面的同類化比較嚴重。那么怎么才能做出與眾不同的效果呢?答案是自定義 View,也可以叫自定義控件,通過自定義 View 我們可以實現各種五花八門的效果。但是自定義 View 是有一定難度的,尤其是復雜的自定義View,大部分時候我們僅僅了解基本控件的使用方法是無法做出復雜的自定義控件的。為了更好地自定義 View,還需要掌握 View 的底層工作原理,比如View的測量流程、布局流程以及繪制流程,掌握這幾個基本流程后,我們就對 View 的底層更加了解,這樣我們就可以做出一個比較完善的自定義 View。

初識 ViewRoot 和 DecorView

在正式介紹 View 的三大流程之前,我們必須先介紹一些基本概念,這樣才能更好地理解View的measure、layout和draw過程,本節主要介紹 ViewRoot 和 DecorView 的概念。

ViewRoot 對應于 ViewRootImpl 類,它是連接Window-Manager和DecorView的紐帶,View的三大流程均是通過ViewRoot來完成的。在ActivityThread中,當Activity對象被創建完畢后,會將DecorView添加到Window中,同時會創建 ViewRootImpl 對象,并將ViewRootImpl對象和DecorView建立關聯,這個過程可參看如下源碼:

root = new ViewRootImpl(view.getContext(),display);     
root.setView(view,wparams,panelParentView);

View的繪制流程是從ViewRoot的performTraversals方法開始的,它經過measure、layout和draw三個過程才能最終將一個View繪制出來,其中measure用來測量View的寬和高,layout用來確定View在父容器中的放置位置,而draw則負責將View繪制在屏幕上。針對performTraversals的大致流程,可用流程圖1來表示。

圖1 performTraversals的工作流程圖

如圖1 所示,performTraversals會依次調用performMea-sure、performLayout和performDraw三個方法,這三個方法分別完成頂級View的measure、layout和draw這三大流程,其中在performMeasure中會調用measure方法,在measure方法中又會調用onMeasure方法,在onMeasure方法中則會對所有的子元素進行measure過程,這個時候measure流程就從父容器傳遞到子元素中了,這樣就完成了一次measure過程。接著子元素會重復父容器的measure過程,如此反復就完成了整個View樹的遍歷。同理,performLayout和performDraw的傳遞流程和performMeasure是類似的,唯一不同的是,performDraw的傳遞過程是在draw方法中通過dispatchDraw來實現的,不過這并沒有本質區別。

measure過程決定了View的寬/高,Measure完成以后,可以通過getMeasuredWidthgetMeasuredHeight方法來獲取到View測量后的寬/高,在幾乎所有的情況下它都等同于View最終的寬/高,但是特殊情況除外,這點在本章后面會進行說明。Layout過程決定了View的四個頂點的坐標和實際的View的寬/高,完成以后,可以通過getTopgetBottomgetLeftgetRight來拿到View的四個頂點的位置,并可以通過getWidth和getHeight方法來拿到View的最終寬/高。Draw過程則決定了View的顯示,只有draw方法完成以后View的內容才能呈現在屏幕上。

如圖2 所示,DecorView作為頂級View,一般情況下它內部會包含一個豎直方向的LinearLayout,在這個LinearLayout里面有上下兩個部分(具體情況和Android版本及主題有關),上面是標題欄,下面是內容欄。在Activity中我們通過setCon-tentView所設置的布局文件其實就是被加到內容欄之中的,而內容欄的id是content,因此可以理解為Activity指定布局的方法不叫setview而叫setContentView,因為我們的布局的確加到了id為content的FrameLayout中。如何得到content呢?可以這樣:ViewGroup content= findViewById (R.android.id.content)。如何得到我們設置的View呢?可以這樣:content.getChil-dAt(0)。同時,通過源碼我們可以知道,DecorView其實是一個FrameLayout,View層的事件都先經過DecorView,然后才傳遞給我們的View。

圖2 頂級View:DecorView的結構

2 理解MeasureSpec

為了更好地理解View的測量過程,我們還需要理解MeasureSpec。從名字上來看,MeasureSpec看起來像“測量規格”或者“測量說明書”,不管怎么翻譯,它看起來都好像是或多或少地決定了View的測量過程。通過源碼可以發現,MeasureSpec的確參與了View的measure過程。讀者可能有疑問,MeasureSpec是干什么的呢?確切來說,MeasureSpec在很大程度上決定了一個View的尺寸規格,之所以說是很大程度上是因為這個過程還受父容器的影響,因為父容器影響View的MeasureSpec的創建過程。在測量過程中,系統會將View的LayoutParams根據父容器所施加的規則轉換成對應的MeasureSpec,然后再根據這個measureSpec來測量出View的寬/高。上面提到過,這里的寬/高是測量寬/高,不一定等于View的最終寬/高。Mea-sureSpec看起來有點復雜,其實它的實現是很簡單的,下面會詳細地分析MeasureSpec。

2.1 MeasureSpec

MeasureSpec代表一個32位int值,高2位代表Spec-Mode,低30位代表SpecSize,SpecMode是指測量模式,而SpecSize是指在某種測量模式下的規格大小。下面先看一下MeasureSpec內部的一些常量的定義,通過下面的代碼,應該不難理解MeasureSpec的工作原理:

public static class MeasureSpec {
        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 {}

        /**
         * 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;


        /**
         * 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);
        }
      ...
      ...
}

MeasureSpec通過將SpecMode和SpecSize打包成一個int值來避免過多的對象內存分配,為了方便操作,其提供了打包和解包方法。SpecMode和SpecSize也是一個int值,一組Spec-Mode和SpecSize可以打包為一個MeasureSpec,而一個Mea-sureSpec可以通過解包的形式來得出其原始的SpecMode和SpecSize,需要注意的是這里提到的MeasureSpec是指Mea-sureSpec所代表的int值,而并非MeasureSpec本身。

SpecMode有三類,每一類都表示特殊的含義,如下所示。

  • UNSPECIFIED 父容器不對View有任何限制,要多大給多大,這種情況一般用于系統內部,表示一種測量的狀態。
  • EXACTLY 父容器已經檢測出View所需要的精確大小,這個時候View的最終大小就是SpecSize所指定的值。它對應于LayoutParams中的match_parent和具體的數值這兩種模式。
  • AT_MOST 父容器指定了一個可用大小即SpecSize,View的大小不能大于這個值,具體是什么值要看不同View的具體實現。它對應于LayoutParams中的wrap_content。

2.2 MeasureSpec和LayoutParams的對應關系

上面提到,系統內部是通過MeasureSpec來進行View的測量,但是正常情況下我們使用View指定MeasureSpec,盡管如此,但是我們可以給View設置LayoutParams。在View測量的時候,系統會將LayoutParams在父容器的約束下轉換成對應的MeasureSpec,然后再根據這個MeasureSpec來確定View測量后的寬/高。需要注意的是,MeasureSpec不是唯一由LayoutParams決定的,LayoutParams需要和父容器一起才能決定View的MeasureSpec,從而進一步決定View的寬/高。另外,對于頂級View(即DecorView)和普通View來說,MeasureSpec的轉換過程略有不同。對于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams來共同確定;對于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams來共同決定,MeasureSpec一旦確定后,onMeasure中就可以確定View的測量寬/高。

對于DecorView來說,在ViewRootImpl中的measureHierarchy方法中有如下一段代碼,它展示了DecorView的MeasureSpec的創建過程,其中desiredWindowWidth和de-sired-WindowHeight是屏幕的尺寸:

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);//desireWindowWidth是屏幕的寬度
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

上面代碼調用的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.
            mesureSpec = 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;
}

通過上述代碼,DecorView的MeasureSpec的產生過程就很明確了,具體來說其遵守如下規則,根據它的LayoutParams中的寬/高的參數來劃分。

  • LayoutParams.MATCH_PARENT:精確模式,大小就是窗口的大小;
  • LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超過窗口的大小;
  • 固定大小(比如100dp):精確模式,大小為LayoutParams中指定的大小。

對于普通View來說,這里是指我們布局中的View,View的measure過程由ViewGroup傳遞而來,先看一下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);
    }

上述方法會對子元素進行measure,在調用子元素的measure方法之前會先通過getChildMeasureSpec方法來得到子元素的MeasureSpec。從代碼來看,很顯然,子元素的MeasureSpec的創建與父容器的MeasureSpec和子元素本身的LayoutParams有關,此外還和View的margin及padding有關,具體情況可以看一下ViewGroup的getChildMeasureSpec方法,清楚展示了普通View的MeasureSpec的創建規則如下所示。

    /**
     * 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);

        //子元素可用的大小為父容器的尺寸減去padding
        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;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
表1 普通View的MeasureSpec的創建規則

這里再做一下說明。前面已經提到,對于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams來共同決定,那么針對不同的父容器和View本身不同的LayoutParams,View就可以有多種MeasureSpec。這里簡單說一下,當View采用固定寬/高的時候,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精確模式并且其大小遵循Layoutparams中的大小。當View的寬/高是match_parent時,如果父容器的模式是精準模式,那么View也是精準模式并且其大小是父容器的剩余空間;如果父容器是最大模式,那么View也是最大模式并且其大小不會超過父容器的剩余空間。當View的寬/高是wrap_content時,不管父容器的模式是精準還是最大化,View的模式總是最大化并且大小不能超過父容器的剩余空間。可能讀者會發現,在我們的分析中漏掉了UNSPECIFIED模式,那是因為這個模式主要用于系統內部多次Measure的情形,一般來說,我們不需要關注此模式。


參考書目

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容