View的工作流程——measure

寫在前面

今天和老爸去醫院了,老爸最近腰疼,幸好沒什么事,各位也要多多愛惜自己的身體,等身體出了問題就來不及了。好了,閑話到此為止,話說《開發藝術探索》里面不少東西我早就看了,但是因為自己當時水平有限,而且有很多也看不大懂,所以看了忘是挺正常的事。不過感覺隨著工作經驗的增長,以前大學里上過《操作系統》、《計算機網絡》、《數據結構》等一些課都被串到了一起,感覺很好,而且愈發能感覺到自己的不足了。這周的文主要算是一篇讀書筆記吧,記一下讀《開發藝術探索》第四章和View相關的筆記,至于ViewGroup我覺得還需要過段時間,在沉淀一下再來和各位一起探索一下。而且說真的,這篇文我也是硬著頭皮寫下來的,因為看源碼看著看著發現我疑問越來越多,很多都是現階段暫時無法解決的,不過問題總是一個一個去解決的,先過一遍View的measure流程再說其他的。

ViewRoot & DecorView

ViewRoot對應于ViewRootImpl類,在ViewRootImpl類中有如下一段注釋:

/** 
 * The top of a view hierarchy, implementing the needed 
 * protocol between View and the WindowManager.
 */ 

View層最頂部,實現了View和WindowManager間所需的協議。這個類很重要,但是我現在對其理解也不是很深(一是因為讀源碼無力,一是沒有系統的閱讀過源碼),所以就拿書上的話來說:它是連接WindowManager和DecorView的紐帶,View的三大流程均是通過ViewRoot來完成的。在ActivityThread中(同樣很有意思的一點是ActivityThread是一個類,并非線程什么的,當然了如果哪一天我讀通了這一塊回來和各位分享的),當Activity對象被創建完畢后,會將DecorView添加到Window中,同時會創建ViewRootImpl對象,并將ViewRootImpl對象和DecorView建立關聯。

接下來直接放結論,盡快進入正題:
View的繪制流程是從ViewRoot的performTraversals開始的,它經過measure、layout和draw三個過程才能最終將一個View繪制出來。

MeasureSpec

在之前Android自定義View你需要知道的一些東西中我已經簡要的介紹過MeasureSpec了,當然在這不敢再麻煩各位去看了,繼續簡介一下,熟悉的可以跳過這段。

首先咱直接看他的注釋,看看注釋是怎么解釋這玩意的:

/**
     * A MeasureSpec encapsulates the layout requirements passed from parent to child.
     * Each MeasureSpec represents a requirement for either the width or the height.
     * A MeasureSpec is comprised of a size and a mode. There are three possible
     * modes:
     * <dl>
     * <dt>UNSPECIFIED</dt>
     * <dd>
     * The parent has not imposed any constraint on the child. It can be whatever size
     * it wants.
     * </dd>
     *
     * <dt>EXACTLY</dt>
     * <dd>
     * 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.
     * </dd>
     *
     * <dt>AT_MOST</dt>
     * <dd>
     * The child can be as large as it wants up to the specified size.
     * </dd>
     * </dl>
     *
     * MeasureSpecs are implemented as ints to reduce object allocation. This class
     * is provided to pack and unpack the <size, mode> tuple into the int.
     */

最上面那段話的大概意思是一個MeasureSpec封裝了一個從父(控件)傳給子(控件)的布局要求。每個MeasureSpec代表的不是寬就是高的要求。一個MeasureSpec包含了一個尺寸和一個(測量)模式,這些模式如下:

  • UNSPECIFIED
    父(容器)對子(控件)沒有任何限制,子控件可以想要多大就有多大。

  • EXACTLY
    父容器已經決定了子控件的精確尺寸,子控件將會變成被給出的約束大小,不管他自己想要變成啥樣。

  • AT_MOST
    子控件可以和和他想要的規格尺寸一樣大。

最后一段話解釋了一下為什么這么實現,暫時不需要咱關心這些。看完了注釋基本上已經對MeasureSpec有了一個基本的認識了。但是看完這段注釋,應該會產生一個疑惑,在注釋中說MeasureSpec是父傳遞給子的,那么最頂層的DecorView的MeasureSpec是如何來的呢?《開發藝術探索》上說是在ViewRootImpl的measureHierarchy方法中創建的,主要代碼如下:

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth,lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight,lp.width);
performMeasure(childWidthMeasureSpec,childHeightmeasureSpec);

其中desiredWindowWidth和desiredWindowHeight就是屏幕的尺寸,追蹤下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;
    }

可以看到該方法中對于match_parent、wrap_content和其余情況的處理:

  • LayoutParams.MATCH_PARENT:采用精確模式測量,大小是窗口大小

  • LayoutParams.WRAP_CONTENT:大小不定,最大是窗口大小

  • 其他:結合我們平時寫xml和代碼的經驗,此處其他應該就是我們制定了大小(比如100dp),大小為指定大小。

View的MeasureSpec的獲取

上面的MeasureSpec注釋里說了,是從父傳遞到子的,那么View很明顯是一個子布局,讓我們上ViewGroup里找找child是如何獲取MeasureSpec的:

    /**
     * Ask all of the children of this view to measure themselves, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * We skip children that are in the GONE state The heavy lifting is done in
     * getChildMeasureSpec.
     *
     * @param widthMeasureSpec The width requirements for this view
     * @param heightMeasureSpec The height requirements for this view
     */
    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

