View绘制流程分析

View绘制流程分析

目录介绍

  • 01.addView的流程分析
    • 1.1 wm.addView()流程
  • 02.requestLayout绘制
    • 2.1 源码流程分析
    • 2.2 View绘制流程简析
  • 03.performMeasure测量
    • 3.1 performMeasure源码
    • 3.2 measure设计思路
    • 3.3 measure测量流程
  • 04.performLayout布局
    • 4.1 performLayout源码
    • 4.2 layout设计思路
    • 4.3 layout布局流程
  • 05.performDraw绘制
    • 5.1 performDraw源码
    • 5.2 draw设计思路
    • 5.3 draw绘制流程
  • 06.View绘制流程总结下
    • 6.1 Activity布局绘制
  • 07.View如何显示在屏幕

00.问题答疑思考

  • requestLayout、invalidate与postInvalidate作用与区别,在requestLayout这个方法里面做了什么?

  • requestLayout,onLayout,onDraw,DrawChild区别与联系?drawChild()是做什么用的?

  • View是如何绘制到屏幕上的?View的刷新机制是什么,有哪些重要的方法?

  • Canvas.save()跟Canvas.restore()的调用时机?Canvas的底层机制,绘制框架,硬件加速是什么原理,canvas lock的缓冲区是怎么回事

  • View绘制流程,当一个TextView的实例调用setText()方法后执行了什么?请说一下原理……

  • 测量:如何理解View中的测量?子控件的测量依赖父布局约束吗?如何理解测量过程中的"递"和"归"的设计思想?

  • 测量:如何理解测量中布局MeasureSpec的设计?mode三种布局模式分别如何理解?mode和size的组成如何理解?

  • 测量:单个控件测量流程是怎么样的?测量策略是如何影响测量结果的?如何标记测量完成?谈一下设计思想?

  • 测量:完整的View树测量流程的设计思路是什么样的?遍历测量孩子的大小如何处理margin和padding逻辑?

  • 布局:getWidth()方法和getMeasureWidth()区别呢?布局的设计思路是什么?

  • 布局:单个View的布局流程是怎么样的?如何理解布局中相对位置和绝对位置?什么情况下需要对控件重新布局?

  • 布局:以LinearLayout为例,完整布局流程是怎么样的。如果其中的一个孩子View修改了top高度,其布局流程会发生什么变化?

01.addView的流程分析

1.1 wm.addView()流程

  • 通过WindowManager添加View分析

    wm.addView(),根据WindowManagerImpl --> WindowManager --> ViewManager,最终可知vm是WindowManagerImpl的实例,具体看WindowManagerImpl的addView方法
    WindowManagerImpl#addView(),在这个类中可以看到调用了mGlobal.addView(),而mGlobal是WindowManagerGlobal的对象。
    WindowManagerGlobal#addView(),这里面逻辑很核心,创建ViewRootImpl对象,这个是布局渲染的核心类
    WindowManagerGlobal#addView#root.setView(),实现了root与ViewRootImpl的关联
    ViewRootImpl#setView()#requestLayout(),在ViewRootImpl的setView方法中,调用requestLayout执行重绘的请求

  • 从这里可以知道几个核心的关键点
    • 在global#addView()的源码中,创建ViewRootImpl对象root,然后将root和view绑定起来

02.requestLayout绘制

2.1 源码流程分析

  • requestLayout是执行View绘制入口

    ViewRootImpl#requestLayout#checkThread(),这个方法是检查当前线程的方法,若当前线程非UI线程,则抛出非UI线程更新UI的错误
    ViewRootImpl#scheduleTraversals(),这里主要是入口
    ViewRootImpl#scheduleTraversals()#mChoreographer.postCallback(mTraversalRunnable),调用一个异步消息,用于执行mTraversalRunnable的run方法
    ViewRootImpl#TraversalRunnable#run(),在TraversalRunnable类的run方法中调用了doTraversal方法
    ViewRootImpl#doTraversal(),这里主要是看performTraversals方法
    ViewRootImpl#performTraversals(),整个View的绘制起始方法,从这个方法开始我们的View经过大小测量,位置测量,界面绘制三个逻辑操作

  • ViewRootImpl#performTraversals方法中核心代码如下所示
    private void performTraversals() {
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        performLayout(lp, mWidth, mHeight);
        performDraw();
    }
    
    • image

