Android——View的工作原理

Posted SyubanLiu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android——View的工作原理相关的知识,希望对你有一定的参考价值。

前言

要想实现自定义View,那么需掌握View的事件体系及其底层工作原理,本节主要围绕View的工作原理来开展,View的事件体系可见另一篇文章。

View的底层工作原理主要涉及到三个流程:View的测量(measure)、View的布局(layout)、View的绘制(draw)。除了这三大基本流程外,有几个View的回调方法也需掌握,如:构造方法、onAttach、onVisibilityChanged、onDetach等等。

自定义View的实现,主要有几种固定类型,有的继承View或者ViewGroup,有的直接继承现有的系统控件

1. ViewRoot和DecorView

ViewRoot对应于ViewRootImpl类,是连接WindowManager和DecorView的纽带,View的三大流程都是通过ViewRoot来完成的。在ActivityThread中,当Activity对象被创建完成后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象(具体细节可去了解android启动流程),并将ViewRootImpl对象和DecorView建立关联。

root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);

View的绘制流程是从 ViewRoot 的 performTraversals 方法开始的,它经过 measure、layout和draw 三个过程后才能最终将一个View绘制出来。其中measure用于测量View的宽高,layout用于View在父容器的放置位置,draw用于View的绘制。

private void performTraversals() {
    // 这个方法代码非常多,但是重点就是执行这三个方法
    // 执行测量
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    // 执行布局(ViewGroup)中才会有
    performLayout(lp, mWidth, mHeight);
    // 执行绘制
    performDraw();
}

在这里插入图片描述

performTraversals 会依次调用 performMeasure、performLayout 和 performDraw 三个方法。

Measure 过程决定了View的宽高,Measure完成以后,可以通过 getMeasureWidth 和 getMeasureHeight 方法来获取到View的测量后的宽高,在一般情况下,它都等于View的最终的宽高,但在某些特情况下除外。

Layout 过程决定了View的四个顶点的位置坐标和实际View的宽高,分别对应着View的 top、left、right、bottom 四个顶点位置,并可以通过 getWidth 和 getHeight 来获取View的实际宽高。

Draw 过程决定View的显示,只有draw完成以后,View才能呈现在屏幕上。

2. MeasureSpec(测量规范)

MeasureSpec 参与View的 measure 过程,很大程度上决定一个View的尺寸规格。在View的测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的 MeasureSpec,然后再根据其测量出View的宽高。

MeasureSpec 代表一个 32位int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(测量模式下的大小)。通过这种方式,可以避免过多的内存开销。一组SpecMode和SpecSize可以打包成一个MeasureSpec。

2.1 MeasureSpec基础

关于SpecMode,主要有三类:

  1. UNSPECIFIED:父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
  2. EXACTLY:父容器检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize指定的值。对应于LayoutParams中的 match_parent 和 具体的数值 这两种模式。
  3. AT_MOST:父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值。对应于LayoutParams的wrap_content。

2.2 MeasureSpec 和 LayoutParams 对应关系

在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后根据这个MeasureSpec来确定View测量后的宽高。因此,View的 MeasureSpec 是由 View自身LayoutParams 和 父容器的MeasureSpec 一块决定的。

对于DecorView,其MeasureSpec由窗口的尺寸和自身的LayoutParams共同决定;对于普通的View,其MeasureSpec由父容器的MeasureSpec和自身LayoutParams来共同决定

SpecMode的规则根据LayoutParams中的宽高这样划分:

  • LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小;
  • LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小;
  • 固定大小:精确模式,大小为LayoutParams中指定的大小;

在这可以简单说一下:

  • 当View采用固定宽高时,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式并且其大小遵循LayoutParams中的大小;
  • 当View宽高是 match_parent 时,如果父容器是精确模式,那么View也是精确模式且其大小是父容器的剩余空间;若父容器是最大模式时,那么View也是最大模式且其大小不会超过父容器的剩余空间;
  • 当View的宽高时 wrap_content 时,不管父容器是哪种模式,View的模式总是最大化且大小不超过父容器的剩余空间。

3. View的工作流程

View的工作流程主要是指:measure、layout、draw,即 测量、布局和绘制。measure确定View的测量宽高,layout确定View的四个顶点位置及最终宽高,draw负责将View绘制到屏幕上。

3.1 measure过程

3.1.1 View的measure过程

对于 AT_MOST 和 EXACTLY 这两种情况, getDefaultSize 返回的大小就是 measureSpec 中的 SpecSize,而这个SpecSize就是测量后的大小

对于 UNSPECIFIED,一般用于系统内部的测量过程,在这种情况下,有两个方法比较重要: getSuggestedMinimumWidth 与 getSuggestedMinimumHeight,这两个方法的的逻辑是:如果View没有设置背景,那么View的宽度为View的mMinWidth,这个值可以是0;若View设置了背景,那么就会通过 max(mMinWidth, mBackground.getMinimumWidth()) 去取最大值,这两方法返回值就是View在UNSPECIFIED情况下的测量宽高。

