View 的绘制流程
Lynch Lee BIG_BOSS

虽然都说“面试造火箭,上班拧螺丝”,但是多看看源码和实现原理总归是好的。View 作为 Android 的基石之一,其绘制流程是非常重要的。小弟不才,在这里记录一下我对 View 绘制流程 的理解,未来等我理顺了可能还会写一篇事件分发机制的理解。本文会从基础的知识储备讲到各个绘制环节,文章包含的内容比较多,让我们开始吧!

基础知识储备

我们先来看几张图,首先是 Activity 视图架构图

Activity

Activity 不负责控制视图,它只负责控制生命周期并处理消息事件。控制视图的工作是由 Window 来完成的。一个 Activity 包含一个 Window ,Window 才是真正代表一个窗口,Activity 可以看作是这个窗口的控制器,负责视图的添加和现实,并通过其他回调方法,与 Window 及 View 进行交互

Window

Window 是视图的容器,其中持有一个 DecorView,DecorView 是 View 的根布局。Window 是一个抽象类,实际上 Activity 持有的是 PhoneWindow 这个子类。PhoneWindow 中有一个内部类 DecorView ,通过创建 DecorView 来加载 Activity 中编写的 XML 布局。Window 通过 WindowManager 将 DecorView 加载到其中,并将 DecorView 交给 ViewRoot,进行视图绘制以及其他交互。

DecorView

DecorViewFrameLayout 的子类,它是 Android 视图树的根节点视图。其作为顶级 View,一般情况下内部包含一个垂直布局的 LinearLayout,这里简单来说有两个部分:一是标题栏;二是内容栏, 通过 setContentView 设置到其中,一般通过代码获取的 content 就是这里的 id 为 content 的 FrameLayout(这里 LinearLayout 中具体有些什么还是取决于系统版本)。

ViewRoot

ViewRoot 的作用非常重要,这里的 View 绘制 和后面会提到的 事件分发机制 都是经由它来执行或者传递的。

ViewRoot 对应 ViewRootImpl 类,它是连接 WindowManagerServiceDecorView 的纽带,View 绘制的三大流程,即测量(measure),布局(layout),绘制(draw)都是由 ViewRoot 来完成。

ViewRoot 不属于视图树的一部分。从源码上看,其既不是 View 的子类,也不是 View 的父类,但是它实现了 ViewParent 接口,所以还是可以把它看作是 View 的父视图。RootView 中存在一个内部类变量继承了 Handler 类,因此可以接收并分发事件,Android 中所有的触摸、按键、界面刷新事件都是由它进行分发的。

四者之间的复杂关系

DecorView 的源码我们暂且不细说了(我不会说是我还没看明白)。

这里最后提一下 ViewRoot 对点击事件的分发,整个过程大致是:

硬件 → ViewRootImpl → DecorView → PhoneWindow → Activity

再往下就是 Android 的事件分发机制了,我们会在下篇文章中再来细说这部分。

小结

Activity 是控制器,负责控制 View 的生命周期并处理事件;Window 是 VIew 的容器;DecorView 是顶级View;ViewRoot 是连接器,连接用户与软件,是实现交互的桥梁。

绘制的基础框架

View 绘制的顺序是 DecorViewViewGroup * N → View,按照这个顺序依次往下,进行 measure(测量) → layout(布局) → draw(绘制)

Measure 流程

测量每个组件的大小

measure() 方法中对租价尺寸进行一下逻辑处理后,再调用 onMeasure() 方法通过 setMeasuredDimension() 设定 View 的尺寸信息,完成 View 的测量工作。

1
2
3
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
}
1
2
3
4
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

看起来测量流程非常简单,但是 widthMeasureSpecheightMeasureSpec 这两个重要的参数是怎么获得的?

MeasureSpec

首先 MeasureSpec 是一种测量规格,它是一个32位的 int 值。
它由占2个字节的测量模式 mode 作为 32 和 31 位,加上后30位的 size 表示具体的测量大小。

