從measure角度來優化ConstraintLayout 1. 實現方案 2.揭露原理 3. 總結

  熟悉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。可實際上,此時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一次。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章