View 的绘制、刷新和合成横跨应用层、框架层、Native 层三套机制,任何一环理解错都会表现为掉帧。本文按一条阅读线串起来:先搞清楚一帧怎么画出来(ViewRootImpl + 绘制/刷新机制),再理解为什么掉帧(渲染机制),然后看 Surface 家族怎么绕开主线程,最后回到 View 的滑动实现。


一、ViewRootImpl 与绘制/刷新机制:measure/layout/draw 由谁驱动

ViewRootImpl 是 View 树与 WindowManager 的桥梁。setView 后持有 DecorView,负责 requestLayoutperformTraversals(也就是 measure / layout / draw),再经 Surface 交合成。IWindowSession 把窗口属性同步给 WMS,输入事件从 InputStage 链进 View 树。

绘制分三步,由 Choreographer 的 VSYNC 回调驱动:measure 决定宽高,layout 决定位置,draw 把内容画到硬件缓冲。MeasureSpec 是父布局给的约束:EXACTLYAT_MOSTUNSPECIFIED 决定 wrapmatch 怎么算。刷新不是「调用即重绘」:Choreographer 驱动帧回调,TraversalRunnable 把三次遍历绑在同一 VSYNC 上;invalidate 只标脏,真正绘制在下一帧。requestLayout 重走测量和布局,invalidate 通常只重绘——这就是「改尺寸 requestLayout、改颜色 invalidate 就够」的原因。

三个容易踩的坑

  • 子线程直接改 ViewViewRootImpl 会检查线程,非创建线程改 UI 直接抛异常;Dialog / Popup 也各有自己的 ViewRootImpl,别假设全局只有一棵树。
  • invalidate 不等于重新排版:它只脏当前显示列表;改 margin 或孩子数量必须 requestLayout
  • 频繁 requestLayout / 在 draw 里分配对象:前者重走 measure/layout,后者触发 GC 抖动,都会掉帧。

什么时候用:自定义 View、卡顿排查和动画占位出问题时回到这套流程;普通业务改颜色用 invalidate 即可,分析掉帧用 Profile GPU 或 Perfetto。


二、渲染机制与掉帧排查:RenderThread、16ms 预算与优化

渲染从 View 树的 measure / layout / draw,到 RenderThreadDisplayList 交给 GPU。硬件加速后很多绘制变成 OpenGL / Vulkan 命令——理解这一点才能解释为什么某些 setLayerType 会变快或更慢。16ms 一帧是 60Hz 的预算,过度绘制、过深层级和主线程耗时都会掉帧。

优化先开 GPU 呈现模式看颜色,再压平 ConstraintLayout,减少 clip 和透明;列表用 RecyclerView 复用,动画放硬件层要有节制,Systrace 看帧边界。把测量和绘制从主线程长任务里拆开,比换旗舰机更能稳定帧率。

四个容易踩的坑

  • 过度绘制:重叠的半透明/全屏背景反复绘制,GPU 呈现模式里一片红。
  • 层级过深:measure/layout 随嵌套深度膨胀,优先压平 ConstraintLayout
  • 软件层和硬件层混用:会打断渲染管线,能硬件加速就不要强制软件层。
  • 主线程长任务阻塞绘制:把测量和绘制从主线程长任务里拆开,比换机更管用。

什么时候用:卡顿、列表滑动掉帧、动画不跟手时回到渲染机制排查;纯业务逻辑问题别在这里找原因。


三、Surface 家族:SurfaceView 与 TextureView 怎么选

SurfaceView 拥有独立的 Surface,由 SurfaceFlinger 单独合成,不走普通 View 的绘制路径。游戏、相机、视频解码这类高帧率场景更适合它,因为可以绕开主线程绘制并尝试硬件 overlay。经典用法是实现 SurfaceHolder.Callback,在 surfaceCreated 启动渲染线程、surfaceDestroyed 立即停止。它默认位于窗口最底层,需要 setZOrderMediaOverlaysetZOrderOnTop 才能盖住其它控件;因为不在 View 树里,旋转、透明度动画会比较别扭。从 API 24 起支持 setChildWindow 嵌入,但仍要小心生命周期与 Activity 重建;能 overlay 时尽量让格式匹配显示设备,减少 GLES 合成。