2.2 View绘制流程简析

  • 接下来看一下View的测量,位置,绘制三个流程
    • image

    ViewRootImpl#performTraversals()#performMeasure,执行View组件的onMeasure方法,主要用于测量View
    ViewRootImpl#performMeasure(),开始执行测量的入口
    View#measure(),这个时候开始调用View类中的测量
    ViewRootImpl#performTraversals()#performLayout,执行View组件的onLayout方法,主要用于布局View
    ViewRootImpl#performLayout(),开始执行布局的入口
    View#layout(),这个时候调用调用View类中的布局
    ViewRootImpl#performTraversals()#performDraw,执行View组件的onDraw方法,主要用于绘制View
    ViewRootImpl#performDraw(),开始执行绘制的入口

  • View的绘制流程主要分为三步:
    • onMeasure:测量视图的大小,从顶层父View到子View递归调用measure()方法,measure()调用onMeasure()方法,onMeasure()方法完成绘制工作。
    • onLayout:确定视图的位置,从顶层父View到子View递归调用layout()方法,父View将上一步measure()方法得到的子View的布局大小和布局参数,将子View放在合适的位置上。
    • onDraw:绘制最终的视图,首先ViewRoot创建一个Canvas对象,然后调用onDraw()方法进行绘制。
  • onMeasure()方法
    • 单一View,一般重写此方法,针对wrap_content情况,规定View默认的大小值,避免于match_parent情况一致。ViewGroup,若不重写,就会执行和单子View中相同逻辑,不会测量子View。一般会重写onMeasure()方法,循环测量子View。
    • Measure完成后可以通过getMeasureWidth和getMeasureHeight方法获取到view的测量后的宽高,在几乎所有的情况下都会等于最终view的宽高
    • onMeasure()方法接收两个参数,widthMeasureSpec和heightMeasureSpec,这两个值分别用于确定视图的宽度和高度的规格和大小。
  • onLayout()方法:
    • 单一View,不需要实现该方法。ViewGroup必须实现,该方法是个抽象方法,实现该方法,来对子View进行布局。
    • layout 过程决定了View的四个顶点的坐标和实际的View的宽高,完成以后可以通过getTop,getBottom,getLeft,getRight来获取View的四个顶点位置,并通过getWidth,getHeight获取View的最终宽高
  • onDraw()方法:
    • 无论单一View,或者ViewGroup都需要实现该方法,因其是个空方法
    • draw过程则决定了View的显示,完成draw后view会显示在屏幕上
    • 绘制背景(background.draw(Canvas))
    • 绘制自己 protected void onDraw(Canvas canvas) onDraw绘制自己,新建一个paint 在canvas上绘制自己的图形
    • 绘制children (dispatchDraw)dispatchDraw会遍历调用所有子元素的draw方法
    • 绘制装饰(onDrawScrollBars)

03.performMeasure测量

3.1 performMeasure源码

  • 从ViewRootImpl类中分析performMeasure测量,这里是测量的入口

    ViewRootImpl#performMeasure(),在performMeasure方法中我们又调用了mView的measure方法。这里的mView就是一开始的Activity的mDector根组件,这里的measure方法就是调用的mDector组件的measure方法
    View#measure(),在View的measure方法中,又调用了onMeasure方法。由于我们的mDector对象是一个FrameLayout,所以这里的onMeasure执行的是FrameLayout的onMeasure方法
    FrameLayout#onMeasure(),这里调用了一个循环逻辑,获取该View的所有子View,并执行所有子View的measure方法,这样又回到View的measure方法。
    - 这样经过一系列的循环遍历过程,如果是ViewGroup就会调用其ViewGroup的onMeasure方法,若果是View组件就会调用View的onMeasure方法
    View#onMeasure(),调用了setMeasuredDimension方法设置测量的结果
    View#setMeasuredDimension(),在这个方法中调用了setMeasuredDimensionRaw方法,把View组件即其子View的大小测量出来了,并且保存在了成员变量mMeasuredWith和mMeasuredHeight中

  • 需要注意测量最后一定要调用setMeasuredDimension()
    • Android提供了setMeasureDimension()函数,将测量结果作为参数并调用该函数,便可以视为View完成了自身的测量。该方法的本质就是将测量结果存起来,以便后续的layout和draw流程中获取控件的宽高。

3.2 measure设计思路

  • View的测量目标便是测量控件的宽高值,View的设计者通过代码编织了一整套复杂的逻辑:
    • 1、对于子View而言,其本身宽高直接受限于父View的布局要求,举例来说,父View被限制宽度为40px,子View的最大宽度同样也需受限于这个数值。因此,在测量子View之时,子View必须已知父View的布局要求,这个 布局要求, Android中通过使用 MeasureSpec 类来进行描述。
    • 2.1、对于完整的测量流程而言,父控件必然依赖子控件宽高的测量;若子控件本身未测量完毕,父控件自身的测量亦无从谈起。Android中View的测量流程中使用了非常经典的 递归思想:对于一个完整的界面而言,每个页面都映射了一个View树,其最顶端的父控件测量开始时,会通过 遍历 将其 布局要求 传递给子控件,以开始子控件的测量,子控件在测量过程中也会通过 遍历 将其 布局要求 传递给它自己的子控件,如此往复一直到最底层的控件…这种通过遍历自顶向下传递数据的方式我们称为 测量过程中的“递”流程。
    • 2.2、当最底层位置的子控件自身测量完毕后,其父控件会将所有子控件的宽高数据进行聚合,然后通过对应的 测量策略 计算出父控件本身的宽高,测量完毕后,父控件的父控件也会根据其所有子控件的测量结果对自身进行测量,这种从底部向上传递各自的测量结果,最终完成最顶层父控件的测量方式我们称为测量过程中的“归”流程,至此界面整个View树测量完毕。
  • 需要注意的是,子控件的测量过程本身还应该依赖于父控件的一些布局约束,比如:
    • 1.父控件固定宽高只有 x p x ,子控件设置为 l a y o u t h e i g h t = " {x}px,子控件设置为layout_height=" xpx,子控件设置为layoutheight="{y}px";
    • 2.父控件高度为wrap_content(包裹内容),子控件设置为layout_height=“match_parent”;
    • 3.父控件高度为match_parent(填充),子控件设置为layout_height=“match_parent”;
    • 这些情况下,因为无法计算出准确控件本身的宽高值,简单的通过setMeasuredDimension()函数似乎不可能达到测量控件的目的,因为 子控件的测量结果是由父控件和其本身共同决定的,而父控件对子控件的布局约束,便是前文提到的 布局要求,即MeasureSpec类。
  • 测量流程中对布局的设计是通过MeasureSpec类来对其进行描述
    • 在设计的过程中,我们将布局要求分成了2个属性。测量大小 意味着控件需要对应大小的宽高,测量模式 则表示控件对应的宽高模式:
    • UNSPECIFIED:父元素不对子元素施加任何束缚,子元素可以得到任意想要的大小;日常开发中自定义View不考虑这种模式,可暂时先忽略;
    • EXACTLY:父元素决定子元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;这里我们理解为控件的宽或者高被设置为 match_parent 或者指定大小,比如20dp;
    • AT_MOST:子元素至多达到指定大小的值;这里我们理解为控件的宽或者高被设置为wrap_content。
  • MeasureSpec用一个32位int值来表示布局要求描述
    • 前2位代表了测量模式,后30位则表示了测量的大小,对于模式和大小值的获取,只需要通过位运算即可。
    • 以宽度举例来说,若我们设置宽度=5px(二进制对应了101),那么mode对应EXACTLY,在创建测量要求的时候,只需要通过二进制的相加,便可得到存储了相关信息的int值:
    • image
    • 而当需要获得Mode的时候只需要用measureSpec与MODE_TASK相与即可,【measureSpec & MODE_MASK】如下图:
    • image
    • 想获得size的话只需要只需要measureSpec与~MODE_TASK相与即可,【measureSpec & ~MODE_MASK】如下图:
    • image

