Skip to content

View 体系:ViewRootImpl 与四大流程的调度真相 ​

本页导读:measure/layout/draw 人人会背,但「它们什么时候被触发、为什么一次 measure 会跑三遍、onResume 之后画面去哪了」才是 Framework 层的考点。本页以 ViewRootImpl 为轴心讲调度、遍历与渲染演进,自定义 View 与硬件层收尾。前置:Activity、架构篇图形栈页。

一、谁是轴心:ViewRootImpl 不是 View ​

  • ViewRootImpl 是视图树的宿主与总调度:它持有 DecorView 引用、持有与 WMS 的会话(IWindowSession/内部类 W)、也持有 Surface(现代为 BLASTBufferQueue 通道)。
  • 它在 WindowManagerGlobal.addView → new ViewRootImpl → setView() 时诞生——即 Activity handleResumeActivity 之后(见 Activity 页第六节);每个窗口一个 ViewRootImpl,Dialog/PopupWindow 各有自己的。
  • 三大职责:接收输入事件(InputEventReceiver 挂在它身上)、与 WMS 同步窗口属性(relayout)、驱动「测量-布局-绘制」的帧循环(对接 Choreographer,见动画页)。
bash
adb shell dumpsys activity top | head -30   # 顶层窗口与视图树概况
adb shell dumpsys gfxinfo com.example        # 帧统计:janky frames 比例
adb shell dumpsys SurfaceFlinger --dump-disp 0 | head   # 系统合成侧的窗口层状态(以版本为准)

二、遍历从哪来:scheduleTraversals 与 vsync ​

四大流程不会自己发生,入口只有两个:

  • invalidate():标脏,沿父链上溯到 ViewRootImpl,触发 scheduleTraversals();
  • requestLayout():置布局脏位沿父链上溯,同样落到 scheduleTraversals()。
java
// frameworks/base/core/java/android/view/ViewRootImpl.java(节选大意)
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        // 同步屏障:保证遍历优先于普通消息被处理(机制见架构篇 Handler 页)
        mTraversalBarrier = mHandler.getLooper().pushSyncBarrier();
        // 关键:注册 vsync 回调,帧对齐由 Choreographer 统一发令
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    }
}

要点三条:

  1. 一次调度 = 一帧最多一次遍历:同帧内十次 invalidate 只合并成一次 traversal;
  2. 同步屏障:遍历类回调抢在普通消息前执行;
  3. doTraversal() 内部顺序固定:performMeasure()(若布局脏)→ performLayout() → performDraw()——一棵树一次深度遍历,不是三棵树各走一遍。
text
一帧 doTraversal 的骨架(示意):
performTraversals()
 ├ relayout:与 WMS 对表(尺寸/可见性/insets)
 ├ performMeasure()   # 布局脏才跑
 ├ performLayout()    # 尺寸变化或布局脏才跑
 ├ performDraw()      # 存在脏区或首帧
 └ 提交帧;若又产生新脏 → scheduleTraversals() 预约下一帧
  • 屏障解除点:doTraversal() 执行完毕后 pop;期间普通消息全部排队,这就是「遍历优先级高于业务 handler 消息」的实现。

三、measure:MeasureSpec 与「尺寸是谈出来的」 ​

  • MeasureSpec = mode + size 打包成一个 int(UNSPECIFIED/EXACTLY/AT_MOST),父容器按自身约束与子 View 的 layout_width/height 换算下发。
  • 遍历自上而下传规格、自下而上报结果:onMeasure() 里 setMeasuredDimension() 是唯一合法上报点。
  • 昂贵点:LinearLayout 加权子 View 会二次测量;ScrollView 对子 View 的 UNSPECIFIED 高度常触发整棵子树重测。经验法则:一次遍历里同一 View 被 measure 两次以上,就值得优化。

