從measure角度來優化ConstraintLayout

??熟悉ConstraintLayout的同學都知道ConstraintLayout內部的子View最少會measure兩次,一旦內部有某些View的measure階段比較耗時,那么measure多次就會把這個耗時問題放大。在我們的項目中,我們通過Trace信息發現App的一部分耗時是因為這個造成,所以優化ConstraintLayout顯得至關重要。
??最初,我們想到的辦法是替換布局,將會measure多次的布局比如說RelativeLayout和ConstraintLayout換成只會measure一次的FrameLayout,這在一定程度上能夠緩解這個問題,但是這樣做畢竟治標不治本。因為在替換布局過程中,會發現很多布局文件根本就換不了,相關的同學在開發過程中選擇其他布局肯定是要使用到其特別的屬性。那么有沒有一種辦法,既能減少原有布局的measure次數,又能保證不影響到其本身的特性呢?基于此,我去閱讀了ConstraintLayout相關源碼,了解其內部實現原理,思考出一種方案,用以減少ConstraintLayout的measure次數,進而減少measure的耗時。
??為啥選擇ConstraintLayout來優化,而不是較為簡單的RelativeLayout呢?那是因為ConstraintLayout的使用太為廣泛,而且RelativeLayout能夠實現的布局,ConstraintLayout都能實現;其次,還有一點點私心,想要學習一下ConstraintLayout的內部實現原理。
??特別注意,本文ConstraintLayout的源碼來自于2.0.4版本

在后續內容之前,大家一定要記住,本文使用的是2.0.4版本的ConstraintLayout。因為不同版本的ConstraintLayout,內部實現不完全相同,所以最終實現的細節可能不同。

1. 實現方案

??我們直接開門見山,來介紹一下整個方案,主要分為兩步:

  1. 自定義ConstraintLayout,重寫onMeasure方法,增加一個判斷,減少沒必要測量
  2. 設置ConstrainLayout的optimizationLevel屬性,將其修改為OPTIMIZATION_GRAPHOPTIMIZATION_GRAPH_WRAP,默認值為OPTIMIZATION_DIRECT

(1). 重寫onMeasure

??我直接貼代碼:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mMeasureOpt && skipMeasure(widthMeasureSpec, heightMeasureSpec)) {
            return;
        }
        mOnMeasureWidthMeasureSpec = widthMeasureSpec;
        mOnMeasureHeightMeasureSpec = heightMeasureSpec;
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    /**
     * 用以判斷是否跳過本次Measure。
     */
    private boolean skipMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mDirtyHierarchy) {
            return false;
        }
        final int childCount = getChildCount();
        for (int index = 0; index < childCount; index++) {
            View child = getChildAt(index);
            if (child.isLayoutRequested() && !(child.getMeasuredHeight() > 0 && child.getMeasuredWidth() > 0)) {
                return false;
            }
        }
        if (mOnMeasureWidthMeasureSpec == widthMeasureSpec && mOnMeasureHeightMeasureSpec == heightMeasureSpec) {
            resolveMeasuredDimension(widthMeasureSpec, heightMeasureSpec, mLayoutWidget.getWidth(), mLayoutWidget.getHeight(), mLayoutWidget.isWidthMeasuredTooSmall(), mLayoutWidget.isHeightMeasuredTooSmall());
            return true;
        }
        if (mOnMeasureWidthMeasureSpec == widthMeasureSpec && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST && MeasureSpec.getMode(mOnMeasureHeightMeasureSpec) == MeasureSpec.AT_MOST) {
            int newSize = MeasureSpec.getSize(heightMeasureSpec);
            if (newSize >= mLayoutWidget.getHeight()) {
                mOnMeasureWidthMeasureSpec = widthMeasureSpec;
                mOnMeasureHeightMeasureSpec = heightMeasureSpec;
                resolveMeasuredDimension(
                        widthMeasureSpec,
                        heightMeasureSpec,
                        mLayoutWidget.getWidth(),
                        mLayoutWidget.getHeight(),
                        mLayoutWidget.isWidthMeasuredTooSmall(),
                        mLayoutWidget.isHeightMeasuredTooSmall()
                );
                return true;
            }
        }
        return false;
    }

??大家從上面的代碼可以看出來幾點:

  1. 在onMeasure方法中調用skipMeasure方法,用以判斷是否跳過當前Measure。
  2. skipMeasure方法中,需要注意兩個點:先是判斷了mDirtyHierarchy,如果mDirtyHierarchy為true,那么就不跳過measure;其次,遍歷了每個Child,并且判斷child.isLayoutRequested() && !(child.getMeasuredHeight() > 0 && child.getMeasuredWidth() > 0),如果這個條件為true,那么也不跳過measure。如果前面兩個條件都不滿足,那么就繼續往下判斷是否需要跳過,后面會詳細解釋為啥要這么做,這里先不多說。