3.3 measure测量流程

3.3.1 测量单个控件
  • 只考虑单个控件的测量,整个过程需要定义三个重要的函数,分别为:
    • final void measure(int widthMeasureSpec, int heightMeasureSpec):执行测量的函数;
    • void onMeasure(int widthMeasureSpec, int heightMeasureSpec):真正执行测量的函数,开发者需要自己实现自定义的测量逻辑;
    • final void setMeasuredDimension(int measuredWidth, int measuredHeight):完成测量的函数;
  • 1.measure()入口函数:标记测量的开始
    • 首先父控件需要通过调用子控件的measure()函数,并同时将宽和高的 布局要求 作为参数传入,标志子控件本身测量的开始:
    // 这个是父控件的代码,让子控件开始测量
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    
    • 对于View的测量流程,其必然包含了2部分:公共逻辑部分 和 开发者自定义测量的逻辑部分,为了保证公共逻辑部分代码的安全性,设计者将measure()方法配置了final修饰符:
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
      // ... 公共逻辑
    
      // 开发者需要自己重写onMeasure函数,以自定义测量逻辑
      onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
    
    • 开发者不能重写measure()函数,并将View自定义测量的策略通过定义一个新的onMeasure()接口暴露出来供开发者重写。
  • 2.onMeasure()函数:自定义View的测量策略
    • onMeasure()函数中,View自身也提供了一个默认的测量策略:
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
    
    • 以宽度为例,通过这样获取View默认的宽度:getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)

    View#getSuggestedMinimumWidth(),在某些情况下(比如自身设置了minWidth或者background属性),View需要通过getSuggestedMinimumWidth()函数作为默认的宽度值
    View#getDefaultSize(minWidth, widthMeasureSpec),根据 布局要求 计算出View最后测量的宽度值。根据不同的测量模式,返回的测量结果不同。

  • 3.setMeasuredDimension()函数:标志测量的完成
    • setMeasuredDimension(width,height)函数的存在意义非常重要,在onMeasure()执行自定义测量策略的过程中,调用该函数标志着View的测量得出了结果。

    View#setMeasuredDimension(),该方法的本质就是将测量结果存起来,以便后续的layout和draw流程中获取控件的宽高

  • 最后总结一下单个View测量流程
    • 经过measure() -> onMeasure() -> setMeasuredDimension()函数的调用,最终View自身测量流程执行完毕。