测量模式分为三类:

  • UNSPECIFIED :不对 View 进行任何限制,要多大给多大,一般用于系统内部
  • EXACTLY:对应 LayoutParams 中的 match_parent 和具体数值这两种模式。检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值,
  • AT_MOST :对应 LayoutParams 中的 wrap_content。View 的大小不能大于父容器的大小。

对于 DecorView 是通过屏幕大小和自身的布局参数 LayoutParams 确定其大小。

对于其他 View 包括 ViewGroup 则是通过父布局的 MeasureSpec 和自身 LayoutParams 确定,这部分比较复杂,简单来说就是设定为 wrap_content 时 View 的大小不能超过父布局,因此需要重写 onMeasure() 方法处理这个情况。

更具体的解析可以看看大佬的文章

测量流程图(我真的不想画图了,那天有空了再复刻一个自己版本,就借用大佬的图吧):

这里要注意的是:ViewGroup 的测量是依赖于 View 的测量的,因为 ViewGroup 中是没有 onMeasure() 方法的,它的 onMeasure() 是一个抽象方法,需要每个子 View 根据自己的布局特性去实现。

Layout 流程

测量完 View 后,就需要将其 布局 到 Window 上,Layout 主要是通过确定上下左右四个点来确定位置。

Layout 也是从父容器到子组件的顺序,不同的是 ViewGroup 先在 layout() 中确定自己的布局,然后在 onLayout() 方法中再调用子 View 的 layout() 方法,让子 View Layout 出来。在 Measure 过程中,就如上面一节提到的,ViewGroup 一般是先测量子 View 的大小,然后再确定自身的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void layout(int l, int t, int r, int b) {

...

int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;

// setOpticalFrame() 和 setFrame() 确定 View 自身位置
// 确定四个点的位置并检查位置是否发生变化
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

// 如果视图大小或者位置发生变化就调用 onLayout()
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// 确定该 View 中的所有子 View 在父容器中的位置
onLayout(changed, l, t, r, b);

...
}

其中 onLayout() 是一个空方法,只有当前 View 有子 View (即 ViewGroup)时才要实现这个方法,特别是自定义 View 时需要通过重写这个方法实现特定的效果。

布局流程图:

Draw 流程

绘制的过程如下:

  1. 绘制背景 drawBackground(canvas)
  2. 绘制自己 onDraw(canvas)
  3. 绘制 Children dispatchDraw(canvas)
  4. 绘制装饰 onDrawForeground(canvas)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public void draw(Canvas canvas) {

...

drawBackground(canvas);

// 如果有必要,就保存图层(还有一个复原图层)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {

// Step 3, draw the content
onDraw(canvas);

// Step 4, draw the children
dispatchDraw(canvas);

...

// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);

return;
}
...
}

所有的视图最终都会调用 View 的这个 draw(canvas) 方法绘制,在自定义 View 时,应该复写 onDraw(canvas) 方法实现绘制,如果说要复写这个方法应该先调用 super.draw(canvas) 完成系统的绘制,然后再进行自定义的 draw 方法来绘制。

更具体的源码分析可以看看这篇文章

绘制流程图:

自定义 View

我们在自定义 View 的时候需要根据自定义不同的 View 来分别实现不同的方法:onMeasure(), onLayout(), onDraw()

onMeasure()

  • 对于View,一般重写此方法,针对 wrap_content 情况,规定 View 默认的大小值,避免和 match_parent 情况一致。
  • 对于 ViewGroup,若不重写,就会执行和单一 View 中相同逻辑,不会测量子 View。一般会重写 onMeasure() 方法,循环测量子 View。

onLayout()

  • 对于 View,不需要实现该方法。
  • 对于 ViewGroup 必须实现,该方法是个抽象方法,实现该方法,来对子 View 进行布局。

onDraw()

无论 View 或是 ViewGroup 都需要实现该方法进行绘制。


参考链接:

简析Window、Activity、DecorView以及ViewRoot之间的错综关系

图解View测量、布局及绘制原理

  • Post title:View 的绘制流程
  • Post author:Lynch Lee
  • Create time:2021-05-11 16:40:38
  • Post link:https://neeomaclynch.github.io/2021/05/11/View-的绘制流程/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.
 Comments