探索 Android View 绘制流程

最近重新看了一下任玉刚大佬的《Android 开发艺术探索》,写了篇笔记,分享给大家。

1. ViewRootImpl 与 DecorView

image

接下来的讲解的源码版本为 Android 10 。

ViewRootImpl 是连接 WindowManagerDecorView 的纽带,测量、放置和绘制三大流程都是通过 ViewRootImpl 实现的。

ActivityThread 的 handleResumeActivity() 方法中,会调用 WindowManager 的 addView() 方法,而具体添加 DecorView 的操作是在 WindowManagerGlobal 中。

在 WindowManagerGlobal 的 addView() 方法中,会把 DecorView 添加到 Window 中,同时会创建 ViewRootImpl ,并调用 ViewRootImpl 的 setView() 方法 把 ViewRootImpl 和 DecorView 关联起来。

View 的绘制流程是从 ViewRootImpl 的 performTraversals() 方法开始的,它经过测量(measure)、放置(layout)和绘制(draw)三个过程才能把一个 View 绘制出来,measure() 方法用于测量 View 的宽高,layout() 用于确定 View 在父容器中的放置位置,draw() 负责做具体的绘制操作。

针对 performTraversals 的大致流程,可用下图表示。

image

View 绘制主要的三个方法就是 onMeasure()onLayout()onDraw(),这三个方法要解决的问题就是画多大在哪画画什么

ViewRootImpl 的 performTraversal() 方法会依次调用 performMeasure()performLayout()performDraw() 三个方法,这三个方法分别完成 DecorView 的测量、放置和绘制三大流程。

performMeasure() 方法会调用 DecorView 的 measure() 方法,在 measure() 方法中又会调用自己的 onMeasure() 方法。

DecorView 的 onMeasure() 方法会调用父类 FrameLayout 的 onMeasure() 方法,在 FrameLayout 的 onMeasure() 方法中,会调用子元素的 onMeasure() 方法测量子元素的宽高,接着子元素会重复父容器的 measure 过程,如此反复完成整个 View 树的遍历。

而 performLayout() 和 performDraw() 的执行流程与 performMeasure() 是类似的。

measure 过程决定了 View 的宽高,layout 过程决定了 View 的四个顶点的坐标和实际的 View 宽高,draw 过程则决定了 View 的具体绘制操作,只有 draw() 方法完成后 View 的内容才会在屏幕上展示。

1.1 Activity 视图层级结构

假如我们有一个继承了 AppCompatActivity 的 MainActivity,并且 activity_main 布局的内容如下。

image

我们现在能感知到的视图层级是下面这样的。

image

当我们在 MainActivity 中调用父类的 setContentView() 后,AppCompatActivity 会调用 AppCompatDelegateImpl 的 setContentView() 方法,AppCompatDelegateImpl 在这个方法中会把 RelativeLayout 添加到 id 为 content 的 ViewGroup 中。

image

其中 ContentFrameLayout 也就是 id 为 content 的 ViewGroup 。

image

ensureSubDecor() 方法会在 subDecor 没有初始化时用 createSubDecor() 方法创建 subDecor ,createSubDecor() 方法会调用 Window 的 setContetnView() 方法,把 abc_screen_toolbar 布局设为 Window 的内容视图,而这里的 mHasActionBar 只有在 feature 为 FEATURE_SUPPORT_ACTION_BAR 时才会为 true。

abc_screen_toolbar 布局的内容如下。

image

把 RelativeLayout 放到 mSubDecor 中后,视图层级就变成下面这样了。

image

Window 的实现类为 PhoneWindow,在 PhoneWindow 的 setContentView() 方法中,会调用 installDecor() 方法创建 DecorView ,然后调用 LayoutInflate 的 inflate() 方法把 ActionBarOverlayLayout 加入到 DecorView 中。

image

在 installDecor() 方法中,会调用 generateLayout() 方法生成 mContentParent。

generateLayout() 方法中,会根据不同的 feature 来生成不同的 DecorView,比如没有设定任何 feature 时,对应的 DecorView 的布局就是 screen_simple

screen_simple 布局的实现如下。

image

前面的布局加入到 screen_simple 中后,视图层级就是下面这样的。

image

这里的 action_mode_bar_stub 是用来显示 ActionMode 的,而 FrameLayout 就是 ID_ANDROID_CONTENT 对应的 ViewGroup。

到这里好像还是少了点什么,状态栏哪去了?

image

根据 Layout Inspector 的分析,LinearLayout 下面还有一个 id 为 statusBarBackground 的 View ,根据这个 id 在 DecorView 中找到了对应的 mStatusColorViewState 。

image

而在 DecorView 的 updateColorViewInt() 方法中,则把状态栏通过 addView() 方法添加到了 DecorView 中。

该方法的调用时序图如下。

image

也就是完整的 DecorView 视图层次如下。

image

上图对应的 View 树如下。

image

2. 测量规格 MeasureSpec

