Android View的绘制流程分析

1 Android UI 框架的核心类

在梳理View的绘制流程之前,我们先来熟悉一下AndroidUI框架中几个重要类的概念:

Window: 外观和行为策略的抽象基类,它的实例应该作为顶级视图被添加到 Window Manager 中。它提供标准的UI策略,例如 background,标题区域,默认关键处理等。

PhoneWindow: 是 Window 的唯一实现类,在 Activity 的 attach() 方法中构造一个 PhoneWindow 实例。

DecorView: 在 PhoneWindow 中创建的顶级视图,是 View Tree 的实际根节点,由此可见,View 是依附在 Window 之上的。

ViewRootImpl: 视图层次结构的顶部,在 View(DecorView) 和 WindowManager 之间实现所需的协议。 由 WindowMangerGlobal 的 addView() 方法创建。

WindowManagerImpl: 实现了 WindowManager 接口,是整个应用程序中所有 Window 的管理者。在 Activity 的 attach() 方法中由 PhoneWindow 调用 setWindowManager() 方法创建。

WindowManagerGlobal: 在 WindowManagerImpl 中创建的单例对象,在整个 Android 进程中是一个全局变量,它包含了能和 WMS 进行双向 Binder 通信的通道。一个应用进程中所有的 Activity 都是通过这个进程内唯一的 WindowManagerGlobal 对象和 WMS 进行通信。

WindowManagerService: 远程窗口管理类,从它的角度,并不关心应用中的各种View,它管理和调度的对象是Window。

WindowManager 类关系图

总结:

在 Activity 的 attach() 方法中,会创建 PhoneWindow 和 WindowMangerImpl 实例。而 WindowMangerImpl 持有 WindowManagerGlobal 的引用。

在 Activity 的 onCreate() 回调方法中,setContentView() 方法会调用 PhoneWindow#setContentView() 方法,如果 DecorView 对象还未创建,会调用 installDecor() 方法来创建 Activity 中的 DecorView。并将 inflate 出的 View 添加到 DecorView的 ContentView 中,而 ContentView 继承自 FrameLayout。

在 Activity 的 makeVisible() 方法中,调用了 WindowManager (WindowManagerGlobal) 的 addView() 方法,参数是 DecorView 对象,在addView() 方法中最重要的是创建了ViewRootImpl 对象,并通过 setView() 方法把 ViewRootImpl 对象和 DecorView 对象关联在一起。

WindowManagerService 通过与 WindowManagerGlobal 的双向 Binder 通信完成对 Window (PhoneWindow) 的管理。

WindowManagerGlobal 通过 ViewRootImpl 完成对 Window (PhoneWindow) 和 View (DecorView) 的管理。

2 View 的绘制流程分析

下面开始对View的绘制流程进行梳理:

首先,View Tree 发生重绘基本都会跟 View # requestLayout() 或 View # invalidate() 方法的调用相关。

那么我们首先来看一下 View 中的 requestLayout() 方法的源码

View # requestLayout()

可以看到,16行,在 mPrivateFlags 中添加了 PFLAG_FORCE_LAYOUT 标记,该标记将在之后的 measure 流程中标记哪些View 需要重新进行尺寸的测量。第20行,调用了父View 中的 requestLayout() 方法,该方法会一直向上传递,直到父View 是 ViewRootImpl,最终调用 ViewRootImpl # requestLayout() 方法,开始一次 View Tree 遍历。

那么我们来看一下 ViewRootImpl 中 requestLayout() 中的源码

ViewRootImpl # requestLayout()

在 RootViewImpl 类中,requestLayout() 和 invalidate() 方法都会调用 scheduleTraversals(),这是一个异步方法,最终会调用到 performTraversals() 方法,它是 View 绘制流程的核心方法,分别调用 performMeasure()、performLayout()、performDraw() 方法来进行 View 的尺寸测量 (measure),位置测量 (layout),绘制 (draw) 三大流程

RootViewImpl # performTraversals()

2.1 measure 测量 View 的大小

在 ViewRootImpl # performTraversals() 中我们看到调用了 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); 方法,参数 childWidthMeasureSpec 和 childHeightMeasureSpec 是 MeasureSpec 类型,分别表示了该 ViewRootImpl 关联的 顶层 View 对象的尺寸。注意,该顶层 View 在Activity 中是 DecorView,但是若只是通过 WindowManger # addView添加的悬浮框则不是。(MeasureSpec 的概念不再赘述)

若顶层 View 是 DecorView 对象,DecorView 继承自 FrameLayout,而 ViewGroup 继承自 View 类,且 View 的 measure() 方法是 final 修饰的,并不能被子类重写,因此我们来一起看一下这个 View # measure() 方法。