3.3.2 完整测量流程

  • 对于一个完整的界面而言,每个页面都映射了一个View树,见微知著,了解了单个View的测量过程,从宏观的角度思考,View树整体的测量流程将如何实现?
  • 1、设计思路
    • 首先需要理解的是,每种ViewGroup的子类的测量策略(也就是onMeasure()函数内的逻辑)不尽相同,但整体思路都大同小异,即 遍历 测量所有子控件,根据父控件自身测量策略进行宽高的计算并得出测量结果。
    • 以 竖直方向布局 的LinearLayout为例,如何完成LinearLayout高度的测量?本文抛去不重要的细节,化繁为简,将LinearLayout高度的测量策略简单定义为 遍历获取所有子控件,将高度累加 ,所得值即自身高度的测量结果——如果不知道每个子控件的高度,LinearLayout自然无法测量出本身的高度。
    • 因此对于View树整体的测量而言,控件的测量实际上是 自底向上 的,正如文章开篇 整体思路 一节所描述的:对于完整的测量流程而言,父控件必然依赖子控件宽高的测量;若子控件本身未测量完毕,父控件自身的测量亦无从谈起。
    • 因为子控件的测量逻辑受限于父控件传过来的 布局要求(MeasureSpec), 因此整体逻辑应该是:
    • 1.测量开始时,由顶层的父控件将布局要求传递给子控件,以通知子控件开始执行测量;
    • 2.子控件根据测量策略计算出自身的布局要求,再传递给下一级的子控件,通知子控件开始测量,如此往复,直至到达最后一级的子控件;
    • 3.最后一级的子控件测量完毕后,执行setMeasuredDimension()函数,其父控件根据自己的测量策略,将所有child的宽高和布局属性进行对应的计算(比如上文中LinearLayout就是计算所有子控件高度的和),得到自己本身的测量宽高;
    • 4.该控件通过调用setMeasuredDimension()函数完成测量,这之后,它的父控件再根据其自身测量策略完成测量,如此往复,直至完成顶层级View的测量,自此,整个页面测量完毕。
    • 经典的 递归思想,1、2步骤,开始测量的通知自顶至下,我们称之为测量步骤的 递流程;3、4步骤,测量完毕的顺序却是自底至顶,我们称之为测量步骤的 归流程。
  • 2、递流程的实现
    • 在整个递流程中,MeasureSpec所代表的 布局要求 占有至关重要的作用,了解了它在这个过程中的意义,也就理解了为什么我们常说 子控件的测量结果是由父控件和其本身共同决定的。
    • 依然以 竖直方向布局 的LinearLayout为例,我们需要遍历测量其所有的子控件,因此,在onMeasure()函数中,第一次我们编码如下:
    // 1.0版本的LinearLayout
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
      // 1.通过遍历,对每个child进行测量
      for(int i = 0 ; i < getChildCount() ; i++){  
        View child = getChildAt(i);
        // 2.直接测量子控件
        child.measure(widthMeasureSpec, heightMeasureSpec);
      }
      // ...
      // 3.所有子控件测量完毕...
      // ...
    }
    
    • 思考,若父布局传过来大小的是屏幕的高度,那么将其作为参数直接执行child.measure(widthMeasureSpec, heightMeasureSpec),让子控件直接开始测量,是合理的吗?
    • 答案当然是否定的,试想这样一个简单的场景,若LinearLayout本身设置了padding值,那么子控件的最大高度便不能再达到heightMeasureSpec中size的大小了,但是如果像上述代码中的步骤2一样,直接对子控件进行测量,子控件就可以从heightMeasureSpec参数中取得屏幕的高度,通过setMeasuredDimension()将自己的高度设置和父控件高度一致——这导致了padding值配置的失效,并不符合预期。
    • 需要额外设计一个可重写的函数,用于自定义对child的测量:计算子控件的布局要求,并把新的布局要求传给子控件,再让子控件根据新的布局要求进行测量,这样就解决了上述的问题,由此也说明了为什么 子控件的测量结果是由父控件和其本身共同决定的。
    protected void measureChild(View child, int parentWidthMeasureSpec,
                int parentHeightMeasureSpec) {
            // 获取子元素的布局参数
        final LayoutParams lp = child.getLayoutParams();
        // 通过padding值,计算出子控件的布局要求
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
        // 将新的布局要求传入measure方法,完成子控件的测量
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    
    • getChildMeasureSpec()函数的作用是根据父布局的MeasureSpec和padding值,计算出对应子控件的MeasureSpec,因为这个函数的逻辑是可以复用的。
    • 这个函数才是子控件的测量结果是由父控件和其本身共同决定的 最直接的体现,同时,在不同的布局模式下(match_parent、wrap_content、指定dp/px),其对应子控件的布局要求的返回值亦不同。
    // 2.0版本的LinearLayout
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
      // 1.通过遍历,对每个child进行测量
      for(int i = 0 ; i < getChildCount() ; i++){  
         View child = getChildAt(i);
         // 2.计算新的布局要求,并对子控件进行测量
         measureChild(child, widthMeasureSpec, heightMeasureSpec);
      }
      // ...
      // 3.所有子控件测量完毕...
      // ...
    }
    
  • 3、归流程的实现
    • 所有子控件测量完毕,接下来 归流程 的实现就很简单了,将所有child的height进行累加,并调用 setMeasuredDimension()结束测量即可:
    // 3.0版本的LinearLayout
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
      // 1.通过遍历,对每个child进行测量
      for(int i = 0 ; i < getChildCount() ; i++){  
         View child = getChildAt(i);
         // 2.计算新的布局要求,并对子控件进行测量
         measureChild(child, widthMeasureSpec, heightMeasureSpec);
      }
      // 3.完成子控件的测量,对高度进行累加
      int height = 0;
      for(int i = 0 ; i < getChildCount() ; i++){  
          height += child.getMeasuredHeight();  
      }
      // 4.完成LinearLayout的测量
      setMeasuredDimension(width, height);
    }
    

04.performLayout布局

4.1 performLayout源码

  • 从ViewRootImpl类中分析performLayout测量,这里是测量的入口

    ViewRootImpl#performLayout(),具体看一下host.layout(),这个host就是一开始的Activity的mDector根组件
    View#layout(),看源码执行了onLayout方法,host是一个FrameLayout,所以跟measure类似的,看一下FrameLayout的onLayout方法的实现
    FrameLayout#onLayout(),调用了layoutChildren方法
    FrameLayout#layoutChildren(),在这个方法中,遍历执行View的layout方法,若是ViewGroup则执行具体的ViewGroup的layout方法,若是View,则执行View的layout方法
    View#layout(),经过layout方法,如果是View组件的话就已经将View组件的位置信息计算出来并保存在对象的成员变量中

