從measure角度來優(yōu)化ConstraintLayout

??熟悉ConstraintLayout的同學(xué)都知道ConstraintLayout內(nèi)部的子View最少會(huì)measure兩次,一旦內(nèi)部有某些View的measure階段比較耗時(shí)虎忌,那么measure多次就會(huì)把這個(gè)耗時(shí)問題放大婴洼。在我們的項(xiàng)目中栈虚,我們通過Trace信息發(fā)現(xiàn)App的一部分耗時(shí)是因?yàn)檫@個(gè)造成,所以優(yōu)化ConstraintLayout顯得至關(guān)重要。
??最初,我們想到的辦法是替換布局谣妻,將會(huì)measure多次的布局比如說RelativeLayout和ConstraintLayout換成只會(huì)measure一次的FrameLayout,這在一定程度上能夠緩解這個(gè)問題卒稳,但是這樣做畢竟治標(biāo)不治本蹋半。因?yàn)樵谔鎿Q布局過程中,會(huì)發(fā)現(xiàn)很多布局文件根本就換不了充坑,相關(guān)的同學(xué)在開發(fā)過程中選擇其他布局肯定是要使用到其特別的屬性湃窍。那么有沒有一種辦法闻蛀,既能減少原有布局的measure次數(shù),又能保證不影響到其本身的特性呢您市?基于此,我去閱讀了ConstraintLayout相關(guān)源碼役衡,了解其內(nèi)部實(shí)現(xiàn)原理茵休,思考出一種方案,用以減少ConstraintLayout的measure次數(shù)手蝎,進(jìn)而減少measure的耗時(shí)榕莺。
??為啥選擇ConstraintLayout來優(yōu)化,而不是較為簡(jiǎn)單的RelativeLayout呢棵介?那是因?yàn)镃onstraintLayout的使用太為廣泛钉鸯,而且RelativeLayout能夠?qū)崿F(xiàn)的布局,ConstraintLayout都能實(shí)現(xiàn)邮辽;其次唠雕,還有一點(diǎn)點(diǎn)私心,想要學(xué)習(xí)一下ConstraintLayout的內(nèi)部實(shí)現(xiàn)原理吨述。
??特別注意岩睁,本文ConstraintLayout的源碼來自于2.0.4版本

在后續(xù)內(nèi)容之前,大家一定要記住揣云,本文使用的是2.0.4版本的ConstraintLayout捕儒。因?yàn)椴煌姹镜腃onstraintLayout,內(nèi)部實(shí)現(xiàn)不完全相同邓夕,所以最終實(shí)現(xiàn)的細(xì)節(jié)可能不同刘莹。

1. 實(shí)現(xiàn)方案

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

  1. 自定義ConstraintLayout点弯,重寫onMeasure方法,增加一個(gè)判斷汪榔,減少?zèng)]必要測(cè)量
  2. 設(shè)置ConstrainLayout的optimizationLevel屬性蒲拉,將其修改為OPTIMIZATION_GRAPHOPTIMIZATION_GRAPH_WRAP,默認(rèn)值為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;
    }

??大家從上面的代碼可以看出來幾點(diǎn):

  1. 在onMeasure方法中調(diào)用skipMeasure方法雌团,用以判斷是否跳過當(dāng)前Measure。
  2. skipMeasure方法中士聪,需要注意兩個(gè)點(diǎn):先是判斷了mDirtyHierarchy锦援,如果mDirtyHierarchy為true,那么就不跳過measure剥悟;其次灵寺,遍歷了每個(gè)Child曼库,并且判斷child.isLayoutRequested() && !(child.getMeasuredHeight() > 0 && child.getMeasuredWidth() > 0),如果這個(gè)條件為true略板,那么也不跳過measure毁枯。如果前面兩個(gè)條件都不滿足,那么就繼續(xù)往下判斷是否需要跳過叮称,后面會(huì)詳細(xì)解釋為啥要這么做种玛,這里先不多說。

(2). 設(shè)置optimizationLevel

??設(shè)置optimizationLevel有兩個(gè)方法瓤檐,一是在xml文件中赂韵,通過layout_optimizationLevel屬性設(shè)置,二是通過setOptimizationLevel方法設(shè)置挠蛉。至于為啥需要設(shè)置optimizationLevel祭示,下面的內(nèi)容會(huì)有解釋。