因此,直接继承View的自定义控件需要重写 onMeasure 方法并设置 wrap_content 时的自身大小,否则在不居中使用 wrap_content 相当于使用了 match_parent。解决办法就是给View指定一个默认的内部宽高并在 wrap_content 时设置此宽高接口。

3.1.2 ViewGroup的measure过程

对于ViewGroup来说,除了完成自身的measure过程外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个过程。在 measureChild 方法中,思想是去取子元素的LayoutParams,然后再通过 getChildMeasureSpec 来创建子元素的 MeasureSpec,接着将 MeasureSpec 直接传递给View的measure方法来进行测量。

因此,ViewGroup 的 measure 过程是先去遍历测量子元素的宽高,当子元素测量完毕后,ViewGroup再去根据子元素的情况来测量自身的宽高

View的measure过程是三大流程中最复杂的一个,当measure完成后,可以通过 getMeasureWidth 和 getMeasureHeight 来获取测量的宽高,在一般情况下,测量的宽高等于最终的宽高,但由于 padding 或者 margin 等因素会导致不同,因此建议是在 onLayout 下去获取View的最终宽高。

问题来了,如何在Activity启动过程中去获取某个View的宽高呢?由于View的measure过程跟Activity的生命周期不是同步执行的,所以无法保证在Activity某个生命周期时,该View已经测量完成,如果没有测量完成,那么取出来的宽高就为0。那么接下来提供几种方法来获取:

  1. Activity/View#onWindowFocusChanged:当Activity窗口的焦点丢失或者得到时,都会调用此方法,因此会存在频繁调用问题,即Activity的onResume与onPause频繁执行时,该方法也会被频繁执行。但在这个方法中,View已经初始化完毕,可去获取View的宽高。
  2. view.post(runnable):通过post可以将一个runnable投递到消息队列的尾部(这里可去研究Android启动流程、VSync刷新、Handler的同步屏障),然后等待Looper调用此runnable时,View已经初始化完毕。
  3. ViewTreeObserver:使用 ViewTreeObserver 众多回调可以完成此操作,例如 OnGlobalLayoutListener 这个接口,当View树的状态发生改变或者View树内部的可见性发生改变时,onGlobalLayout就会被回调。但要注意的是,如果View树的状态发生改变时,该方法可以会被执行多次。
  4. view.measure(widthMeasureSpec, heightMeasureSpec):通过手动的方式去对View进行measure过程来得到View的宽高,需要根据View的LayouParams分情况处理。

3.2 layout 过程

layout方法确定View本身的位置,而onLayout方法会确定子元素的位置。

layout 方法的大致流程如下:首先会通过 setFrame 方法来设定View的四个顶点的位置,即初始化 mLeft、mRight、mTop 和 mBottom,View的四个顶点一旦确定,那么View在父容器中位置也就确定了;接着调用onLayout 方法,这个方法时父容器确定子元素的位置,其具体的实现和具体的布局有关,所以View和ViewGroup都没有真正实现onLayout方法。

3.3 draw 过程

draw过程的流程如下:

  1. 绘制背景 background.draw(canvas)
  2. 绘制自己(onDraw)
  3. 绘制children(dispatchDraw)
  4. 绘制装饰(onDrawScrollBars)

View的绘制的传递是通过 dispatchDraw 来实现的,dispatchDraw 会遍历所有子元素的 draw 方法。

View还有一个特殊的方法 setWillNotDraw,如果一个View不需要绘制内容,那么会设置这个标志位为true,系统会进行相应的优化。在默认情况下,View没有启用这个优化标志位,ViewGroup会默认启用这个标志位。因此这个标志位的作用是:自定义继承于ViewGroup并且本身不需要绘制功能时,就开启该优化标志位;若明确ViewGroup需要进行绘制onDraw时,我们需要手动关闭这个 WILL_NOT_DRAW 标志位。

4. 自定义View

自定义View是一个综合体系,它涉及到 View的层次结构、事件分发机制和View的工作原理

4.1 自定义View的分类

  1. 继承 View 重写 onDraw 方法
  2. 继承 ViewGroup 派生特殊的 Layout
  3. 继承特定的 View (例如TextView)
  4. 继承特定的 ViewGroup (例如LinearLayout)

4.2 自定义View须知

  1. 让 View 支持 wrap_content(设定固定宽高)
  2. 如有必要让 View 支持 padding:如果不在View的onDraw中处理padding的话,那么padding属性是不起作用的
  3. 尽量不要在 View 中使用 Handler,可使用 View 自身的post
  4. View 中若有 动画或者线程,需要及时停止,避免发生内存泄露,View#onDetachedFromWindow
  5. 当 View 带有 滑动或者嵌套 情形时,需要处理好滑动冲突

以上是关于Android——View的工作原理的主要内容,如果未能解决你的问题,请参考以下文章

Android艺术开发探索第四章——View的工作原理(上)

[Android] View 工作原理

[Android] View 工作原理

Android UI绘制之View绘制的工作原理

Android学习笔记View的工作原理

[Android] View 工作原理