4.2 layout设计思路

  • 布局设计的总体思路,负责对所有子控件进行对应策略的布局,这就是 布局流程(layout)。
    • 1.对于叶子节点的View而言,其本身没有子控件,因此一般情况下仅需要记录自己在父控件的位置信息,并不需要处理为子控件布局的逻辑;
    • 2.1对于整体的布局流程而言,子控件的位置必然交由父控件布置,和 测量流程 一样,Android中布局流程中也使用了递归思想:对于一个完整的界面而言,每个页面都映射了一个View树,其最顶端的父控件开始布局时,会通过自身的布局策略依次计算出每个子控件的位置。
    • 2.2值得一提的是,为了保证控件树形结构的 内部自治性,每个子控件的位置为 相对于父控件坐标系的相对位置 ,而不是以屏幕坐标系为准的绝对位置。位置计算完毕后,作为参数交给子控件,令子控件开始布局;如此往复一直到最底层的控件,当所有控件都布局完毕,整个布局流程结束。

4.3 layout布局流程

4.3.1 单个View的布局流程
  • 思考一个问题,布局流程的本质是测量结束之后,将每个子控件分配到对应的位置上去——既然有子控件,那说明进行布局流程的主体理应是ViewGroup,那么作为叶子节点的单个View来说,为什么也会有布局流程呢?
  • 读者认真思考可以得出,布局流程实际上是一个复杂的过程,整个流程主要逻辑顺序如下:
    • 1.决定是否需要重新进行测量流程onMeasure();
    • 2.将自身所在的位置信息进行保存;
    • 3.判断本次布局流程是否引发了布局的改变;
    • 4.若布局发生了改变,令所有子控件重新布局;
    • 5.若布局发生了改变,通知所有观察布局改变的监听发送通知。
  • 整个布局过程中,除了4是ViewGroup自身需要做的,其它逻辑对于View和ViewGroup而言都是公共的——这说明单个View也是有布局流程的需求的。
  • 现在将整个布局过程定义三个重要的函数,分别为:
    • void layout(int l, int t, int r, int b):控件自身整个布局流程的函数;
    • void onLayout(boolean changed, int left, int top, int right, int bottom):ViewGroup布局逻辑的函数,开发者需要自己实现自定义布局逻辑;
    • void setFrame(int left, int top, int right, int bottom):保存最新布局位置信息的函数;
  • 1.layout函数:标志布局的开始
    • 站在单个View的角度,首先父控件需要通过调用子控件的layout()函数,并同时将子控件的位置(left、right、top、bottom)作为参数传入,标志子控件本身布局流程的开始
    // 伪代码实现
    public void layout(int l, int t, int r, int b) {
      // 1.决定是否需要重新进行测量流程(onMeasure)
      if(needMeasureBeforeLayout) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec)
      }
    
      // 先将之前的位置信息进行保存
      int oldL = mLeft;
      int oldT = mTop;
      int oldB = mBottom;
      int oldR = mRight;
      // 2.将自身所在的位置信息进行保存;
      // 3.判断本次布局流程是否引发了布局的改变;
      boolean changed = setFrame(l, t, r, b);
    
      if (changed) {
        // 4.若布局发生了改变,令所有子控件重新布局;
        onLayout(changed, l, t, r, b);
        // 5.若布局发生了改变,通知所有观察布局改变的监听发送通知
        mOnLayoutChangeListener.onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
      }
    }
    
    • 通过伪代码的方式对布局流程进行了描述,实际上View本身的layout()函数内部虽然多处不同,但核心思想是一致的——layout()函数实际上代表了控件自身布局的整个流程,setFrame()和onLayout()函数都是layout()中的一个步骤。
  • 2.setFrame函数:保存本次布局信息
    • 为什么需要保存布局信息?因为我们总是有获取控件的宽和高的需求——比如接下来的onDraw()绘制阶段;而保存了布局信息,就能通过这些值计算控件本身的宽高。
    • 保存最新的布局信息,现在将目光转到mLeft、mTop、mRight、mBottom四个变量上。这四个变量对应的自然是View自身所在的位置,那么View是如何通过这四个变量描述控件的位置信息呢?
  • 3.相对位置和绝对位置
    • image
    • image
    • 这时候不可避免的会面临另外一个问题,这个mLeft、mTop、mRight、mBottom的值所对应的坐标系是哪里呢?
    • 这里需要注意的是,为了保证控件树形结构的 内部自治性,每个子控件的位置为 相对于父控件坐标系的相对位置 ,而不是以屏幕坐标系为准的绝对位置
    • 反过来想,如果这些位置信息是以屏幕坐标系为准,那么就意味着每个叶子节点的View会持有保存从根节点ViewGroup直到自身父ViewGroup每个控件的位置信息,在计算布局时则更为繁琐,很明显是不合理的设计。
  • 4.onLayout函数:计算子控件的位置
    • 对于叶子节点的View而言,其并没有子控件,因此一般情况下并没有为子控件布局的意义(特殊情况请参考AppCompatTextView等类),因此View的onLayout()函数被设计为一个空的实现。
    • 而在ViewGroup中,不同类型的ViewGroup有不同的布局策略,这些布局策略的逻辑各不相同,因此该方法被设计为抽象接口,开发者必须实现这个方法以定义ViewGroup的布局策略。
    • 以LinearLayout为例,其布局策略为 根据排布方向,将其所有子控件按照指定方向依次排列布局。
