View测量流程之RelativeLayout源码详解

背景:上一篇分析了LinearLayout的垂直方向的测量源码,然后又看了下水平方向的测量源码,发现主干任务步骤其实几乎一模一样,觉得不用再写一篇来记录了,主要的步骤就是四步:

  1. 遍历测量每个子view的宽度,跳过部分带权重的子view。
  2. 把满足条件的带权重的子view,找出来重新测量。
  3. 设置宽高。
  4. 把满足条件的 子view 重新测量。
    代码几乎和垂直测量是一样的。只不过在高度的计算那里, 加了 上坡度 下坡度的 附加逻辑,代码也不多。所以我跳过了水平方向的测量,直接进入相对布局RelativeLayout的测量。然后从源码的角度分析一下,为什么推荐使用线性布局 而不推荐使用相对布局。

RelativeLayout 相对布局的测量源码 并不是很多,主要的测量思路有两步:

  1. 先测水平方向的子view。
  2. 再测垂直方向的子view。
    我们知道RelativeLayout的最基本的用法就是,找一个参照的view,然后相对于这个参照的view,确定一个view的位置。比如在相对布局中有一个子view A,然后有一个子view B,我们通过相对布局的属性可以将B放在A的上边,下边,左边,右边,非常灵活,我们只要以A为参照就行了。怎么以A为参照,相信这是一个连出入门的android都会的内容,像什么leftToRight 呀,below呀,above呀等等。。这些属性我们可能写的很多,但是我们可能并没有从源码的角度,去了解过,也许只是知道这么用就行了。我们这篇文章,就从源码的角度来看看这些属性都是干嘛用的?怎么来完成测量的?

其实RelativeLayout的测量我们只看两步,就是水平和垂直方向的测量这两个关键的地方,因为测量的性能消耗就在这里。代码就不全贴了,只把关键的代码贴出来。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mDirtyHierarchy) {
            mDirtyHierarchy = false;

            /** 先把子view 归类 */
            sortChildren();
        }
...

先进入onMeasure()方法,第一个关键点就是这个sortChildren()方法,这个方法主要是干嘛的呢,从字面意思大概就能猜出来,是把子view进行归类。为什么要归类呢?我们进入这个方法看看。

private void sortChildren() {
        
        /** 1.获取子view的数量 */
        final int count = getChildCount();
        
        /** 2.初始化数组, 这个数组用来存放垂直方向的子view */
        if (mSortedVerticalChildren == null || mSortedVerticalChildren.length != count) {
            mSortedVerticalChildren = new View[count];
        }
    
        /** 3. 初始化数组, 这个数组用来存放水平方向的子view */
        if (mSortedHorizontalChildren == null || mSortedHorizontalChildren.length != count) {
            mSortedHorizontalChildren = new View[count];
        }

        /** 4. 清空依赖图 */
        final DependencyGraph graph = mGraph;
        graph.clear();

        /** 5. 把所有的子view添加到 graph 这个数据结构中 */
        for (int i = 0; i < count; i++) {
            graph.add(getChildAt(i));
        }

        /** 6. 根据规则 把子view归类到对应的垂直数组和水平数组中 */
        graph.getSortedViews(mSortedVerticalChildren, RULES_VERTICAL);
        graph.getSortedViews(mSortedHorizontalChildren, RULES_HORIZONTAL);
    }

代码中体现的思路很清楚,就是把所有的子view归类到分别的水平和垂直的数组中,我们想知道他是怎么进行归类的呢。我们看到归类的步骤是在DependencyGraph这个类中进行的,那就进入这个类看看究竟。

private static class DependencyGraph {
        /**
         * List of all views in the graph.
         *
         * 保存了所有的view
         */
        private ArrayList<Node> mNodes = new ArrayList<Node>();

        /**
         * List of nodes in the graph. Each node is identified by its
         * view id (see View#getId()).
         *
         * 保存了每一个拥有id的view,就是说设置了id的子view才会被保存到这个结构中
         *
         */
        private SparseArray<Node> mKeyNodes = new SparseArray<Node>();

