【Android】Framework笔记——View的绘制流程(一)


我们都知道在 Activity 的启动过程,当 AMS 对启动 Activity 相关的信息进行校验确认无误后会调用 ActivityThread 中的 handleLaunchActivity 方法。

首先它会调用 performLaunchActivity 方法。在其中会调用 Instrumentation::callActivityOnCreate 方法调用 Activity 的 onCreate 方法,之后调用了 activity::performStart 方法从而通过 Instrumentation 间接地调用了 Activity 的 onStart 方法,performLaunchActivity 方法调用结束之后,会调用 handleResumeActivity 方法进行 Activity 的 resume 这一流程。

在 onCreate 方法中我们会通过 setContentView 对 Activity 设置 ContentView,并将其中的各种 View 创建出来。而真正的 View 的绘制过程,是在 handleResumeActivity 中进行的。

handleResumeActivity

接着又会调用到 handleResumeActivity 方法。

在这个过程中首先会调用 performResumeActivity 方法,它会调用到 Activity 的 onResume 方法。

之后又会调用 wm.addView(decor, l) 方法将 Activity 的 DecorView 添加到 WindowManager 中。

addView

在 WindowManagerImpl 的 addView 方法又会转调到 WindowManagerGlobal::addView。在它的 addView 方法中会创建这个 View 所对应的 ViewRootImpl,之后将 View 及其对应的 LayoutParams、 ViewRootImpl 分别添加至 mViews、mRoots、mParams 从而使其一一对应。之后,调用了 ViewRootImpl::setView 方法。

而在 ViewRootImpl 的 setView 方法中,会调用 requestLayout 方法触发界面的刷新,这就是 View 绘制的起点了。

scheduleTraversals

在 requestLayout 方法中,首先会调用 checkThread 方法检查当前所在的线程,之后便会调用 scheduleTraversals 方法调度一次绘制的流程。

scheduleTraversals 首先在 MessageQueue 中放入了一个同步屏障的 Message,之后调用 mChoreographer::postCallback 注册了 VSync 信号的监听器,使得 mTraversalRunnable 在下一次 VSync 信号到来时会调用到 mTracersalRunnable。

doTraversal

当 mTraversalRunnable 执行时会调用 doTraversal 方法,它首先移除了这个同步消息屏障,之后调用了 performTraversals 方法。

performTraversals

performTraversals 中,首先将 RunQueue 这一个 Runnable 队列进行执行,之后分别通过 performMeasureperformLayoutperformDraw 方法分别进行了 View 的三大阶段——Measure、Layout、Draw。

相关问题

这时候就有一道十分经典的面试题了:

问:我们在 onResume 中分别尝试用 View.postHandler.post 来尝试获取 View 的宽度和高度,为何 View.post 能获取到对应宽高,而 Handler.post 获取不到?

答:因为在 handleResumeActivity 中先执行了 performResumeActivity,再执行了 WindowManager.addView,在 addView 的过程中才进行了 View 的 Measure 等过程,而 View 的绘制过程是通过 Handler 来分发的,也就是说如果我们在 onResume 中 post 一条消息到 MessageQueue 中,它肯定是比 TraversalRunnable 先执行的。而在 View.post 的实现中,若调用的时机比 View 的绘制流程更慢,则它实际上是会将这个获取宽高的 Runnable 先放入了一个 RunQueue 中,而就像前面提到的,RunQueue 中的 Runnable 的执行是在 TraversalsRunnable 之后的。

(其实这里 RunQueue 调用方法并不是马上执行,而是 post 到了 Handler 中,因此要等到 TraversalsRunnable 执行结束后才会执行)

Measure

Measure 阶段主要是为了计算出控件树各个控件若需要显示内容需要的尺寸。它的起点在 ViewRootImpl::measureHierarchy,它的两个参数 desiredWindowWidth 及 desiredWindowHeight 分别代表了窗口的宽高,之后通过 getRootMeasureSpect 获取到了根 MeasureSpec,也就代表了对 DecorView 限制的宽高信息。之后,调用了 performMeasure 方法并传入了对应的 MeasureSpec。

MeasureSpec 由 2 + 32 的形式组成,前两位是代表 SpecMode,也就是测量模式,而后两位代表了 SpecSize,也就是测量的尺寸。

SpecMode 共有下列三种:

  • EXACTLY:对子 View 指定一个确切的建议尺寸 SpecSize
  • AT_MOST:子 View 大小不能超过 SpecSize
  • UNSPECIFIED:对子 View 尺寸不作限制,通常用于系统内部

