Appearance
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()时诞生——即 ActivityhandleResumeActivity之后(见 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);
}
}要点三条:
- 一次调度 = 一帧最多一次遍历:同帧内十次 invalidate 只合并成一次 traversal;
- 同步屏障:遍历类回调抢在普通消息前执行;
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 实战军规
| 场景 | 必须做 | 常见事故 |
|---|---|---|
| 自绘 View | onDraw 零分配(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 零分配、事件分层三件事上。
参考链接
- https://developer.android.com/guide/topics/ui/custom-views
- https://developer.android.com/reference/android/graphics/RenderNode
- https://developer.android.com/topic/performance
- https://developer.android.com/training/smooth-animations
- https://developer.android.com/guide/topics/ui/more-controls(自定义控件行为参考)
- https://cs.android.com/android/platform/frameworks/base/+/main:core/java/android/view/ViewRootImpl.java
- https://cs.android.com/android/platform/frameworks/base/+/main:core/java/android/view/View.java
- 站内:Activity、事件分发、动画、架构篇 图形栈