??通過如上兩步操作進(jìn)行設(shè)置谴古,然后將布局里面的ConstraintLayout替換成為自定義的ConstraintLayout质涛,就可以讓其內(nèi)部的View measure一次。
??我相信讥电,大家在使用此方案之前蹂窖,內(nèi)心有一個(gè)疑問:這個(gè)會(huì)影響使用ConstraintLayout的原有特性嗎?經(jīng)過我簡(jiǎn)單的測(cè)試恩敌,此方案定義的ConstraintLayout并不影響其常規(guī)屬性瞬测。大家可以在KotlinDemo里面找到詳細(xì)的實(shí)現(xiàn)代碼,參考MyConstraintLayout的實(shí)現(xiàn)纠炮。

2.揭露原理

??在上面的內(nèi)容當(dāng)中月趟,我們進(jìn)行了兩步操作實(shí)現(xiàn)了measure 一次。那么這兩步為啥要這么做呢恢口?上面沒有解釋孝宗,在這里我將揭露其內(nèi)部原理。
??通過已有的知識(shí)和了解到的ConstraintLayout的實(shí)現(xiàn)耕肩,我們可以知道ConstraintLayout會(huì)measure多次因妇,主要體現(xiàn)在兩個(gè)地方:ViewRootImpl可能會(huì)多次調(diào)用performMeasure方法,最終會(huì)導(dǎo)致ConstraintLayout的onMeasure方法會(huì)調(diào)用多次猿诸;ConstraintLayout內(nèi)部在measure child的時(shí)候婚被,也有可能導(dǎo)致多次measure。所以梳虽,上面的兩步操作分別解決的這兩個(gè)問題:重寫onMeasure方法是避免它被調(diào)用多次址芯;設(shè)置optimizationLevel是避免child 被measure多次。
??我們來看一下這其中的細(xì)節(jié)。

(1). ConstraintLayout的onMeasure方法

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

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 1. 如果當(dāng)前View樹的狀態(tài)是最新的谷炸,也嘗試遍歷每個(gè)child北专,
        // 看看每個(gè)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. 經(jīng)過上面的重新判斷旬陡,再來判斷是否舍棄本次的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());
    }

??這個(gè)onMeasure方法的實(shí)現(xiàn)拓颓,我將其分為三步:

  1. 當(dāng)mDirtyHierarchy為false時(shí),表示當(dāng)前View 樹已經(jīng)經(jīng)歷過測(cè)量了季惩。但是此時(shí)要從每個(gè)child的isLayoutRequested狀態(tài)來判斷是否需要重新測(cè)量录粱,如果為true,表示當(dāng)前child進(jìn)行了requestLayout操作或者forceLayout操作画拾,所以需要重新測(cè)量。這么看好像沒有毛病菜职,但是為啥我們將isLayoutRequested修改為child.isLayoutRequested() && !(child.getMeasuredHeight() > 0 && child.getMeasuredWidth() > 0)呢青抛?這個(gè)要從ConstrainLayout的第一次測(cè)量說起,當(dāng)整個(gè)布局添加到ViewRootImpl上去的時(shí)候酬核,ViewRootImpl會(huì)調(diào)用Constraintlayout的onMeasure方法蜜另。這里有一個(gè)點(diǎn)需要注意的是,在正式layout之前嫡意,onMeasure方法可能會(huì)調(diào)用多次举瑰,同時(shí)isLayoutRequested會(huì)一直為true,因?yàn)檫@個(gè)狀態(tài)在layout階段才清空的蔬螟。也就是說此迅,在layout之前,盡管mDirtyHierarchy已經(jīng)為false了旧巾,還是會(huì)重新測(cè)量一遍所有的child耸序。可實(shí)際上鲁猩,此時(shí)child的width和height已經(jīng)確定了坎怪,沒必要在測(cè)量一遍,所以這里我增加了寬高的限制廓握,保證child已經(jīng)measure了搅窿,不會(huì)再measure。
  2. 經(jīng)過第一點(diǎn)的判斷隙券,如果此時(shí)mDirtyHierarchy還為false男应,表示當(dāng)前View樹不需要再測(cè)量,因此就直接return即可(實(shí)際上是尔,這里沒有直接return殉了,而是另外做了一些判斷,用以保證measure沒有問題拟枚。)薪铜。我們?cè)诙xskipMeasure方法的時(shí)候众弓,就是這部分的代碼拷貝出來的,用以保證內(nèi)外判斷一致隔箍。
  3. 如果上面兩個(gè)條件都不滿足谓娃,那么就表示需要測(cè)量child,就調(diào)用resolveSystem方法測(cè)量所有的child蜒滩。