View # measure

可以看到,第4行中,使用到了起初 View # requstLayout() 中进行标记的 PFLAG_FORCE_LAYOUT, 用以表明该 View 是否需要被重新测量(measure),在27行,调用了onMeasure 方法,由于 DecorView 是 FrameLayout 子类,因此它实际上调用的是 DecorView # onMeasure() 方法。37行,mPrivateFlags 添加了 PFLAG_LAYOUT_REQUIRED 标记,表示该 View 需要被重新布局(layout)。下面我们来看一下 FrameLayout # onMeasure() 方法。

FrameLayout # onMeasure()

FrameLayout # onMeasure() 方法, 20行,调用 measureChildWithMargins() 对子 View 进行测量,该方法需要除去父容器的 padding 和 margin之后计算子 View 的大小,这个过程会递归测量所有的子View 直到达到叶子节点。maxWidth 和 maxHeight 记录了子类中的最大宽度和最大高度,并添加 padding ,并把测量大小赋值给该 FrameLayout。

概括一下整个流程:测量始于DecorView,通过不断的遍历子View的measure方法,根据ViewGroup的MeasureSpec及子View的LayoutParams来决定子View的MeasureSpec,进一步获取子View的测量宽高,然后逐层返回,不断保存ViewGroup的测量宽高。

2.2 layout 计算 View 的位置

对于DecorView来说,调用layout方法,就是对它自身进行布局,注意到传递的参数分别是0, 0, host.getMeasuredWidth, host.getMeasuredHeight,它们分别代表了一个View的上下左右四个位置,显然,DecorView的左上位置为0,然后宽高为它的测量宽高。

View # layout()

在13行,setOpticalFrame(l, t, r, b) 和 setFrame(l, t, r ,b) 都是对 View 的左上角和右下角进行赋值,而在15行,当 changed = true 表示View 的位置发生了变化,或者 PFLAG_LAYOUT_REQUIRED 标记满足时,会调用 View 的 onLayout() 方法,若该方法在 ViewGroup 中调用,用于确定子 View 的位置,即在该方法内部,子View会调用自身的 layout 方法来进一步完成自身的布局流程。由于不同的布局容器的onMeasure方法均有不同的实现,现在仍然以 FrameLayout # onLayout 方法进行讲解。

FrameLayout # onLayout() 和 layoutChildren()
FrameLayout # onLayout() 和 layoutChildren()

在 FrameLayout 的 onLayout() 方法中,不需要考虑其他常用布局如 LinearLayout,GridLayout 等有各子对象竞争布局空间的问题,FrameLayout 中的各子View 是“层叠”而成的,只需要考虑到 Gravity 属性来确定子View 相对于父布局的位置。

第5行,在 onLayout 方法内部直接调用了layoutChildren方法

先梳理一下以上逻辑:首先先获取父容器的 padding 值,然后遍历其每一个子 View,根据子View 的 layout_gravity 属性、子View的测量宽高、父容器的 padding 值、来确定子View的布局参数,然后调用 child.layout 方法,把布局流程从父容器传递到子元素。

那么,现在就分析完了ViewGroup的布局流程,那么我们接着分析子元素的布局流程。

子View的布局流程

子View的布局流程也很简单,如果子View 是一个 ViewGroup,那么就会重复以上步骤,如果是一个View,那么会直接调用 View # layout 方法,根据以上分析,在该方法内部会设置 view 的四个布局参数,接着调用onLayout方法,我们看看 View # onLayout方法:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}

2.3 draw 绘制 View 的内容

一个 View 的 layout 确定之后,才能在此基础上执行 draw 流程。

由于 ViewGroup 没有重写 draw 方法,因此所有的 View 都是调用 View#draw 方法,因此,我们直接看它的源码

View # draw()

可以看到,draw过程比较复杂,但是逻辑十分清晰,而官方注释也清楚地说明了每一步的做法。我们首先来看一开始的标记位dirtyOpaque,该标记位的作用是判断当前View是否是透明的,如果View是透明的,那么根据下面的逻辑可以看出,将不会执行一些步骤,比如绘制背景、绘制内容等。这样很容易理解,因为一个View既然是透明的,那就没必要绘制它了。接着是绘制流程的六个步骤,这里先小结这六个步骤分别是什么,然后再展开来讲。

绘制流程的六个步骤: 

1、对View的背景进行绘制 

2、保存当前的图层信息(可跳过) 

3、绘制View的内容 

4、对View的子View进行绘制(如果有子View) 

5、绘制View的褪色的边缘,类似于阴影效果(可跳过) 

6、绘制View的装饰(例如:滚动条) 

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

推荐阅读更多精彩内容