performMeasure 中最后调用到了 mView(DecorView) 的 measure 方法,里面首先尝试从缓存中取出,若缓存中无法找到则调用 onMeasure 重新测量。

ViewGroup::onMeasure

而我们都知道 DecorView 是一个 FrameLayout,所以之后调用到了 FrameLayout::onMeasure 方法。它首先遍历了所有子 View,若其未被 measure 过或是其状态不为 GONE,则会调用measureChildWithMargins 方法对该子 View 进行 measure。并且与 maxWidth 及 maxHeight 分别比较从而在遍历完成后得到所有子 View 的最大宽度及最大高度。同时用 childState 整合每个 child 的 State。

之后在将 maxWidth、maxHeight 与建议的最小宽高及 foreground 的宽高比较之后,调用了 resolveSizeAndState 方法得到了最终的 measuredWidth / measuredHeight,并调用 setMeasuredDimension 方法进行设置。

可以看出, ViewGroup 需要先对子 View 进行测量,再进行自身的测量。

而在 measureChildWithMargins 方法中,通过传入自身的 MeasureSpec 以及自己的 Padding 与子 View 的 Margin 的和,经过 getChildMeasureSpec 方法计算后得到了对子 View 限制后的子 MeasureSpec。最后调用了 child::measure 对子 View 进行测量。

在 getChildMeasureSpec 中,它根据自身的 MeasureMode 及子 View 的 LayoutParams 中的值对子 View 的大小和测量模式进行了不同的指定。

一般来说,当子 View 的大小在当且阶段就能确定时,就可以将其 MeasureSpec 设置为 EXACTLY,而当其大小在此时无法确定时,就可以先将其设为 AT_MOST。

而在 resolveSizeAndState 中,若测量模式为 AT_MOST,且计算出来的尺寸大于父 View 给定的最大尺寸,则在测量结果中加上 MEASURED_STATE_TOO_SMALL,否则将测量结果直接设置为传入的大小。若测量模式为 EXACTLY,则说明父 View 有指定的大小,则不再考虑 size,直接使用父 View 指定的 specSize。

View::onMeasure

而对于普通的 View,它们只需要完成自身的测量工作,若不重写 onMeasure,它的默认实现直接通过 getDefaultSize 获取了默认的 size,并设置进 setMeasuredDimension 方法。

而 getDefaultSize 中对 AT_MOST 及 EXACTLY 的处理都相同,直接返回了父 View 的 specSize。也就是说普通的 View 对 match_parent (EXACTLY)及 wrap_content (AT_MOST)的处理方式都相同。因此如果我们重写 View 最好对 wrap_content 进行特殊的处理,否则效果会和 match_parent 效果相同。

Layout

Layout 的起点是 DecorView 的 layout 方法,它实际上调用的是 View::layout 方法,它调用了 setFrame 方法设置了 View 的 mLeft、mTop、mRight、mBottom,它们分别表示了其相对与父 View 的位置。其内部还会对 View 的位置是否发生改变进行判断,若发生了改变则会调用 onLayout 对其重新布局。

ViewGroup::onLayout

继续看到 DecorView(FrameLayout)对 onLayout 的实现,它最终调用了 layoutChildren 方法。

layoutChildren 方法中,在 child 不为 GONE 的情况下才会对它布局,这里主要是根据 Gravity 计算出了子 View 的 left、top、right、bottom,之后通过 child.layout 对子 View 进行 layout。

View::onLayout

由于普通 View 不包含子 View,所以其默认实现为空。

Draw

我们仍然以 DecorView 的 layout 方法开始看(其实就是 View::draw)。其实 Draw 操作都是在通过对 canvas 操作进行绘制,首先它通过 drawBackground 方法对 View 的 background 进行绘制,之后调用 onDraw 对自身的界面进行绘制。之后通过 dispatchDraw 方法对子 View 进行绘制。最后调用 onDrawForeground 对 foreground 进行绘制。

由于 FrameLayout 是一个容器 View,所以其 onDraw 是空实现的,而 dispatchDraw 方法则会遍历其子 View,调用了子 View 的 draw 方法对子任务的绘制事件进行分发。

相关问题

问:View 的 INVISIBLE 与 GONE 有什么区别?

答:其实 View 的 Visibility 有三种:VISIBLE、INVISIBLE、GONE。按道理来说 INVISIBLE、GONE 都是不可见,但在 Measure、Layout 阶段都仅仅是对 GONE 的 View 不做处理。因此 INVISIBLE 和 GONE 的区别是:虽然它们都是不可见的,但 INVISIBLE 的 View 仍然会被用于 Measure 及 Layout,仍会占用父 View 中的体积


Android Developer in GDUT