4.3.2 完整布局流程
  • 整体思路是,对于一个完整的界面而言,每个页面都映射了一个View树,最顶端的父控件开始布局时,会通过自身的布局策略依次计算出每个子控件的位置。位置计算完毕后,作为参数交给子控件,令子控件开始布局;如此往复一直到最底层的控件,当所有控件都布局完毕,整个布局流程结束。
  • 唯一需要注意的是,开发者必须实现onLayout()函数以定义ViewGroup的布局策略,这里以 竖直布局 的LinearLayout的伪代码为例:
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
      int childTop;
      int childLeft;
    
      // 遍历所有子View
      for (int i = 0; i < count; i++) {
        // 获取子View
        final View child = getVirtualChildAt(i);
        // 获取子View宽高,注意这里使用的是 getMeasuredWidth 而不是 getWidth
        final int childWidth = child.getMeasuredWidth();
        final int childHeight = child.getMeasuredHeight();
    
        // 令所有子控件开始布局
        setChildFrame(child, childLeft, childTop, childWidth, childHeight);   
        // 高度累加,下一个子View的 top 就等于上一个子View的 bottom ,符合竖直线性布局从上到下的布局策略   
        childTop += childHeight;      
      }
    }
    
    private void setChildFrame(View child, int left, int top, int width, int height) {
        // 这里可以看到,子控件的mRight实际上就是 mLeft + getMeasuredWidth()
        // 而在getWidth()函数中,mRight-mLeft的结果就是getMeasuredWidth()
        // 因此,getWidth() 和 getMeasuredWidth() 是一致的
        child.layout(left, top, left + width, top + height);
    }
    
  • 在整个布局流程的设计中,设计者将流程中公共的业务逻辑(保存布局信息、通知布局发生改变的监听等)通过layout()函数进行了整合,同时,将ViewGroup额外需要的自定义布局策略通过onLayout()函数向外暴露出来,针对组件中代码的可复用性和可扩展性进行了合理的设计。
    • image

05.performDraw绘制

5.1 performDraw源码流程

  • 从ViewRootImpl类中分析performDraw测量,这里是测量的入口

    ViewRootImpl#performDraw(),看源码可知调用了draw(fullRedrawNeeded)
    ViewRootImpl#draw(),调用了mView的draw方法,这里的mView是我们的mDector,看一下draw方法的具体实现
    ViewRootImpl#drawSoftware(),调用mSurface.lockCanvas方法获取一个Canvas对象,然后drawColor,translate,setScreenDensity等等。最后调用mView.draw(canvas)。
    ViewRootImpl#drawSoftware()#mView.draw(canvas),由于mView是我们的mDector,因此这里可以看FrameLayout的draw方法
    FrameLayout#draw(),如果包含子View,那么也会执行子View的draw方法。

  • 从上述源码可以看到ViewRootImpl有一个Surface属性,当界面绘制时,就调用mSurface.lockCanvas方法获取一个Canvas对象传递个View递归绘制。ViewRootImpl简易类图如下。
    • image
  • 然后看一下Surface的源码

    Surface#lockCanvas,这里调用了nativeLockCanvas方法
    android_view_Surface#nativeLockCanvas()。

    //frameworks\base\core\jni\android_view_Surface.cpp
    static jlong nativeLockCanvas(JNIEnv* env, jclass clazz, jlong nativeObject, jobject canvasObj, jobject dirtyRectObj) {
        sp<Surface> surface(reinterpret_cast<Surface *>(nativeObject));
        Canvas* nativeCanvas = GraphicsJNI::getNativeCanvas(env, canvasObj);
        // 给Canvas设置Bitmap
        nativeCanvas->setBitmap(bitmap);
        sp<Surface> lockedSurface(surface);
        lockedSurface->incStrong(&sRefBaseOwner);
        return (jlong) lockedSurface.get();
    }
    
  • 我们的界面像素数据保存在Surface中,这个Surface就是在ViewRootImpl中创建的。
    • image

5.2 draw绘制设计

  • 绘制整体的设计思路:
    • 大部分view就是先绘制必要的背景,然后调用onDraw绘制自身内容,绘制完后如果是ViewGroup,还会调用dispatchDraw绘制子view,最后绘制前景一些UI即可;对于一些特殊view,只是多加了绘制渐变框的一步(使用保存图层、绘制图层、恢复画布的做法)。
  • 关于ViewGroup中dispatchDraw设计思路
    • 自上而下、一层层地传递下去,直到完成整个View树的draw过程。
    • image

5.3 draw绘制流程

  • ViewRootImpl.performTraversals()真正的绘制
    • 调用relayoutWindow():
    • 创建用户java层的surface:只有用户提供的画面数据;
    • 创建native层的surface:包含用户提供的画面数据(java层的surface)+系统的画面数据(状态栏,电池、wifi等等);
    • 创建完surface后:依次调用:performMeasure(对应view的onMeasure)、performLayout(onLayout)、performDraw(onDraw);
  • 在performDraw()中:
    • 将view的数据传至native层的surface
    • surface中的canvas记录数据
    • 生成bitmap图像数据(此时数据是在surface中)
    • 将surface放入队列中;生产者消费者模式;
    • 通知surfaceflinfer进程去队列中取surface数据
    • surfaceflinfer拿到不同的surface,进行融合,生成bitmap数据
    • 将bitmap数据放入framebuffer中,进行展示

06.View绘制流程总结下

