13.View绘制流程设计

13.View绘制流程设计

目录介绍
  • 01.addView的流程分析
    • 1.1 wm.addView()流程
  • 02.requestLayout绘制
    • 2.1 源码流程分析
    • 2.2 View绘制核心思想
    • 2.3 View绘制流程
    • 2.4 三大流程概括
  • 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刷新操作
    • 7.1 View如何显示在屏幕
    • 7.2 requestLayout刷新
    • 7.3 invalidate刷新
    • 7.4 postInvalidate刷新

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高度,其布局流程会发生什么变化?
  • 绘制:在View中draw绘制思路是如何设计的?ViewGroup中dispatchDraw分发绘制设计思路是怎样的?
  • 绘制:说一下绘制中surface起到什么作用?draw里面绘制了那些东西,简要介绍一下?

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() {
    //核心逻辑:从这个方法开始我们的View经过大小测量,位置测量,界面绘制三个逻辑操作。这里省略很多代码逻辑……
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    performLayout(lp, mWidth, mHeight);
    performDraw();
}

可以看到在方法performTraversals方法,我们调用了performMeasure,performLayout,performDraw三个方法,这几个方法主要用于测量View组件的大小,测量View组件的位置,绘制View组件;

即:测量大小 --> 测量位置 --> 绘制组件

image

2.2 View绘制核心思想

View 的绘制流程设计思路通常包括以下几个步骤:

  1. 测量(Measure):在绘制 View 之前,系统会先测量 View 的大小。这个过程包括确定 View 的宽度和高度,以及子 View 的布局参数。在测量过程中,系统会调用 View 的 onMeasure() 方法来计算 View 的大小。
  2. 布局(Layout):一旦 View 的大小确定,系统会根据布局参数来放置 View 在父容器中的位置。这个过程包括确定 View 的位置、边界和间距等。在布局过程中,系统会调用 View 的 onLayout() 方法来设置 View 的位置。
  3. 绘制(Draw):当 View 的大小和位置确定后,系统会调用 View 的 onDraw() 方法来绘制 View 的内容。在 onDraw() 方法中,你可以使用 Canvas 对象来绘制各种图形、文本、图片等内容。
  4. 处理触摸事件(Handle Touch Events):在绘制完成后,View 可能需要处理用户的触摸事件。这包括点击、滑动、拖动等用户交互操作。可以重写 View 的触摸事件处理方法(如 onTouchEvent())来实现相应的交互逻辑。
  5. 刷新(Invalidate):如果 View 的内容发生变化,你可以调用 invalidate() 方法来请求系统重新绘制 View。系统会在下一个绘制周期中调用 onDraw() 方法来更新 View 的内容。

2.3 View绘制流程

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

image

  1. ViewRootImpl#performTraversals()#performMeasure,执行View组件的onMeasure方法,主要用于测量View
  2. ViewRootImpl#performTraversals()#performLayout,执行View组件的onLayout方法,主要用于布局View
  3. ViewRootImpl#performTraversals()#performDraw,执行View组件的onDraw方法,主要用于绘制View,开始执行绘制的入口

View的绘制流程主要分为三步:

  • onMeasure:测量视图的大小,从顶层父View到子View递归调用measure()方法,measure()调用onMeasure()方法,onMeasure()方法完成绘制工作。
  • onLayout:确定视图的位置,从顶层父View到子View递归调用layout()方法,父View将上一步measure()方法得到的子View的布局大小和布局参数,将子View放在合适的位置上。
  • onDraw:绘制最终的视图,首先ViewRoot创建一个Canvas对象,然后调用onDraw()方法进行绘制。

2.4 三大流程概括

1.onMeasure()方法

  • 单一View,一般重写此方法,针对wrap_content情况,规定View默认的大小值,避免于match_parent情况一致。ViewGroup,若不重写,就会执行和单子View中相同逻辑,不会测量子View。一般会重写onMeasure()方法,循环测量子View。
  • Measure完成后可以通过getMeasureWidth和getMeasureHeight方法获取到view的测量后的宽高,在几乎所有的情况下都会等于最终view的宽高
  • onMeasure()方法接收两个参数,widthMeasureSpec和heightMeasureSpec,这两个值分别用于确定视图的宽度和高度的规格和大小。