老規矩,先讓我這個渣渣來翻譯一下注釋……
遍歷View去測量他們自身,既要考慮MeasureSpec的要求又要考慮padding。跳過GONE狀態的(不測量這個狀態的View),更繁重的事在getChildMeasureSpec完成。

很明顯,咱得看一下getChildMeasureSpec方法了:

    /**
     * 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;
            } 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;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

咱繼續翻譯一下……
困難的事咱來做:算出MeasureSpec傳遞給
一個child。這個方法為一個子view的一個尺寸(高或寬)算出正確的MeasureSpec。

他的目的是組合MeasureSpec和LayoutParams的信息讓child獲取最好的可能結果。例如:如果這個view知道他的尺寸(因為測量模式是EXACTLY),這個child已經指出了他的LayoutParams,他想和他的父容器有一樣的尺寸(match_parent),這時父容器應該要求child布局成被給的精確尺寸。

從上面的注釋我們可以了解到,子View的MeasureSpec并不是由其自身的LayoutParams決定的,而是由其父容器和其自身的LayoutParams一同決定的。接下來看一下代碼,看看究竟是怎么操作的:

  • 首先從ViewGroup的寬/高的MeasureSpec中獲取到對應的specMode,然后根據不同的mdoe去執行不同的操作

  • EXACTLY:如果子View指定了精確值的大小,那么測量模式是EXACTLY,尺寸是傳入的childDimension,這兩個值將會被打包成一個MeasureSpec并返回。如果childDimension等于MATCH_PARENT,那么就將上面獲取到的自身尺寸和EXACTLY模式打包成一個MeasureSpec打包并返回。如果childDimension等于WRAP_CONTENT,那么將自身尺寸賦給孩子的尺寸,讓子View的尺寸不要大于父容器就行了,將這個尺寸和AT_MOST模式打包并返回。

之后的AT_MOST和UNSPECIFIED測量模式和EXACTLY的套路差不多,就不一一看過去了,《Android開發藝術探索》上總結了一個表格:

|columns:childLayoutParams/rows:parentSpecMode|EXACTILY|AT_MOST|UNSPECIFIED
| ------------ |:-----------------: | ------: |
|dp/px|EXACTLY/childSize|EXACTLY/childSize|EXACTLY/childSize|
|match_parent|EXACTLY/parentSize|AT_MOST/parentSize|UNSPECIFIED/0|
|wrap_content|AT_MOST/parentSize|AT_MOST/parentSize|UNSPECIFIED/0|

最后要強調一句和《開發藝術探索》上一樣的話,以上的表格并非是經驗總結,而是將代碼具現為一個表格罷了。

View的measure流程

終于填完了幾個比較重要的坑,可以來講講View的measure流程了。View的測量是由measure()方法實現的 ,在measure()方法中會去調用onMeasure()方法,看一下View的onMeasure()方法的實現:

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

關于這個方法有一些要注意的地方,在以前的Android自定義View你需要知道的一些東西都有說過,而且這個方法的注釋里都有寫你需要注意的東西,感興趣的可以去看看,這里就不贅述了。以上方法調用了setMeasuredDimension方法設置View寬高,因此我們看一下他是如何獲取寬高的就可以了,不多說,看源碼:

    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;
    }

看得出來,這里只是進行了一些簡單的判斷和賦值操作,如果測量模式是AT_MOST和EXACTLY:那么就返回View測量后的大小,如果是UNSPECIFIED模式那么就返回傳入的size值。接下來看看傳入的這個size值是怎么獲取的:

    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

從以上代碼我們可以看出,如果view有背景則從最小寬度和background的寬度中返回較大的那個,如果沒有背景則返回最小寬度,這個最小寬度我們可以通過xml或者setMinimumWidth來設置,默認為0。

接下來通過一個問題來回憶一下今天所了解的東西:在View中使用wrap_content為什么效果和match_parent效果一樣?
首先這個問題需要去ViewGroup里看看,因為View的MeasureSpec是從ViewGroup中傳遞而來的,前面我們畫的那張表格就是處理代碼的具現,我們可以直接查表~

查表可知在match_parent和wrap_content這兩種情況下傳遞給View的size都是parentSize(忽略UNSPECIFIED的情況),那么再看看View有沒有對這兩種情況做特殊的處理就行了。而在上面的getDefaultSize方法里可以看到AT_MOST和EXACTLY兩種模式返回值都是相同的,因此在自定義繼承于View的控件時需要我們去重寫onMeasure()方法來處理wrap_content的情況。

讀到這你可能會發現View和ViewGroup這倆貨根本分不開,事實也是如此,但是在這我就不繼續記我的筆記了,因為有些事我也沒想明白,所以留待以后填坑吧(立了個flag)。

后記

還有一個ViewGroup的measure流程這里并沒有繼續了,想留著以后再來填這個坑吧。記得原來讀《Android開發藝術探索》都是:“哦,原來是這樣的” 狀態,現在讀則是會對一些代碼的流程產生疑惑,而這些疑惑以我現在水平還是有些難以解決的。不過學習就是不斷完善和不斷有新的問題的過程,如同我們初中高中所學的物理一樣,很多時候我們學到的只是相對正確的知識,但是在以后不斷學習的過程中,我們可以不斷的糾正自己以前的錯誤和帶有局限的理解。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容