6.1 Activity布局绘制

  • 想要弄清楚View是怎么绘制的得先弄明白View是怎么创建出来的。我们先来看下View的创建流程。
    • image
  • 总结如下所示:
    • Activity执行onResume之后再ActivityThread中执行Activity的makeVisible方法。
    • View的绘制流程包含了测量大小,测量位置,绘制三个流程;
    • Activity的界面绘制是从mDoctor即根View开始的,也就是从mDoctor的测量大小,测量位置,绘制三个流程;
    • View体系的绘制流程是从ViewRootImpl的performTraversals方法开始的;
    • View的测量大小流程:performMeasure --> measure --> onMeasure等方法;
    • View的测量位置流程:performLayout --> layout --> onLayout等方法;
    • View的绘制流程:onDraw等方法;
    • View组件的绘制流程会在onMeasure,onLayout以及onDraw方法中执行分发逻辑,也就是在onMeasure同时执行子View的测量大小逻辑,在onLayout中同时执行子View的测量位置逻辑,在onDraw中同时执行子View的绘制逻辑;
    • Activity中都对应这个一个Window对象,而每一个Window对象都对应着一个新的WindowManager对象(WindowManagerImpl实例);

07.View如何显示在屏幕

  • 一个 view 究竟是如何显示在屏幕上的?
    • 一般都比较了解 view 渲染的三大流程,但是 view 的渲染远不止于此:此处以一个通用的硬件加速流程来表征
    • image
  • 关于整个View显示在屏幕上的流程如下
    • Vsync 调度:很多同学的一个认知误区在于认为 vsync 是每 16ms 都会有的,但是其实 vsync 是需要调度的,没有调度就不会有回调;
    • 消息调度:主要是 doframe 的消息调度,如果消息被阻塞,会直接造成卡顿;
    • input 处理:触摸事件的处理;
    • 动画处理:animator 动画执行和渲染;
    • view 处理:主要是 view 相关的遍历和三大流程;
    • measure、layout、draw:view 三大流程的执行;
    • DisplayList 更新:view 硬件加速后的 draw op;
    • OpenGL 指令转换:绘制指令转换为 OpenGL 指令;
    • 指令 buffer 交换:OpenGL 的指令交换到 GPU 内部执行;
    • GPU 处理:GPU 对数据的处理过程;
    • layer 合成:surface buffer 合成屏幕显示 buffer 的流程;
    • 光栅化:将矢量图转换为位图;
    • Display:显示控制;
    • buffer 切换:切换屏幕显示的帧 buffer;

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/4753.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

页面布局 so easy——Android开发常见的界面布局方式详解

​ 在Android应用中&#xff0c;界面由布局和控件组成。布局好比是建筑里的框架&#xff0c;控件相当于建筑里的砖瓦。针对界面中控件不同的排列位置&#xff0c;Android定义了相应的布局进行管理。本篇将针对Android界面中常见的布局进行详细地讲解。 View视图 所有的UI元素…

C 语言网络编程 — 内核协议栈收包/发包流程

目录 文章目录目录关键技术DMAsk_buff 结构体Net driver Rx/Tx Ring BufferBuffer Descriptor TableNAPI 收包机制网卡多队列内核协议栈收包/发包流程概览内核协议栈收包流程详解驱动程序层&#xff08;数据链路层&#xff09;VLAN 协议族Linux Bridge 子系统网络协议层&#x…

PCB模块化设计01——USB接口详解知识要点

目录PCB模块化设计01——USB接口详解知识要点一、定义二、USB分类&#xff1a;三、传输协议四、USB接口布局布线要求PCB模块化设计01——USB接口详解知识要点 一、定义 USB是通用串行总线(Universal Serial Bus)&#xff0c;分为HOST/DEVICE两个角色&#xff0c;所有的数据传…

【C++学习】日积月累——继承详解(1)

一、继承的概念及定义 1.1 继承的概念 继承&#xff08;inheritance&#xff09;机制是面向对象程序设计使代码可以复用的最重要的手段&#xff0c;它允许程序员在保持原有类特性的基础上进行扩展&#xff0c;增加功能&#xff0c;这样产生新的类&#xff0c;称该类为派生类。…

JavaSE思维导图——总结篇

&#x1f468;‍&#x1f4bb;作者简介&#xff1a;学习时长两年半的java博主 &#x1f39f;️个人主页&#xff1a;君临๑ ps&#xff1a;点赞是免费的&#xff0c;却可以让写博客的作者开心好几天&#x1f60e; 进入正题。关于Java专栏的规划如下 写作计划&#xff1a;大概一…

【微服务 从0开始 】Spring Cloud 配置文件

&#x1f50e;这里是【秒懂云原生】&#xff0c;关注我学习云原生不迷路 &#x1f44d;如果对你有帮助&#xff0c;给博主一个免费的点赞以示鼓励 欢迎各位&#x1f50e;点赞&#x1f44d;评论收藏⭐️ &#x1f440;专栏介绍 【秒懂云原生】 目前主要更新微服务&#xff0c;…

抖音本地商家怎么做短视频运营?

抖音作为一款以短视频为核心的本地化社交平台&#xff0c;对于实体店的短视频运营来说&#xff0c;需要注重产品定位、目标人群、短视频制作、发布、私信评论维护和同行客户挖掘等方面。   一、做好产品定位   实体店在进行短视频运营时&#xff0c;首先需要做好产品定位。…