        /**
         * Temporary data structure used to build the list of roots
         * for this graph.
         *
         * 临时的数据结构 用来 保存 图中的 根 root。这个根是什么意思?后面代码中会提到
         */
        private ArrayDeque<Node> mRoots = new ArrayDeque<Node>();

      ........

这个类里面三个重要的变量,注释都写在代码上了,我们记得上一步操作是把所有的子view add 进来了,我们看看add方法。

/**
         * Adds a view to the graph.
         *
         * @param view The view to be added as a node to the graph.
         */
        void add(View view) {
            final int id = view.getId();
            final Node node = Node.acquire(view);

            if (id != View.NO_ID) {
                mKeyNodes.put(id, node);
            }

            mNodes.add(node);
        }

就是通过view生成一个数据结构 Node 节点。然后所有的view 生成的node 都保存进了mNodes这个变量,而只有带有id的view才被保存进了,mKeyNodes 这个称为关键节点,id为key,node为值。
add 进入graph 之后,立马执行了getSortedViews()方法进行归类,好吧我们看看这个方法如果归类的。

/** 6. 根据规则 把子view归类到对应的垂直数组和水平数组中 */
        graph.getSortedViews(mSortedVerticalChildren, RULES_VERTICAL);
        graph.getSortedViews(mSortedHorizontalChildren, RULES_HORIZONTAL);

通过方法这个字面意思,以及这个方法的参数,我们可以大胆的推测,这个方法是通过传入的规则,把子view进行归类,然后把符合条件的子view放在我们传入的这个数组中。
在进入这个方法之前我们先看看这个RULES_VERTICAL 、RULES_HORIZONTAL 分别是什么?

private static final int[] RULES_VERTICAL = {
            ABOVE, BELOW, ALIGN_BASELINE, ALIGN_TOP, ALIGN_BOTTOM
    };