(2). 設置optimizationLevel

??設置optimizationLevel有兩個方法,一是在xml文件中,通過layout_optimizationLevel屬性設置,二是通過setOptimizationLevel方法設置。至于為啥需要設置optimizationLevel,下面的內容會有解釋。

??通過如上兩步操作進行設置,然后將布局里面的ConstraintLayout替換成為自定義的ConstraintLayout,就可以讓其內部的View measure一次。
??我相信,大家在使用此方案之前,內心有一個疑問:這個會影響使用ConstraintLayout的原有特性嗎?經過我簡單的測試,此方案定義的ConstraintLayout并不影響其常規屬性。大家可以在KotlinDemo里面找到詳細的實現代碼,參考MyConstraintLayout的實現。

2.揭露原理

??在上面的內容當中,我們進行了兩步操作實現了measure 一次。那么這兩步為啥要這么做呢?上面沒有解釋,在這里我將揭露其內部原理。
??通過已有的知識和了解到的ConstraintLayout的實現,我們可以知道ConstraintLayout會measure多次,主要體現在兩個地方:ViewRootImpl可能會多次調用performMeasure方法,最終會導致ConstraintLayout的onMeasure方法會調用多次;ConstraintLayout內部在measure child的時候,也有可能導致多次measure。所以,上面的兩步操作分別解決的這兩個問題:重寫onMeasure方法是避免它被調用多次;設置optimizationLevel是避免child 被measure多次。
??我們來看一下這其中的細節。

(1). ConstraintLayout的onMeasure方法

??我們直接來看onMeasure方法的源碼:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 1. 如果當前View樹的狀態是最新的,也嘗試遍歷每個child,
        // 看看每個child是否重新layout。
        if (!mDirtyHierarchy) {
            // it's possible that, if we are already marked for a relayout, a view would not call to request a layout;
            // in that case we'd miss updating the hierarchy correctly.
            // We have to iterate on our children to verify that none set a request layout flag...
            final int count = getChildCount();
            for (int i = 0; i < count; i++) {
                final View child = getChildAt(i);
                if (child.isLayoutRequested()) {
                    mDirtyHierarchy = true;
                    break;
                }
            }
        }
        // 3. 經過上面的重新判斷,再來判斷是否舍棄本次的measure(不measure child就理解為舍棄本次measure)
        if (!mDirtyHierarchy) {
            if (mOnMeasureWidthMeasureSpec == widthMeasureSpec && mOnMeasureHeightMeasureSpec == heightMeasureSpec) {
                resolveMeasuredDimension(widthMeasureSpec, heightMeasureSpec, mLayoutWidget.getWidth(), mLayoutWidget.getHeight(),
                        mLayoutWidget.isWidthMeasuredTooSmall(), mLayoutWidget.isHeightMeasuredTooSmall());
                return;
            }
            if (mOnMeasureWidthMeasureSpec == widthMeasureSpec
                    && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
                    && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST
                    && MeasureSpec.getMode(mOnMeasureHeightMeasureSpec) == MeasureSpec.AT_MOST) {
                int newSize = MeasureSpec.getSize(heightMeasureSpec);
                if (DEBUG) {
                    System.out.println("### COMPATIBLE REQ " + newSize + " >= ? " + mLayoutWidget.getHeight());
                }
                if (newSize >= mLayoutWidget.getHeight()) {
                    mOnMeasureWidthMeasureSpec = widthMeasureSpec;
                    mOnMeasureHeightMeasureSpec = heightMeasureSpec;
                    resolveMeasuredDimension(widthMeasureSpec, heightMeasureSpec, mLayoutWidget.getWidth(), mLayoutWidget.getHeight(),
                            mLayoutWidget.isWidthMeasuredTooSmall(), mLayoutWidget.isHeightMeasuredTooSmall());
                    return;
                }
            }
        }
        mOnMeasureWidthMeasureSpec = widthMeasureSpec;
        mOnMeasureHeightMeasureSpec = heightMeasureSpec;

        mLayoutWidget.setRtl(isRtl());

        if (mDirtyHierarchy) {
            mDirtyHierarchy = false;
            if (updateHierarchy()) {
                mLayoutWidget.updateHierarchy();
            }
        }
        // 3. measure child
        resolveSystem(mLayoutWidget, mOptimizationLevel, widthMeasureSpec, heightMeasureSpec);
        resolveMeasuredDimension(widthMeasureSpec, heightMeasureSpec, mLayoutWidget.getWidth(), mLayoutWidget.getHeight(),
                mLayoutWidget.isWidthMeasuredTooSmall(), mLayoutWidget.isHeightMeasuredTooSmall());
    }