2021蓝桥杯真题图像模糊 C语言/C++

题目描述 小蓝有一张黑白图像&#xff0c;nm 个像素组成&#xff0c;其中从上到下共 n 行&#xff0c;每行从左到右 m 列。每个像素由一个 0 到 255 之间的灰度值表示。 现在&#xff0c;小蓝准备对图像进行模糊操作&#xff0c;操作的方法为&#xff1a; 对于每个像素&#…

首屏加载优化

最近沉迷逛某蓝色软件&#xff0c;收益良多&#xff01;万分感谢博主 海阔_天空&#xff0c;写的太棒了&#x1f44d;&#x1f389; 下面是原文链接&#xff0c;我在原文的基础上浅做个笔记&#xff0c;方便个人快速复习 前端性能优化——首页资源压缩63%、白屏时间缩短86% -…

溯源(五)之攻击源的获取

溯源&#xff08;一&#xff09;之溯源的概念与意义 溯源&#xff08;二&#xff09;之 windows-还原攻击路径 溯源&#xff08;三&#xff09;之Linux-入侵排查 溯源&#xff08;四&#xff09;之流量分析-Wireshark使用 溯源整体流程的思维导图 攻击源的获取 1、获取哪些数…

Spring Data JPA

1. Spring Data环境搭建 Spring Data提供了一套统一的基于Spring的数据访问模型&#xff0c;它可以轻松的实现数据库访问&#xff0c;包括各种关系型、非关系型数据库、Map-Reduce框架、云数据服务等。 Spring Data 包含多个子项目&#xff1a; • Commons - 提供共享的基础框架…

ExtScreen,为智能电视和VR设备打造的快应用引擎

和手机相比&#xff0c;智能电视端的生态一直都不怎么行&#xff0c;具体来讲有以下这几个问题&#xff1a; 电视芯片运算能力差&#xff0c;配置普遍不如手机&#xff1b;电视交互基于遥控器&#xff0c;完全不同于触摸屏操作的手机&#xff1b;电视的生态比较封闭&#xff0…

【JavaWeb】Cookie和Session

目录 Cookie Cookie定义 Cookie数据的来源 Cookie数据的存储 Cookie数据的使用 使用Cookie原因 Session Session定义 如何存储数据 Cookie和Session的区别 使用Cookie和Session简单实现登录页面 Cookie Cookie定义 Cookie是浏览器提供持久化存储数据的机制。 Cook…

这么方便吗?用ChatGPT生成Excel(详解步骤)

文章目录前言使用过 ChatGPT 的人都知道&#xff0c;提示占据非常重要的位置。而 Word&#xff0c;Excel、PPT 这办公三大件中&#xff0c;当属 Excel 最难搞&#xff0c;想要熟练掌握它&#xff0c;需要记住很多公式。但是使用提示就简单多了&#xff0c;和 ChatGPT 聊聊天就能…

【vue3】基础概念的介绍

⭐【前言】 首先&#xff0c;恭喜你打开了一个系统化的学习专栏&#xff0c;在这个vue专栏中&#xff0c;大家可以根据博主发布文章的时间顺序进行一个学习。博主vue专栏指南在这&#xff1a;vue专栏的学习指南 &#x1f973;博主&#xff1a;初映CY的前说(前端领域) &#x1f…

【音视频】zlmediakit总结一

推拉流理论 推流&#xff1a;将直播的内容推送至服务器的过程。 拉流&#xff1a;指服务器已有直播内容&#xff0c;用指定地址进行拉取的过程。 拉流&#xff0c;即是指服务器里面有流媒体视频文件&#xff1b; 但zlmediakit里也有个广义的拉流概念如下。对于用户而言&#xf…

面试官灵魂拷问[二]:SQL 语句中 where 条件后写上 1=1 是什么意思?

面试官灵魂拷问系列又来更新啦! “SQL 语句中 where 条件后写上 11 是什么意思&#xff1f;” 这玩意就跟很多新语言支持尾部逗号的原理一样的。 比如 Kotlin 支持数组写成 [1, 2, 3, 4, ] &#xff0c;注意4后边那个逗号&#xff0c;为什么呢&#xff1f;因为当你增加一个项…

医院LIS系统源码,云LIS系统源码,独立实验室LIS源码

实验室云LIS系统源码 LIS系统源码 LIS源码 基于B/S架构的实验室管理系统云LIS&#xff0c;整个系统的运行基于WEB层面&#xff0c;只需要在对应的工作台安装一个浏览器软件有外网即可访问。 私信了解更多源码内容&#xff01; 技术架构&#xff1a;Asp.NET CORE 3.1 MVC SQ…

MySQL表设计思路(一对多、多对多...)

要开始单独负责需求了&#xff0c;捋一捋表设计的思路。 文章目录一、MySQL中的数据类型二、一对一的关系设计二、一对多的关系设计三、多对多的关系设计四、经验总结一、MySQL中的数据类型 字符串类型 varchar&#xff1a;即variable char &#xff0c;可边长度的字符串&#…

Tomcat启动JSP项目,搞起来了

虽然有点复古&#xff0c;但是还是有很多小伙伴在使用的&#xff0c;小编来一篇保姆级教程 1、用idea打开jsp项目 2、添加tomcat配置 3、点击后会出现配置框,这里画框的地方都选上&#xff0c;版本选择1.8&#xff0c;其他的信息内容默认后&#xff0c;点击确认 4、点击…