    private static final int[] RULES_HORIZONTAL = {
            LEFT_OF, RIGHT_OF, ALIGN_LEFT, ALIGN_RIGHT, START_OF, END_OF, ALIGN_START, ALIGN_END
    };

哦!看到这个熟悉的字眼是不是恍然大悟,这不就是我们在xml里面经常写的布局属性吗。这样我们就能猜出个大概了,原来 我们的相对定位是通过 水平和垂直方向分别来唯一定为一个位置的。有些是水平属性,有些是垂直属性。我们大胆猜测,是不是写了水平属性的view会被归类为水平view,而写了垂直属性的就被归类为垂直view呢?
然后我们知道一个view要唯一定位,基本上水平属性和垂直属性都要写才能唯一的定位到某个位置。就是说有可能,一个view既存在于水平类的数组中,同时也存在于垂直类的数组中。如果后面的测量是分别测量了水平数组和垂直数组,那么不就是出现一个view被测量两次的情况了么?这不就出现性能消耗了么?哦哦原来有可能是这么回事啊! 跟随思路发散出去,我们已经猜出了个大概。但是具体源码是如何实现的,我们也正好学习学习google工程师编码的思维。别人的思维看的多了,慢慢的也就变成自己的了。

/**
         * Builds a sorted list of views. The sorting order depends on the dependencies
         * between the view. For instance, if view C needs view A to be processed first
         * and view A needs view B to be processed first, the dependency graph
         * is: B -> A -> C. The sorted array will contain views B, A and C in this order.
         *
         * @param sorted The sorted list of views. The length of this array must
         *        be equal to getChildCount().
         * @param rules The list of rules to take into account.
         */
        void getSortedViews(View[] sorted, int... rules) {
            final ArrayDeque<Node> roots = findRoots(rules);
            int index = 0;

            Node node;
            while ((node = roots.pollLast()) != null) {
                final View view = node.view;
                final int key = view.getId();

                sorted[index++] = view;

                /** 有且只有依赖了 上面的view 那么就把找到的view 加入到 roots 队列中 */
                final ArrayMap<Node, DependencyGraph> dependents = node.dependents;
                final int count = dependents.size();
                for (int i = 0; i < count; i++) {
                    final Node dependent = dependents.keyAt(i);
                    final SparseArray<Node> dependencies = dependent.dependencies;

                    dependencies.remove(key);
                    if (dependencies.size() == 0) {
                        roots.add(dependent);
                    }
                }
            }

            if (index < sorted.length) {
                throw new IllegalStateException("Circular dependencies cannot exist"
                        + " in RelativeLayout");
            }
        }

好了,上面yy了那么多,还是落实代码吧。这段代码虽然不长,但是要完全理解和消化,还是需要仔细的斟酌的。先看实参,一个数值,一个rules 规则,这两个参数我们前面已经看过了。然后继续 findRoots() 这个方法看起来像是找到根节点。这个根节点是什么呢?我们进入这个方法看看。

/**
         * Finds the roots of the graph. A root is a node with no dependency and
         * with [0..n] dependents.
         *
         * @param rulesFilter The list of rules to consider when building the
         *        dependencies
         *
         * @return A list of node, each being a root of the graph
         */
        private ArrayDeque<Node> findRoots(int[] rulesFilter) {
            final SparseArray<Node> keyNodes = mKeyNodes;
            final ArrayList<Node> nodes = mNodes;
            final int count = nodes.size();

            // Find roots can be invoked several times, so make sure to clear
            // all dependents and dependencies before running the algorithm
            for (int i = 0; i < count; i++) {
                final Node node = nodes.get(i);
                node.dependents.clear();
                node.dependencies.clear();
            }

            // Builds up the dependents and dependencies for each node of the graph
            for (int i = 0; i < count; i++) {
                final Node node = nodes.get(i);

                final LayoutParams layoutParams = (LayoutParams) node.view.getLayoutParams();
                final int[] rules = layoutParams.mRules;
                final int rulesCount = rulesFilter.length;

                // Look only the the rules passed in parameter, this way we build only the
                // dependencies for a specific set of rules
                for (int j = 0; j < rulesCount; j++) {
                    final int rule = rules[rulesFilter[j]];
                    if (rule > 0) {
                        // The node this node depends on
                        final Node dependency = keyNodes.get(rule);
                        // Skip unknowns and self dependencies
                        if (dependency == null || dependency == node) {
                            continue;
                        }
                        // Add the current node as a dependent
                        dependency.dependents.put(node, this);
                        // Add a dependency to the current node
                        node.dependencies.put(rule, dependency);
                    }
                }
            }

            final ArrayDeque<Node> roots = mRoots;
            roots.clear();

            // Finds all the roots in the graph: all nodes with no dependencies
            for (int i = 0; i < count; i++) {
                final Node node = nodes.get(i);
                if (node.dependencies.size() == 0) roots.addLast(node);
            }

            return roots;
        }

这个方法里面全都是关于Node的操作,所以我们要先看看这个Node到底是啥,究竟有啥用,知道了这个,我们才能更好的了解这个方法到底做了什么?我们看看Node这个数据结构:

/**
         * A node in the dependency graph. A node is a view, its list of dependencies
         * and its list of dependents.
         * 
         * node 是 view ,包含他依赖的列表,和依赖他的列表
         *
         * A node with no dependent is considered a root of the graph.
         * 
         * 没有依赖的node 就是 root node
         */
        static class Node {
            /**
             * The view representing this node in the layout.
             */
            View view;

            /**
             * The list of dependents for this node; a dependent is a node
             * that needs this node to be processed first.
             *
             * 被别的View作为参考,这里关联的是 依赖当前view的其他view集合
             */
            final ArrayMap<Node, DependencyGraph> dependents =
                    new ArrayMap<Node, DependencyGraph>();