自定义 View 的 measure 三问:

  • wrap_content 时给多大?(getDefaultSize 兜底语义要背)
  • padding 算没算进「内容尺寸」?
  • 给孩子的 spec 真需要 EXACTLY 吗?(强摊父尺寸 = 废掉协商)
kotlin
// 协商式 onMeasure 骨架
override fun onMeasure(wSpec: Int, hSpec: Int) {
    val wMode = MeasureSpec.getMode(wSpec)
    val target = if (wMode == MeasureSpec.EXACTLY) MeasureSpec.getSize(wSpec)
    else (contentWidth + paddingLeft + paddingRight)
        .coerceAtMost(MeasureSpec.getSize(wSpec))   // 自报上限
    setMeasuredDimension(target, suggestedHeight)
    // 下发给孩子 AT_MOST,而不是强行 EXACTLY
}

四、layout 与 draw:摆放、录制、上屏 ​

  • onLayout 只在整个布局阶段发生一次,职责是把 measure 出的尺寸摆进父给的 frame;layout() 自带 changed 判断避免无谓重摆。
  • 绘制阶段的演进:
text
经典五步(legacy draw,软件渲染时代):
① 背景 → ② 自身 onDraw → ③ dispatchDraw 子 View → ④ 缓存/边缘效果 → ⑤ 装饰件
现代硬件路径:
onDraw 不再直接画像素,而是把绘制指令录进 RenderNode 的 DisplayList;
RenderThread 在帧尾统一提交 GPU,SurfaceFlinger 合成上屏
  • 硬件渲染演进:Android 3.0 HWUI + OpenGL DisplayList → 4.1 Project Butter(vsync 对齐与帧节奏统一)→ 5.0 RenderThread 异步渲染成熟 → Android 12+ 全面 RenderNode 化,android.graphics.RenderNode 自 API 31 公开、RenderEffect 提供硬件效果链;LAYER_TYPE_SOFTWARE 只剩兼容价值。

  • 脏区语义变化:硬件路径以「重录 RenderNode」为粒度,旧脏矩形优化的收益变小——少重录才是新正解。

  • outline/elevation 阴影同样记在 RenderNode 上:View.setOutlineProvider 给形状,投影由渲染端合成,圆角裁剪在硬件层是 outline clip,不是 onDraw 里画。

  • DisplayList 有复用:未失效的 RenderNode 直接重放,这正是「改一个子 View 不会重录整棵树」的依据。

五、自定义 View 实战军规 ​

场景必须做常见事故
自绘 ViewonDraw 零分配(Paint/Rect 成员化)每帧 new 对象 → GC 抖动掉帧
复合布局onMeasure 缓存子测量结果layout 里调 measure 触发二次遍历
可滚动内容用 ViewConfiguration 取 slop/velocity 阈值硬编码 dp 阈值,换设备手感崩
事件参与只覆写需要的 dispatch/onTouchEvent 层三层全覆写互相打架
无障碍补 onInitializeAccessibilityNodeInfo 语义TalkBack 完全听不懂你的图表
复用容器控制 item 布局深度与 measure 次数RecyclerView 滑动 measure 风暴

军规的共同点:把成本从每帧挪到一次性——测量缓存、Paint 预建、阈值查配置。

kotlin
class ChartView(ctx: Context) : View(ctx) {
    private val line = Paint().apply {          // Paint 成员化:onDraw 零分配
        isAntiAlias = true
        strokeWidth = resources.displayMetrics.density
        color = Color.DKGRAY
    }
    override fun onDraw(canvas: Canvas) {
        val y = height / 2f
        canvas.drawLine(0f, y, width.toFloat(), y, line)
    }
}