TextureView 把内容画进 View 树里的硬件纹理,因此能做动画、圆角和透明度,这是它相对 SurfaceView 的优势。相机预览、视频播放需要和其它 UI 叠加时,常常选 TextureView。代价是它走 GPU 合成,功耗和延迟通常高于 SurfaceView 的 overlay。使用时要等 isAvailable 再绑定 SurfaceTexture,并在 onSurfaceTextureDestroyed 归还资源;频繁 resize 会触发纹理重建,容易闪一下。Android 7 之后若只需简单叠加,也可以看 SurfaceViewsetChildSurfacePackage

选型原则:要参与 View 动画就用 TextureView,要省电、低延迟就优先 SurfaceView

三个容易踩的坑

  • 要省电低延迟却选了 TextureView:它走 GPU 合成,功耗和延迟通常高于 SurfaceView 的 overlay。
  • SurfaceView 盖不住控件:默认最底层,忘了 setZOrderMediaOverlay / setZOrderOnTop 就被埋了。
  • TextureView 频繁 resize 闪一下:纹理重建导致,绑定/归还资源(isAvailable / onSurfaceTextureDestroyed)要配对。

什么时候用:游戏、相机、视频解码、投屏这类高帧率或需叠加 UI 的场景二选一;纯普通控件不需要碰 Surface 家族。


四、View 的滑动实现:六种方式及其适用边界

View 的滑动有六种常见方式,差异在于「动的是 View 还是内容」:

  • a. layout(left, top, right, bottom):修改 View 四个方向的属性值来改坐标,从而滑动 View 本身。
  • b. offsetLeftAndRight() / offsetTopAndBottom():指定偏移量滑动 View,比重新 layout 更轻。
  • c. 改 LayoutParams:布局参数里保存了 View 的布局信息,通过修改布局参数(如 margin)来滑动 View。
  • d. 动画移动:补间平移动画不能改变 View 的位置参数,属性动画才可以真正改位置。
  • e. scrollTo / scrollBy:移动的是 View 的内容scrollBy(50, 50) 你会看到屏幕上的内容向屏幕左上角移动——参考对象不同导致,可看作移动的是手机屏幕,屏幕向右下角移动,内容就像左上角移动了。
  • f. Scroller:需配合 computeScroll 实现滑动,它本身不会滑动 View,作用像插值器——计算当前时间点该滑到的距离,View 不断重绘、不断调用 computeScroll(空方法需重写),从中获取当前位置并调 scrollTo 实现滑动。

三个容易踩的坑

  • 以为补间平移动画改了位置:补间动画只改绘制位置不改布局参数,结束会「瞬移」回原处——要真改位置用属性动画。
  • 混淆 scrollTo 的参考系:它移的是内容不是 View,方向直觉相反,容易滑反。
  • Scroller 不重写 computeScroll 就没效果:Scroller 只是插值器,滑动动作必须自己写在 computeScroll 里。

什么时候用:跟手拖动用 scrollTo/scrollBy + Scroller;动画改位置用属性动画;简单位移改 LayoutParamsoffset* 即可。


附:速查表

场景 用什么 不要用什么
改颜色 / 局部重绘 invalidate 别用 requestLayout(会重走 measure/layout)
改尺寸 / 增删子 View requestLayout 别只 invalidate(不重新排版)
高帧率 / 省电低延迟(游戏、相机、视频) SurfaceView(overlay 合成) 别用 TextureView(GPU 合成更耗)
需要动画、圆角、透明度叠加 TextureView 别用 SurfaceView(不在 View 树,动画别扭)
真正改变 View 位置 属性动画 / 改 LayoutParams 别用补间平移动画(不改位置参数)
跟手滚动 Scroller + computeScroll + scrollTo 别在 draw 里分配对象(GC 抖动掉帧)
卡顿排查 Systrace / GPU 呈现模式 / Perfetto 别只靠「换旗舰机」(先拆主线程长任务)
子线程改 UI 切回主线程再改 别在子线程直接改(ViewRootImpl 查线程会崩)