            /**
             * The list of dependencies for this node.
             * 这里保存的是 当前view的参考view,即:当前View的位置 依赖于这些View
             */
            final SparseArray<Node> dependencies = new SparseArray<Node>();

            /*
             * START POOL IMPLEMENTATION
             */
            // The pool is static, so all nodes instances are shared across
            // activities, that's why we give it a rather high limit
            private static final int POOL_LIMIT = 100;
            private static final SynchronizedPool<Node> sPool =
                    new SynchronizedPool<Node>(POOL_LIMIT);

            static Node acquire(View view) {
                Node node = sPool.acquire();
                if (node == null) {
                    node = new Node();
                }
                node.view = view;
                return node;
            }

            void release() {
                view = null;
                dependents.clear();
                dependencies.clear();

                sPool.release(this);
            }
            /*
             * END POOL IMPLEMENTATION
             */
        }
        }

对这个类的注释,可能看着挺迷糊的,我们来看一下这个类里面的三个变量,view 不用多说,就是保存的子view; 我们前面也说了,在相对布局中要唯一定为位置,需要找到一个参照的View,然后相对这个位置布局,所以在相对布局中,一个view有可能被别的子view 参照,然后也有可能参照别的子view,这个应该很好理解吧。我自己的定位可能需要依赖别的view才能完成,这就是我要参照别人。然后别人的位置需要依赖我的位置,才能确定,那这就是我被别人依赖。理解了这两种关系,我们就知道dependents 和 dependencies 这两个变量是什么意思了。dependents 保存的是我被哪些view依赖着。dependencies 保存的是,我依赖着哪些view。统统都保存起来。
了解了这三个属性,我们大概也就知道Node这个数据结构是干嘛的了。然后我们继续上面未完的方法。

首先,拿到mKeyNodes 和 mNodes,一个包含的是带有id的view ,一个包含的是全部view。先清空每个node中的依赖,保证数据干净。 然后 遍历每个node节点,获取每个view的LayoutParams

 final LayoutParams layoutParams = (LayoutParams) node.view.getLayoutParams();
                final int[] rules = layoutParams.mRules;
                final int rulesCount = rulesFilter.length;

获取到每个LayoutParams中的规则。我们看看这个LayoutParams中的Rules是怎么来的呢?进入一看:

private int[] mRules = new int[VERB_COUNT];

首先 是一个数组。然后看看在哪里赋值的:

for (int i = 0; i < N; i++) {
                int attr = a.getIndex(i);
                switch (attr) {
                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignWithParentIfMissing:
                        alignWithParent = a.getBoolean(attr, false);
                        break;
                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toLeftOf:
                        rules[LEFT_OF] = a.getResourceId(attr, 0);
                        break;
                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toRightOf:
                        rules[RIGHT_OF] = a.getResourceId(attr, 0);
                        break;
                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_above:
                        rules[ABOVE] = a.getResourceId(attr, 0);
                        break;
                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_below:
                        rules[BELOW] = a.getResourceId(attr, 0);
                        break;
                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignBaseline:
                        rules[ALIGN_BASELINE] = a.getResourceId(attr, 0);
                        break;
                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignLeft:
                        rules[ALIGN_LEFT] = a.getResourceId(attr, 0);
                        break;
                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignTop:
                        rules[ALIGN_TOP] = a.getResourceId(attr, 0);
                        break;
                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignRight:
                        rules[ALIGN_RIGHT] = a.getResourceId(attr, 0);
                        break;
                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignBottom:
                        rules[ALIGN_BOTTOM] = a.getResourceId(attr, 0);
                        break;
                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentLeft:
                        rules[ALIGN_PARENT_LEFT] = a.getBoolean(attr, false) ? TRUE : 0;
                        break;
                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentTop:
                        rules[ALIGN_PARENT_TOP] = a.getBoolean(attr, false) ? TRUE : 0;
                        break;
                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentRight:
                        rules[ALIGN_PARENT_RIGHT] = a.getBoolean(attr, false) ? TRUE : 0;
                        break;
                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentBottom:
                        rules[ALIGN_PARENT_BOTTOM] = a.getBoolean(attr, false) ? TRUE : 0;
                        break;
                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_centerInParent:
                        rules[CENTER_IN_PARENT] = a.getBoolean(attr, false) ? TRUE : 0;
                        break;
                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_centerHorizontal:
                        rules[CENTER_HORIZONTAL] = a.getBoolean(attr, false) ? TRUE : 0;
                        break;
                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_centerVertical:
                        rules[CENTER_VERTICAL] = a.getBoolean(attr, false) ? TRUE : 0;
                       break;
                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toStartOf:
                        rules[START_OF] = a.getResourceId(attr, 0);
                        break;
                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toEndOf:
                        rules[END_OF] = a.getResourceId(attr, 0);
                        break;
                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignStart:
                        rules[ALIGN_START] = a.getResourceId(attr, 0);
                        break;
                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignEnd:
                        rules[ALIGN_END] = a.getResourceId(attr, 0);
                        break;
                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentStart:
                        rules[ALIGN_PARENT_START] = a.getBoolean(attr, false) ? TRUE : 0;
                        break;
                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentEnd:
                        rules[ALIGN_PARENT_END] = a.getBoolean(attr, false) ? TRUE : 0;
                        break;
                }
            }