??上面的第一點(diǎn)中滨达,我已經(jīng)解釋了為啥我們需要重寫onMeasure方法,目的是為了過濾沒必要的測(cè)量俯艰。那么可能有人要問捡遍,正常的測(cè)量會(huì)被過濾嗎?其實(shí)重點(diǎn)在于mDirtyHierarchy為false的情況下竹握,會(huì)影響到某些測(cè)量嗎画株?從一個(gè)方面來看,第一次測(cè)量基本沒有什么問題啦辐,還有一種情況就是谓传,動(dòng)態(tài)的修改View的寬高會(huì)有影響嗎?動(dòng)態(tài)修改布局芹关,最終都會(huì)導(dǎo)致requestLayout续挟,然而我們從ConstraintLayout的實(shí)現(xiàn)可以看出來,Google爸爸在requestLayout和forceLayout兩個(gè)方法里面都將mDirtyHierarchy設(shè)置為true了侥衬,所以理論上不會(huì)造成影響诗祸。

(2). measure child

??從上面的介紹,我們知道ConstraintLayout在measure child浇冰,也有可能measure多次贬媒,我們來看一下為啥會(huì)measure多次。細(xì)節(jié)我們就不分析了肘习,我們直接跳到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) {
           // 判斷優(yōu)化是否失效
        }
        // ······
        optimize &= (widthMode == EXACTLY && heightMode == EXACTLY) || optimizeWrap;

        int computations = 0;

        if (optimize) {
           // 如果優(yōu)化生效际乘,那么通過Graph的方式測(cè)量child,這個(gè)過程中只會(huì)measure child 一次。
        } else {
           // ·······
        }

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

??從這里,我們可以看出來投蝉,只要我們?cè)O(shè)置了optimizationLevel养葵,就有可能讓所有的child只measure一次,這也是我們想要的結(jié)果瘩缆。而且关拒,就算measure有問題,ConstaintLayout在測(cè)量過程中發(fā)現(xiàn)了問題,即allSolved為false着绊,也會(huì)進(jìn)行兜底谐算。

3. 總結(jié)

??經(jīng)過上面的介紹,我們基本能理解整個(gè)優(yōu)化ConstraintLayout measure具體內(nèi)容归露,在這里洲脂,我簡(jiǎn)單的做一個(gè)總結(jié)。

  1. 重寫onMeasure方法是為了保證ConstraintLayout的onMeasure只會(huì)執(zhí)行一次剧包。
  2. 設(shè)置optimizationLevel恐锦,是為了保證child只會(huì)被measure一次。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末疆液,一起剝皮案震驚了整個(gè)濱河市一铅,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌堕油,老刑警劉巖馅闽,帶你破解...
    沈念sama閱讀 206,602評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異馍迄,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)局骤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門攀圈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人峦甩,你說我怎么就攤上這事赘来。” “怎么了凯傲?”我有些...
    開封第一講書人閱讀 152,878評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵犬辰,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我冰单,道長(zhǎng)幌缝,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,306評(píng)論 1 279
  • 正文 為了忘掉前任诫欠,我火速辦了婚禮涵卵,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘荒叼。我一直安慰自己轿偎,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評(píng)論 5 373
  • 文/花漫 我一把揭開白布被廓。 她就那樣靜靜地躺著坏晦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上昆婿,一...
    開封第一講書人閱讀 49,071評(píng)論 1 285
  • 那天球碉,我揣著相機(jī)與錄音,去河邊找鬼挖诸。 笑死汁尺,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的多律。 我是一名探鬼主播痴突,決...
    沈念sama閱讀 38,382評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼狼荞!你這毒婦竟也來了辽装?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,006評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤相味,失蹤者是張志新(化名)和其女友劉穎拾积,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體丰涉,經(jīng)...
    沈念sama閱讀 43,512評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡拓巧,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了一死。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肛度。...
    茶點(diǎn)故事閱讀 38,094評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖投慈,靈堂內(nèi)的尸體忽然破棺而出承耿,到底是詐尸還是另有隱情,我是刑警寧澤伪煤,帶...
    沈念sama閱讀 33,732評(píng)論 4 323
  • 正文 年R本政府宣布加袋,位于F島的核電站,受9級(jí)特大地震影響抱既,放射性物質(zhì)發(fā)生泄漏职烧。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評(píng)論 3 307
  • 文/蒙蒙 一蝙砌、第九天 我趴在偏房一處隱蔽的房頂上張望阳堕。 院中可真熱鬧,春花似錦择克、人聲如沸恬总。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽壹堰。三九已至拭卿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間贱纠,已是汗流浹背峻厚。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留谆焊,地道東北人惠桃。 一個(gè)月前我還...
    沈念sama閱讀 45,536評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像辖试,于是被迫代替她去往敵國(guó)和親辜王。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容