按注释来说,MeasureSpec 封装了从父 View 传给子 View 的布局要求,MeasureSpec 在很大程度上决定了一个 View 的尺寸规格,具体的尺寸会受到父容器的影响,因为父容器影响 View 的 MeasureSpec 的创建过程。

在测量过程中,系统会把 View 的 LayoutParams 根据父容器设定的规则转换为对应的 MeasureSpec,然后再根据这个 MeasureSpec 测量出 View 的宽高。

要注意的是,这里的说的宽高是测量宽高,不一定是 View 的最终宽高,原因后面会讲到。

MeasureSpec 代表一个 32 位 int 值,高 2 位代表测量模式 SpecMode,低 30 位代表规格大小 SpecSize,MeasureSpec 通过把 SpecMode 和 SpecSize 打包成一个 int 值避免过多的对象内存分配,

MeasureSpec 中定义了下面三种测量规格。

image
  • 待定 UNSPECIFIED

    表示父 View 对子 View 的大小不做限制;

  • 精确 EXATCTLY

    父 View 计算好了子 View 具体的宽高,子 View 的最终大小就是 SpecSize 指定的值;

  • 最多 AT_MOST

    父 View 指定了一个可用大小,View 的大小不能大于这个值;

MeasureSpec 用来打包 SpecMode 和 SpecSize 的方法是 makeMeasureSpec() ,代码如下。

image

3. MeasureSpec 与 LayoutParams 的关系

在 View 测量时,系统会把 LayoutParams 在父 View 的约束下,转换成对应的 MeasureSpec,然后再根据这个 MeasureSpec 确定 View 测量后的宽高,要靠 LayoutParams 和父 View 一起才能决定子 View 的 测量模式。

DecorView 的测量规格由窗口的尺寸和其 LayoutParams 共同确定,而普通 View 的测量规格由父 View 的 MeasureSpec 和自身的 LayoutParams 决定,MeasureSpec 确定后,就可以在 onMeasure() 方法中确定 View 的测量宽高。

在 ViewRootImpl 的 performTraversals() 方法中,有一段调用 measureHierarchy() 方法的代码,也就是传给 measureHierarchy() 的大小为屏幕尺寸。

image

measureHierarchy() 方法是用来设定子 View ,也就是 DecorView 的大小的。

image

measureHierarchy() 中的的 childWidthMeasureSpec 和 childHeightMeasureSpec 就是 DecorView 的测量规格 MeasureSpec。

image

通过上面代码可以看出,DecorView 会根据 LayoutParams 中的宽高来设定宽高测量规格。

  • MATCH_PARENT

    精确模式,DecorView 大小就是窗口大小;

  • WRAP_CONTENT

    最大模式,大小不定,但是不能超过窗口大小;

  • 固定大小

    精确模式,大小为 LayoutParams 中指定的大小;

对于普通 View 来说,View 的 measure 过程由 ViewGroup 传递而来,而 ViewGroup 是在 measureChildWithMargins() 方法中确定子 View 的测量规格的。

image

下面是 ViewGroup 的 getChildMeasureSpec() 方法获取子 View 的测量规格的方式。

image

其中一段代码如下。

image

上面这段代码中的 size 是去掉了 padding 后的 size。

这里要注意的是,不是所有 ViewGroup 都会用这样的方式决定子 View 的测量规格,比如 RelativeLayout 用的就是不一样的测量规格。

4. View 测量过程

对于 ViewGroup,除了要完成自己的测量,还要遍历调用子元素的 measure() 方法,而 View 只需要通过 measure() 方法就能确定测量规格。

View 的测量过程由 View 的 measure() 方法完成,measure() 方法是一个 final 类型的方法,子类不能重写。

View 的 measure() 方法会调用 onMeasure() 方法,这个方法我们是可以重写的,onMeasure() 的实现如下。

image

widthMeasureSpec 和 heightMeasureSpec 是从父 View 传过来的宽高测量规格,getDefaultSize() 方法是用来获取默认宽高的,getDefaultSize() 的实现如下。

image

从 getDefaultSize() 方法中可以看出,当测量模式为 UNSPECIFIED 时,宽/高就是最小宽/高,当测量模式为 AT_MOST 或 EXACTLY 时,宽/高就是 ViewGroup 指定的 SpecSize。

View 的宽/高由 specSize 决定,直接继承 View 的自定义控件需要重写 onMeasure() 方法并设置 wrap_content 时的自身大小,否则咋布局中使用 wrap_content 相当于使用 match_parent 。

从前面的代码可以了解到,如果 View 在布局中使用 wrap_content,那么它的 specMode 是 AT_MOST 模式,这时它的宽/高为 specSize ,这时 View 的 specSize 为 ViewGroup 的 specSize。

比如 activity_main 的布局是下面这样的。

image

那么 MyView 测量后的大小就是 600 ,这个 600 是 dp 换算为 px 后的值。