然后找到了这个东西,这个就很明显了吧,xml中的自定义属性,每个相对布局的属性都放在了对应的数组序列里面。看到这里差不多就知道了其实findRoots()这个方法传进来的rulesFilter 其实就是Rules数组中的索引。

for (int j = 0; j < rulesCount; j++) {
                    final int rule = rules[rulesFilter[j]];
                    if (rule > 0) {
                        // The node this node depends on
                        final Node dependency = keyNodes.get(rule);
                        // Skip unknowns and self dependencies
                        if (dependency == null || dependency == node) {
                            continue;
                        }
                        // Add the current node as a dependent
                        dependency.dependents.put(node, this);
                        // Add a dependency to the current node
                        node.dependencies.put(rule, dependency);
                    }
                }

把索引拿出来,到每个子view的Rules中去遍历,看看子view中有没有写水平或者垂直的属性,在LayoutParams中我们看到了赋值代码块中,rules这个数组里面其实存放的是view 的 id,是通过getResourceId()这个方法获取的view的id,其实就是参照的view的id,比如像这样:android:layout_below="@+id/content" 。这句话的意思就是 rules[BELOW] = content 这个id的值。因为keyNodes中保存的是带有id的view,并且以id为key。

final Node dependency = keyNodes.get(rule);

所以获取到指定的node,然后保存 依赖项 和 被依赖项。

 final ArrayDeque<Node> roots = mRoots;
            roots.clear();

            // Finds all the roots in the graph: all nodes with no dependencies
            for (int i = 0; i < count; i++) {
                final Node node = nodes.get(i);
                if (node.dependencies.size() == 0) roots.addLast(node);
            }

最后定义一个用于返回的roots,前面我们看到mRoots的定义是,没有任何依赖项才是 root。最后遍历所有node,如果node没有依赖项,那么就把这个node加入到roots这个队列中去。好了findRoots()这个方法就看完了。
其实主要的步骤就是,把所有的node遍历一遍,然后看看有没有符合规则的view,如果有就把依赖项和被依赖项保存起来,最后再次遍历,找到没有依赖项的node放入队列中,作为roots返回。

这里我们知道了找寻roots的原理,然后回到getSortedViews()方法,找到了roots接下来的事情就简单了,就是遍历roots,然后把roots里面的每个view,放到对应的数组中:

