从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。可实际上,此时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阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容