整理自模拟面试,覆盖自定义 View / invalidate / requestLayout / ViewRootImpl / SurfaceView / 同步屏障
一句话:先确定继承层级,再处理属性初始化,然后完成测量、布局、绘制、事件和状态保存。
ViewFrameLayout / LinearLayout 等自己摆放子 View:继承 ViewGroup
初始化
obtainStyledAttributes初始化 Paint、Path、Rect、手势检测器等
测量
onMeasure()MeasureSpec.EXACTLY / AT_MOST / UNSPECIFIED调用 setMeasuredDimension()
布局
ViewGroup 需要重写 onLayout()普通 View 一般不需要自己处理布局
绘制
onDraw() 中绘制内容ViewGroup 还会涉及 dispatchDraw()
交互
onTouchEvent()必要时处理滑动冲突:requestDisallowInterceptTouchEvent(true)
刷新与状态保存
invalidate()requestLayout()onSaveInstanceState() / onRestoreInstanceState()wrap_content 失效,多半是 onMeasure() 没处理好onDraw() 里不要频繁创建对象,避免掉帧和 GCinvalidate() 和 requestLayout() 的区别核心区别:
- invalidate():请求重绘,主要触发 draw
- requestLayout():请求重新测量和布局,通常会触发 measure + layout + draw
| 方法 | 适用场景 | 典型结果 |
|---|---|---|
invalidate() |
内容变了,但尺寸位置没变 | 重新走 onDraw() |
requestLayout() |
尺寸、位置、布局规则变了 | 重新走 onMeasure() / onLayout(),通常也会重绘 |
invalidate()requestLayout()invalidate():重画,不重新摆requestLayout():重新摆,通常也会重画一句话:因为 Android 的 View 体系不是线程安全的,UI 操作默认必须收口到主线程。
多线程同时修改容易状态错乱
ViewRootImpl 只认创建它的线程
ViewRootImplOnly the original thread that created a view hierarchy can touch its views.
Looper + MessageQueueSurfaceView 为什么看起来能在子线程绘制一句话:因为 SurfaceView 的显示内容不直接画在普通 View 树的 Canvas 上,而是画到独立的 Surface 上。
SurfaceView受主线程布局系统管理
内层 Surface
lockCanvas() / unlockCanvasAndPost() 进行绘制layoutParams、visibility、位置尺寸变化仍然要遵守主线程规则measure/layout/drawSurfaceView:布局归主线程,内容绘制可走独立 Surfaceinvalidate()尺寸变了却只调了 invalidate(),没有 requestLayout()
子线程更新 UI
未切回主线程,刷新链路不会按预期执行
时机不对
持有的是旧 View 引用
实际不可见
GONE / INVISIBLE被遮挡、透明度为 0、被裁剪、移出屏幕
被后续流程覆盖
RecyclerView 复用重新 bind新状态把旧状态覆盖
自定义 View 实现问题
onDraw() 没用到更新后的字段onMeasure() 尺寸写死setWillNotDraw(true) 导致不绘制invalidate() 还是 requestLayout()onDraw() / onMeasure() 和外部覆盖逻辑invalidate() 还会重绘吗结论:一般不会立即产生一次对用户可见的重绘。
invalidate() 只是标记“需要重绘”ViewRootImpl 在后续有效帧里安排 traversalinvalidate() 可能被合并postInvalidate() 本质上也只是投递重绘请求,不保证息屏时立刻画出来结论:会。invalidate() 只是请求,不保证最后一定有一次有效绘制。
Activity 已 onStop / onDestroyFragment 已 onDestroyViewView 已 detach
窗口不可见
页面被覆盖
异步回调回来时对象已经不是当前界面
bindingRecyclerView item 已复用invalidate() 不能丢”ViewModel / 状态流 / 持久化数据源isAttachedToWindow、isAdded、viewLifecycleOwner一句话:invalidate() / requestLayout() 最终都会把刷新请求交给 ViewRootImpl,由它通过 Choreographer 在下一帧触发 performTraversals(),统一执行 measure、layout、draw。
invalidate():请求重绘requestLayout():请求重新布局
请求汇总到 ViewRootImpl
invalidateChildInParent()scheduleTraversals()
scheduleTraversals() 安排遍历
mTraversalScheduled = true通过 Choreographer.postCallback() 注册 traversal callback
下一帧执行 doTraversal()
调用 performTraversals()
performTraversals() 执行三大流程
performMeasure()performLayout()performDraw()
进入实际绘制
draw()onDraw() / dispatchDraw()View.invalidate()
-> ViewParent.invalidateChild()
-> ViewRootImpl.invalidateChildInParent()
-> ViewRootImpl.scheduleTraversals()
-> Choreographer.postCallback()
-> 下一帧 ViewRootImpl.doTraversal()
-> ViewRootImpl.performTraversals()
-> performDraw()
-> View.draw() / onDraw() / dispatchDraw()
invalidate() 不是立刻 onDraw()ViewRootImpl 会把多次刷新请求合并到同一帧处理一句话:同步屏障是 MessageQueue 里的一种特殊消息,用来拦住普通同步消息,让异步消息优先执行。
ViewRootImpl.scheduleTraversals() 会插入同步屏障Choreographer 投递异步的 traversal 回调MessageQueue 中本质上是一个 target == null 的特殊消息节点| 知识点 | 速记 |
|---|---|
| 自定义 View 流程 | 初始化属性,再处理测量、布局、绘制、事件、状态 |
invalidate() |
只请求重绘,不重新测量布局 |
requestLayout() |
请求重新测量布局,通常也会重绘 |
| View 线程模型 | View 体系非线程安全,UI 更新必须主线程收口 |
SurfaceView |
View 外壳在主线程,独立 Surface 内容可子线程绘制 |
| 更新无效 | 可能是刷新方法错、线程错、时机错、不可见或被覆盖 |
| 息屏刷新 | invalidate() 只是请求,息屏时通常不会立即可见重绘 |
| 生命周期影响 | 可能丢的是这次刷新机会,真正不能丢的是最新状态 |
| ViewRootImpl | 统一调度 measure/layout/draw,核心入口是 performTraversals() |
| 同步屏障 | 挡同步消息,让异步渲染消息优先执行 |