void getSortedViews(View[] sorted, int... rules) {
            final ArrayDeque<Node> roots = findRoots(rules);
            int index = 0;

            Node node;
            while ((node = roots.pollLast()) != null) {
                final View view = node.view;
                final int key = view.getId();

                sorted[index++] = view;

                /** 有且只有依赖了 上面的view 那么就把找到的view 加入到 roots 队列中 */
                final ArrayMap<Node, DependencyGraph> dependents = node.dependents;
                final int count = dependents.size();
                for (int i = 0; i < count; i++) {
                    final Node dependent = dependents.keyAt(i);
                    final SparseArray<Node> dependencies = dependent.dependencies;

                    dependencies.remove(key);
                    if (dependencies.size() == 0) {
                        roots.add(dependent);
                    }
                }
            }

            if (index < sorted.length) {
                throw new IllegalStateException("Circular dependencies cannot exist"
                        + " in RelativeLayout");
            }
        }

先把roots当前的view加入到数组中,然后找寻唯一依赖 当前子view的 其他view,如果其他的view,有且仅依赖当前子view,那么,就把找到的这个view,加入到roots队列中,通过这样一层一层去找,直到遍历完整个roots,这样就把所有符合条件的view归类到对应的数组中了。

view归类我们终于走完了,接下来就是,水平view数组的测量和垂直view数组的测量了。

 View[] views = mSortedHorizontalChildren;
        int count = views.length;

        /** 测量 施加了 水平规则的 view */
        for (int i = 0; i < count; i++) {
            View child = views[i];
            if (child.getVisibility() != GONE) {
                LayoutParams params = (LayoutParams) child.getLayoutParams();
                int[] rules = params.getRules(layoutDirection);

                applyHorizontalSizeRules(params, myWidth, rules);
                measureChildHorizontal(child, params, myWidth, myHeight);

                if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {
                    offsetHorizontalAxis = true;
                }
            }
        }

mSortedHorizontalChildren 我们知道这个数组就是我们上面已经归类好的满足水平属性的view数组,这里每个view进行了一次测量,通过measureChildHorizontal()这个方法,把这个数组里面的每个view测量了一遍。

views = mSortedVerticalChildren;
        count = views.length;
        final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
    
        /** 测量 施加了 垂直规则的 view */
        for (int i = 0; i < count; i++) {
            final View child = views[i];
            if (child.getVisibility() != GONE) {
                final LayoutParams params = (LayoutParams) child.getLayoutParams();

                applyVerticalSizeRules(params, myHeight, child.getBaseline());
                measureChild(child, params, myWidth, myHeight);
                if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) {
                    offsetVerticalAxis = true;
                }

                if (isWrapContentWidth) {
                    if (isLayoutRtl()) {
                        if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
                            width = Math.max(width, myWidth - params.mLeft);
                        } else {
                            width = Math.max(width, myWidth - params.mLeft + params.leftMargin);
                        }
                    } else {
                        if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
                            width = Math.max(width, params.mRight);
                        } else {
                            width = Math.max(width, params.mRight + params.rightMargin);
                        }
                    }
                }

                if (isWrapContentHeight) {
                    if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
                        height = Math.max(height, params.mBottom);
                    } else {
                        height = Math.max(height, params.mBottom + params.bottomMargin);
                    }
                }

                if (child != ignore || verticalGravity) {
                    left = Math.min(left, params.mLeft - params.leftMargin);
                    top = Math.min(top, params.mTop - params.topMargin);
                }

                if (child != ignore || horizontalGravity) {
                    right = Math.max(right, params.mRight + params.rightMargin);
                    bottom = Math.max(bottom, params.mBottom + params.bottomMargin);
                }
            }
        }

接着,mSortedVerticalChildren 这个垂直数组里面的view也进行一次测量。通过measureChild()这个方法测量。

其实看到这里我们大概就已经知道了,相对布局,的测量规则,而且也能直观的明白为什么很多android 老司机给你建议,尽量少使用相对布局。因为水平view数组和垂直view数组,这两个数组的交集很大,也就是说,根据源码的归类规则,我们知道,水平数组里面的view 和 垂直数组里面的view 有很多都是重复的,那么测量的时候这个重复的view,都被重复测量了两次,造成了资源的浪费和性能的损耗!!!

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