第13章 View体系与绘制流程
各位同学,今天我们来聊聊Android里最核心、也最“接地气”的一块——View的绘制流程。说实话,我当年刚入行时,觉得写个XML布局,Activity里setContentView一下,界面就出来了,没什么好研究的。直到有一次,我接手了一个列表滑动卡顿的项目,才真正意识到:不懂绘制流程,你连性能优化的门都摸不着。
这一章,我会带你从View的树结构开始,一步步拆解Measure、Layout、Draw这三大流程,再深入到ViewRootImpl和Choreographer的协作机制,最后聊聊硬件加速渲染。嗯,内容不少,但都是干货。
13.1 View树结构:从根到叶的层级体系
Android的界面,本质上是一棵树。这棵树的根节点是DecorView,叶子节点是各种TextView、ImageView之类的。中间节点则是各种ViewGroup,比如LinearLayout、FrameLayout。
你想想看,为什么是树结构?因为只有树才能高效地做递归遍历。Measure、Layout、Draw这三个流程,全是递归下去的。父节点测量完,才能测量子节点;父节点布局完,才能布局子节点。这个顺序,一点都不能乱。
核心要点:View树的结构决定了绘制流程的递归特性。每个ViewGroup都负责管理自己的子View,子View又可以继续嵌套。这种设计,说白了就是“分而治之”。
我在项目中遇到过一个问题:一个页面嵌套了七八层LinearLayout,结果在低端机上渲染掉帧严重。后来我用了ConstraintLayout,把层级压到了两层,问题就解决了。所以,保持View树扁平,是性能优化的第一课。
13.2 Measure流程:测量每个View的尺寸
Measure是三大流程的第一步。它的目标很明确:确定每个View的宽和高。
这个过程从ViewRootImpl开始,调用performTraversals(),然后触发performMeasure()。最终,每个View的onMeasure()方法会被调用。
这里有个关键参数:MeasureSpec。它由两部分组成——size和mode。mode有三种:
- UNSPECIFIED:父容器不对子View做任何限制,子View想要多大就多大。ScrollView里常用。
- EXACTLY:父容器已经确定了子View的精确尺寸,比如match_parent或固定dp值。
- AT_MOST:父容器给了一个最大尺寸,子View不能超过这个值,比如wrap_content。
我个人习惯在自定义View时,先判断MeasureSpec的mode,再决定怎么处理。比如实现一个正方形ImageView:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
// 取宽高中较小的那个,保证正方形
int size = Math.min(width, height);
setMeasuredDimension(size, size);
}
避坑指南:我曾经在自定义View时忘了调用setMeasuredDimension(),结果应用直接崩溃。记住,onMeasure方法的最后,必须调用这个方法,否则系统不知道你的View到底有多大。
13.3 Layout流程:确定每个View的位置
Measure测量完尺寸,接下来就是Layout。这一步决定每个View放在哪里,也就是确定它的left、top、right、bottom四个坐标。
Layout流程从performLayout()开始,最终调用每个View的onLayout()方法。对于ViewGroup来说,你需要在这里遍历所有子View,调用它们的layout()方法。
举个例子,一个简单的自定义ViewGroup,实现水平排列:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
int childWidth = child.getMeasuredWidth();
child.layout(childLeft, 0, childLeft + childWidth, child.getMeasuredHeight());
childLeft += childWidth;
}
}
}
注意,这里用的是getMeasuredWidth(),而不是getWidth()。为什么?因为Measure阶段已经确定了尺寸,Layout阶段只是把位置定下来。如果你在Layout里改了尺寸,那Measure就白做了。
13.4 Draw流程:把View画到屏幕上
Draw流程,说白了就是把View的内容画到Canvas上。这个过程从performDraw()开始,最终调用onDraw()。
绘制顺序是这样的:
- 绘制背景(background)
- 绘制自身内容(onDraw)
- 绘制子View(dispatchDraw)
- 绘制装饰(foreground、scrollbars等)
这个顺序很重要。如果你在onDraw()里画了一个圆,然后在dispatchDraw()里画了一个矩形,那矩形会盖在圆上面。嗯,这就是所谓的“画家算法”。
注意:不要在onDraw()里创建对象。因为绘制流程每16ms就会触发一次,频繁创建对象会导致GC频繁,引发掉帧。我见过一个同事在onDraw里new了一个Paint对象,结果列表滑动时卡成PPT。
13.5 ViewRootImpl与Choreographer:绘制流程的调度者
ViewRootImpl是View树和WindowManager之间的桥梁。它负责接收系统的事件(比如触摸、按键),然后触发View树的遍历流程。
而Choreographer,是Android 4.1引入的“节拍器”。它的作用很简单:保证绘制流程和屏幕刷新率同步。屏幕每16ms刷新一次,Choreographer就在每次刷新前通知ViewRootImpl去执行绘制。
为什么会需要Choreographer?因为如果没有它,你可能会在屏幕刷新的中间时刻去更新UI,导致画面撕裂。Choreographer通过postFrameCallback机制,确保所有UI更新都在垂直同步信号到来之前完成。
我记得有一次排查一个掉帧问题,发现是某个第三方SDK在子线程里频繁调用了requestLayout(),导致Choreographer的帧回调队列被塞满。嗯,从那以后,我对requestLayout()的调用就格外小心了。
13.6 硬件加速渲染:从CPU到GPU的进化
Android从3.0开始引入硬件加速,从4.0开始默认开启。说白了,就是把绘制工作从CPU转移到GPU。
在软件渲染模式下,每次绘制都是CPU把像素数据算好,然后交给屏幕。而硬件加速模式下,GPU直接处理OpenGL指令,效率高得多。
但硬件加速也不是万能的。有些API在硬件加速下不支持,比如Canvas.clipPath()。如果你在onDraw()里用了这些API,系统会回退到软件渲染,性能反而下降。
我的建议:在自定义View时,尽量使用硬件加速支持的API。比如用drawRoundRect()代替clipPath()来实现圆角。如果你不确定某个API是否支持,可以在开发者选项里开启“强制进行GPU渲染”来测试。
13.7 知识体系总览
下面这张图,是我梳理的View体系与绘制流程的核心逻辑。你可以把它当作本章的“地图”。
从这张图你可以看到,整个绘制流程是一条清晰的链路:ViewRootImpl发起请求,Choreographer控制节奏,三大流程依次执行,最终作用到View树上。而硬件加速,则是底层渲染方式的优化。
好了,这一章的内容就到这里。View体系是Android Framework的基石,理解了它,你才能写出流畅、高效的界面。下一章,我们会深入事件分发机制,那又是一个让人头疼但又必须啃下来的硬骨头。
公众号:蓝海资料掘金营,微信deep3321