image-20201208165443702

ViewGroup 的 SpecSize 是自身剩余的空间大小,也就是默认子 View 的宽/高为父 View 的剩余控件大小,相当于为宽/高设定的 wrap_content 无效,变成了 match_parent 。

如果我们不想让自定义 View 在宽/高设为 wrap_content 时与父 View 的大小一致,那我们可以像下面这样设定自己的计算好的默认宽/高。

image

下面来看下 ViewGroup 的测量过程。

ViewGroup 是一个抽象类,没有定义测量的的具体过程,具体的测量过程需要子类实现,下面以 LinearLayout 为例,看一下它的 onMeasure() 方法的实现。

image

LinearLayout 会根据我们设定的方向设定子 View 的测量规格,下面来看下 measureVertical() 的实现。

image

在 measureVertical() 方法中,把每一个子元素都传给了 measureChildBeforeLayout() ,而 measureChildBeforeLayout() 只是调用了 ViewGroup 的 measureChildWithMargin() 方法。

image

5. View 放置过程

layout() 方法的作用是 ViewGroup 用于确定子元素的位置,当 ViewGroup 的位置确定后,会在 onLayout() 方法中遍历所有子 View 并调用子 View 的 layout() 方法。

layout() 方法用于确定 View 自己的位置,而 onLayout() 方法则用于确定所有子元素的位置,View 的 layout() 方法的实现如下。

image

View 的 layout() 方法首先会通过 setFrame() 方法设定 View 的边框,也就是 mLeft、mRight、mTop 和 mBottom 四个顶点的值,这时 View 在父 View 中的位置就确定了。

image

设定了四个顶点后,layout() 方法就会调用 onLayout() 方法确定子 View 的位置,View 和 ViewGroup 都没有实现 onLayout() 方法,下面以 LinearLayout 为例,看下 LinearLayout 的 onLayout() 方法的实现。

LinearLayout 的 onLayout() 方法会根据不同的排列方向调用不同的放置方法,当方向为 VERTICAL 时,对应的放置方法为 layoutVertical() ,下面来看下 layoutVertical() 方法的实现。

LinearLayout.layoutVertical__.png

layoutVertical() 方法会遍历所有子 View 并调用 setChildFrame() 方法指定子 View 的边框(frame)在哪个位置,而 setChildFrame() 方法只是简单调用了 子 View 的 layout() 方法。

childTop 的值会逐渐增加,下一个子 View 的 top 为上一个子 View 的 bottom,也就是排列方向为 VERTICAL 的 LinearLayout 的特性。

6. View 绘制过程

View 绘制分为下面 6 步:

  1. 绘制背景
  2. 保存 Canvas 图层为后续淡出做准备(可选)
  3. 绘制 View 的内容
  4. 绘制子 View (dispatchDraw)
  5. 绘制淡出边缘并恢复 Canvas 图层(可选)
  6. 绘制装饰(比如 foreground 和 scrollbar)

一般情况下第 2 步和第 5 步是不执行的。

View.draw__.png

下面来看下绘制相关方法的实现。

View.drawBackground__.png

drawBackground() 方法首先会通过 Drawable 的 setBounds() 方法设置背景绘制的范围,然后如果我们调用过 scrollTo() 方法,那么 drawBackground() 就会把画布平移到指定位置后再绘制。

View 和 ViewGroup 没有实现 onDraw() 方法,接下来就是 dispatchDraw() 方法,View 没有实现这个方法,下面来看下 ViewGroup 的 dispatchDraw() 方法的实现。

ViewGroup.dispatchDraw__.png

在 ViewGroup 的 dispatchDraw() 方法中,首先会调用 buildOrderedChildList() 方法获取子 View 列表,然后遍历子 View ,通过 drawChild() 方法调用每一个子 View 的 draw() 方法。

而第 6 步 drawForegounrd() 只是获取 foreground 对应的 Drawable 并调用它的 draw() 方法。

7. 总结

根据前面讲解的内容,从 ViewRootImpl 的 performTraversals() 方法开始,大致的方法调用时序图如下。

image

View 绘制的三大过程分别是测量、放置和绘制,对应的的三个方法为 onMeasure() 、onLayout() 和 onDraw() 。

测量过程中最重要的就是理解 MeasureSpec 以及自定义 View 时要重写 onMeasure() 方法设置默认宽高。

MeasureSpec 由测量模式 SpecMode 和 SpecSize 组成,SpecMode 分为待定(UNSPECIFIED)、精确(EXACTLY)和最大(AT_MOST)。

放置过程中最关键的方法就是 setFrame() ,这个方法会把父 View 在 onLayout() 方法中计算好的四个顶点的值赋值给 mTop、mLeft 、mRight 和 mBottom 。

绘制过程的 draw() 方法中主要的 4 个绘制步骤为:绘制背景、绘制 View 内容、绘制子 View 内容以及绘制装饰。

参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335

推荐阅读更多精彩内容