zoukankan      html  css  js  c++  java
  • View的三次measure,两次layout和一次draw

    我在《Android视图结构》这篇文章中已经描述了Activity,WindowView在视图架构方面的关系。前天,我突然想到为什么在setContentView中能够调用findViewById函数?View那时不是还没有被加载,测量,布局和绘制啊。然后就搜索了相关的条目,发现findViewById只需要在inflate结束之后就可以。于是,我整理了Activity生命周期和View的生命周期的关系,并再次做一下总结。

     为了节约你的时间,本篇文章的主要内容为: 
    - Activity的生命周期和它包含的View的生命周期的关系 
    - Activity初始化时View为什么会三次measure,两次layout但只一次draw? 
    - ViewRoot的初始化过程

    Activity的生命周期和View的生命周期

     我通过一个简单的demo来验证Activity生命周期方法和View的生命周期方法的调用先后顺序。请看如下截图 
    截图

    onCreate函数中,我们通常都调用setContentView来设置布局文件,此时Android系统就会读取布局文件,但是视图此时并没有加载到Window上,并且也没有进入自己的生命周期。 
     只有等到Activity进入resume状态时,它所拥有的View才会加载到Window上,并进行测量,布局和绘制。所以我们会发现相关函数的调用顺序是:

      • onResume(Activity)
      • onPostResume(Activity)
      • onAttachedToWindow(View)
      • onMeasure(View)
      • onMeasure(View)
      • onLayout(View)
      • onSizeChanged(View)
      • onMeasure(View)
      • onLayout(View)
      • onDraw(View)

         大家会发现,为什么onMeasure先调用了两次,然后再调用onLayout函数,最后还有在依次调用onMeasure,onLayoutonDraw函数呢?

    ViewGroup的measure

     大家应该都知道,有些ViewGroup可能会让自己的子视图测量两次。比如说,父视图先让每个子视图自己测量,使用View.MeasureSpec.UNSPECIFIED,然后在根据每个子视图希望得到的大小不超过父视图的一些限制,就让子视图得到自己希望的大小,否则就用其他尺寸来重新测量子视图。这一类的视图有FrameLayout,RelativeLayout等。 
     在《Android视图结构》中,我们已经知道Android视图树的根节点是DecorView,而它是FrameLayout的子类,所以就会让其子视图绘制两次,所以onMeasure函数会先被调用两次。

    // FrameLayout的onMeasure函数,DecorView的onMeasure会调用这个函数。
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
        .....
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                ......
            }
        }
        ........
        count = mMatchParentChildren.size();
        if (count > 1) {
            for (int i = 0; i < count; i++) {
                ........
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }
    

     你以为到了这里就能解释通View初始化时的三次measure,两次layout却只一次draw吗?那你就太天真了!我们都知道,视图结构中不仅仅是DecorViewFrameLayout,还有其他的需要两次measure子视图的ViewGroup,如果每次都导致子视图两次measure,那效率就太低了。所以Viewmeasure函数中有相关的机制来防止这种情况。

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
      ......
      // 当FLAG_FORCE_LAYOUT位为1时,就是当前视图请求一次布局操作
      //或者当前当前widthSpec和heightSpec不等于上次调用时传入的参数的时候
      //才进行从新绘制。
        if (forceLayout || !matchingSize &&
                (widthMeasureSpec != mOldWidthMeasureSpec ||
                        heightMeasureSpec != mOldHeightMeasureSpec)) {
                ......
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                ......
        }
        ......
    }  

    源码看到这里,我几乎失望的眼泪掉下来!没办法,只能再想其他的方法来分析这个问题。

    断点调试大法好

     为了分析函数调用的层级关系,我想到了断点调试法。于是,我果断在onMeasureonLayout函数中设置了断点,然后进行调试。


    函数调用帧

     在《Android视图架构》一文中,我们知道ViewRootDecorView的父视图,虽然它自己并不是一个View,但是它实现了ViewParent的接口,Android正是通过它来实现整个视图系统的初始化工作。而它的performTraversals函数就是视图初始化的关键函数。 
     对比两次onMeasure被调用时的函数调用帧,我们可以轻易发现ViewRootImplperformTraversals函数中直接和间接的调用了两次performMeasure函数,从而导致了View最开始的两次measure过程。 
     然后在相同的performTraversals函数中会调用performLayout函数,从而导致View进行一轮layout过程。 
     但是为什么这次performTraversals并没有触发View的draw过程呢?反而是View又将重新进行一轮measure,layout过程之后才进行draw。

    两次performTraversals

     通过断点调试,我们发现在View初始化的过程中,系统调用了两次performTraversals函数,第一次performTraversals函数导致了View的前两次的onMeasure函数调用和第一次的onLayout函数调用。后一次的performTraversals函数导致了最后的onMeasure,onLayout,和onDraw函数的调用。但是,第二次performTraversals为什么会被触发呢?我们研究一下其源码就可知道。

    private void performTraversals() {
        ......
        boolean newSurface = false;
        //TODO:决定是否让newSurface为true,导致后边是否让performDraw无法被调用,而是重新scheduleTraversals
        if (!hadSurface) {
            if (mSurface.isValid()) {
                // If we are creating a new surface, then we need to
                // completely redraw it.  Also, when we get to the
                // point of drawing it we will hold off and schedule
                // a new traversal instead.  This is so we can tell the
                // window manager about all of the windows being displayed
                // before actually drawing them, so it can display then
                // all at once.
                newSurface = true;
                        .....
            }
        }
                ......
        if (!cancelDraw && !newSurface) {
            if (!skipDraw || mReportNextDraw) {
                ......
                performDraw();
            }
        } else {
            if (viewVisibility == View.VISIBLE) {
                // Try again
                scheduleTraversals();
            } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).endChangingAnimations();
                }
                mPendingTransitions.clear();
            }
        }
        ......
    }
    

      由源代码可以看出,当newSurface为真时,performTraversals函数并不会调用performDraw函数,而是调用scheduleTraversals函数,从而再次调用一次performTraversals函数,从而再次进行一次测量,布局和绘制过程。 
     我们由断点调试可以轻易看到,第一次performTraversals时的newSurface为真,而第二次时是假。 
    断点调试截图

  • 相关阅读:
    streamsets 集成 cratedb 测试
    streamsets k8s 部署试用
    streamsets rest api 转换 graphql
    StreamSets sdc rpc 测试
    StreamSets 相关文章
    StreamSets 多线程 Pipelines
    StreamSets SDC RPC Pipelines说明
    StreamSets 管理 SDC Edge上的pipeline
    StreamSets 部署 Pipelines 到 SDC Edge
    StreamSets 设计Edge pipeline
  • 原文地址:https://www.cnblogs.com/xinmengwuheng/p/6037858.html
Copyright © 2011-2022 走看看