??這個onMeasure方法的實現,我將其分為三步:

  1. mDirtyHierarchy為false時,表示當前View 樹已經經歷過測量了。但是此時要從每個child的isLayoutRequested狀態來判斷是否需要重新測量,如果為true,表示當前child進行了requestLayout操作或者forceLayout操作,所以需要重新測量。這么看好像沒有毛病,但是為啥我們將isLayoutRequested修改為child.isLayoutRequested() && !(child.getMeasuredHeight() > 0 && child.getMeasuredWidth() > 0)呢?這個要從ConstrainLayout的第一次測量說起,當整個布局添加到ViewRootImpl上去的時候,ViewRootImpl會調用Constraintlayout的onMeasure方法。這里有一個點需要注意的是,在正式layout之前,onMeasure方法可能會調用多次,同時isLayoutRequested會一直為true,因為這個狀態在layout階段才清空的。也就是說,在layout之前,盡管mDirtyHierarchy已經為false了,還是會重新測量一遍所有的child??蓪嶋H上,此時child的width和height已經確定了,沒必要在測量一遍,所以這里我增加了寬高的限制,保證child已經measure了,不會再measure。
  2. 經過第一點的判斷,如果此時mDirtyHierarchy還為false,表示當前View樹不需要再測量,因此就直接return即可(實際上,這里沒有直接return,而是另外做了一些判斷,用以保證measure沒有問題。)。我們在定義skipMeasure方法的時候,就是這部分的代碼拷貝出來的,用以保證內外判斷一致。
  3. 如果上面兩個條件都不滿足,那么就表示需要測量child,就調用resolveSystem方法測量所有的child。

??上面的第一點中,我已經解釋了為啥我們需要重寫onMeasure方法,目的是為了過濾沒必要的測量。那么可能有人要問,正常的測量會被過濾嗎?其實重點在于mDirtyHierarchy為false的情況下,會影響到某些測量嗎?從一個方面來看,第一次測量基本沒有什么問題,還有一種情況就是,動態的修改View的寬高會有影響嗎?動態修改布局,最終都會導致requestLayout,然而我們從ConstraintLayout的實現可以看出來,Google爸爸在requestLayout和forceLayout兩個方法里面都將mDirtyHierarchy設置為true了,所以理論上不會造成影響。

(2). measure child

??從上面的介紹,我們知道ConstraintLayout在measure child,也有可能measure多次,我們來看一下為啥會measure多次。細節我們就不分析了,我們直接跳到measure child的地方--BasicMeasure的solverMeasure方法里面:

    public long solverMeasure(ConstraintWidgetContainer layout,
                              int optimizationLevel,
                              int paddingX, int paddingY,
                              int widthMode, int widthSize,
                              int heightMode, int heightSize,
                              int lastMeasureWidth,
                              int lastMeasureHeight) {
        // ······

        boolean optimizeWrap = Optimizer.enabled(optimizationLevel, Optimizer.OPTIMIZATION_GRAPH_WRAP);
        boolean optimize = optimizeWrap || Optimizer.enabled(optimizationLevel, Optimizer.OPTIMIZATION_GRAPH);

        if (optimize) {
           // 判斷優化是否失效
        }
        // ······
        optimize &= (widthMode == EXACTLY && heightMode == EXACTLY) || optimizeWrap;

        int computations = 0;

        if (optimize) {
           // 如果優化生效,那么通過Graph的方式測量child,這個過程中只會measure child 一次。
        } else {
           // ·······
        }

        if (!allSolved || computations != 2) {
           // 如果沒有優化,或者優化的measure沒有完全解決measure,會兜底測量
           // 這個過程可能會有多次measure child
        }
        if (LinearSystem.MEASURE) {
            layoutTime = (System.nanoTime() - layoutTime);
        }
        return layoutTime;
    }

??從這里,我們可以看出來,只要我們設置了optimizationLevel,就有可能讓所有的child只measure一次,這也是我們想要的結果。而且,就算measure有問題,ConstaintLayout在測量過程中發現了問題,即allSolved為false,也會進行兜底。

3. 總結

??經過上面的介紹,我們基本能理解整個優化ConstraintLayout measure具體內容,在這里,我簡單的做一個總結。

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

推薦閱讀更多精彩內容