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为真,而第二次时是假。 
    断点调试截图

  • 相关阅读:
    记录一下自己的洛谷的题解
    初学java 学生管理系统——v0002版本
    初学java 学生管理系统——v0001版本
    Redis守护进程作用+数据类型
    java实现发送短信验证码
    Kali入侵入门版笔记!!!
    2020实现ssh公网外联和外网远程穿透以及内网穿透防火墙
    监控键盘和鼠标记录内容和截屏,更新版本2.0,增加了Linux服务端!!!
    Git管理软件开发项目入门版
    2020年Windows下开机自动执行最强
  • 原文地址:https://www.cnblogs.com/xinmengwuheng/p/6037858.html
Copyright © 2011-2022 走看看