2.onLayout()方法:

  • 单一View,不需要实现该方法。ViewGroup必须实现,该方法是个抽象方法,实现该方法,来对子View进行布局。
  • layout 过程决定了View的四个顶点的坐标和实际的View的宽高,完成以后可以通过getTop,getBottom,getLeft,getRight来获取View的四个顶点位置,并通过getWidth,getHeight获取View的最终宽高

3.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测量,这里是测量的入口。通过递归地测量子 View,整个 View 树能够正确地计算出每个 View 的大小和位置,从而实现正确的布局和显示效果。

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设计思路

3.2.1 View测量逻辑

View的测量目标便是测量控件的宽高值,View的设计者通过代码编织了一整套复杂的逻辑:

  • 1、对于子View而言,其本身宽高直接受限于父View的布局要求,举例来说,父View被限制宽度为40px,子View的最大宽度同样也需受限于这个数值。因此,在测量子View之时,子View必须已知父View的布局要求,这个布局要求, Android中通过使用 MeasureSpec 类来进行描述。
  • 2.1、对于完整的测量流程而言,父控件必然依赖子控件宽高的测量;若子控件本身未测量完毕,父控件自身的测量亦无从谈起。Android中View的测量流程中使用了非常经典的递归思想:对于一个完整的界面而言,每个页面都映射了一个View树,其最顶端的父控件测量开始时,会通过 遍历 将其 布局要求 传递给子控件,以开始子控件的测量,子控件在测量过程中也会通过 遍历 将其 布局要求 传递给它自己的子控件,如此往复一直到最底层的控件…这种通过遍历自顶向下传递数据的方式我们称为 测量过程中的“递”流程。
  • 2.2、当最底层位置的子控件自身测量完毕后,其父控件会将所有子控件的宽高数据进行聚合,然后通过对应的 测量策略 计算出父控件本身的宽高,测量完毕后,父控件的父控件也会根据其所有子控件的测量结果对自身进行测量,这种从底部向上传递各自的测量结果,最终完成最顶层父控件的测量方式我们称为测量过程中的“归”流程,至此界面整个View树测量完毕。

需要注意的是,子控件的测量过程本身还应该依赖于父控件的一些布局约束,比如:

  • 1.父控件固定宽高只有xpx,子控件设置为layoutheight="{x}px,子控件设置为layout_height="xpx,子控件设置为layoutheight="{y}px";
  • 2.父控件高度为wrap_content(包裹内容),子控件设置为layout_height=“match_parent”;
  • 3.父控件高度为match_parent(填充),子控件设置为layout_height=“match_parent”;

这些情况下,因为无法计算出准确控件本身的宽高值,简单的通过setMeasuredDimension()函数似乎不可能达到测量控件的目的,因为 子控件的测量结果是由父控件和其本身共同决定的,而父控件对子控件的布局约束,便是前文提到的 布局要求,即MeasureSpec类。

3.2.2 MeasureSpec意图设计

测量流程中对布局的设计是通过MeasureSpec类来对其进行描述。在设计的过程中,我们将布局要求分成了2个属性。测量大小 意味着控件需要对应大小的宽高,测量模式 则表示控件对应的宽高模式:

模式(Mode):MeasureSpec 中的模式部分定义了 View 的大小如何被测量。主要有三种模式:

  • UNSPECIFIED:父元素不对子元素施加任何束缚,子元素可以得到任意想要的大小;日常开发中自定义View不考虑这种模式,可暂时先忽略;
  • EXACTLY:父元素决定子元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;这里我们理解为控件的宽或者高被设置为 match_parent 或者指定大小,比如20dp;
  • AT_MOST:子元素至多达到指定大小的值;这里我们理解为控件的宽或者高被设置为wrap_content。

大小(Size):MeasureSpec 中的大小部分指定了 View 在某个方向(宽度或高度)上的大小限制。

  • 这个大小可以是具体的像素值,也可以是特殊值 MATCH_PARENT(填充父容器)或 WRAP_CONTENT(根据内容自适应大小)。

设计 MeasureSpec 的意图是为了让开发者能够更好地控制 View 的大小和布局,同时保证 View 在不同情况下都能正确地测量和显示。通过合理地使用 MeasureSpec,可以确保 View 在不同屏幕尺寸和布局要求下都能够正确地适应和显示。

3.2.3 MeasureSpec值设计

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刷新操作

7.1 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;
7.2 requestLayout刷新
7.3 invalidate刷新
7.4 postInvalidate刷新
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值