六、RenderNode 与硬件层:把变换动画变成零成本 ​

  • 每个 View 自带 RenderNode;translationX/Y、scaleX/Y、alpha、rotation 属于渲染属性,改它们只更新 RenderNode 的变换矩阵,主线程不重录 DisplayList,RenderThread 直接复用——这是属性动画丝滑的物理基础(动画页展开)。

  • setLayerType(LAYER_TYPE_HARDWARE) 把子树固化成纹理:适合「动画期间临时冻结 + 高频失效」组合,动画结束立刻关回去(显存要还)。

  • 进阶(API 31+):android.graphics.RenderNode 可直接挂进 Canvas 自定义录制、RenderEffect 链做硬件模糊/位移——过去私有 API 玩法的官方正名。

  • 内存视角:硬件层纹理按视图尺寸占显存,大屏 View 整层固化可能一次吃掉几十 MB——LAYER_TYPE_NONE 才是默认正确答案。

七、帧与内存:Surface 从哪来 ​

窗口生命线的最后一环,把「绘制」接上「显示」:

text
setView() → relayout(WindowState) → WMS 决定尺寸/可见性/层序
 └ 应用侧 Surface(BLASTBufferQueue,Android 12+ 默认通道)
    └ 每帧:RenderThread 出队 buffer → GPU 渲染 → 入队 → SurfaceFlinger latch 合成
WMS 改尺寸(分屏拖拽、旋转)→ relayout 回来 → 整树重 measure/layout/draw
  • 首帧优化的一切手段都作用在这条线上:启动页提前上屏(WMS 画)、Baseline Profile 缩 JIT 时间、首屏骨架降级——本质都是让第一次 latch 更早发生。
  • 这条线也解释三类现象:分屏拖拽重绘卡顿(每帧 relayout)、后台窗口自动省帧(WMS 标记不可见,遍历直接跳过)、SurfaceView 的「视频洞」(独立 Surface 独立合成层)。

八、常见误区问答 ​

Q1:View.post(Runnable) 到底何时执行? attach 前进 RunQueue,attach(拥有 ViewRootImpl)后转入正常 handler;它不保证「布局已完成」,要拿尺寸请挂 doOnPreDraw 一类 preDraw 监听。

Q2:requestLayout 和 invalidate 选哪个? 尺寸/位置变了要重摆 → requestLayout(重走 measure+layout+draw);只换外观 → invalidate(重录受影响 RenderNode)。别在 onDraw 里 requestLayout——自造遍历循环。

Q3:为什么 getLocationInWindow 返回全 0? 在 layout 完成前调用了。挪到布局之后的时机(onWindowFocus 后、或一次性 OnGlobalLayout 监听,用完即摘)。

Q4:overdraw 还值得优化吗? HWUI 下过度绘制主要转化为带宽与 GPU 成本,低端机仍敏感;开发者选项「显示 GPU 视图更新」或 Profiler 过度绘制视图定位红区,优先砍全屏不透明背景与不可见子树。

版本注记:基于 Android 16 / API 36;Android 3 差异:无硬件加速,legacy 五步 draw;Android 5 差异:RenderThread 模型成型;Android 12 差异:BLAST 缓冲通道 + RenderNode/RenderEffect 公开(API 31);Android 15 差异:RenderEffect 特效族扩充(模糊等)。

Q5:为什么我把绘制放在子线程? Canvas/RenderNode 不是线程安全的:自绘只认主线程(或 SurfaceView 的独立 Surface + 自有锁模型,见架构篇图形栈页)。

顺带一个冷知识:View.invalidate(int l,int t,int r,int b) 的区域参数在硬件路径上主要用于「是否值得重录」的判定,不是像素级裁剪开关。

本页小结:ViewRootImpl 是 View 体系的总机——invalidate/requestLayout 经同步屏障汇入 Choreographer 的 vsync,一帧一次 measure/layout/draw 树遍历;硬件渲染时代 onDraw 是「录 DisplayList」,RenderNode 让变换属性动画零重录;自定义 View 的功夫全在 measure 谈判、onDraw 零分配、事件分层三件事上。

参考链接 ​

基于 Android 16 / API 36 · 内容为原创整理,转载请注明出处