15、View绘制流程(下):自定义View、硬件加速、SurfaceFlinger合成原理
好,咱们接着上一章聊。上回我们把View绘制的三大流程——measure、layout、draw——拆了个底朝天。今天这篇,我打算把剩下的几个硬骨头啃下来:自定义View到底该怎么写才优雅?硬件加速到底加速在哪?SurfaceFlinger这个“幕后黑手”又是怎么把一帧帧画面合成到屏幕上的?
说实话,这几个话题每个都能单独开一门课。但咱们这门课叫《Android框架深度解析》,所以我会尽量从源码和架构层面,把它们的核心脉络给你捋清楚。
15.1 自定义View:不只是重写onDraw
很多同学一提到自定义View,第一反应就是“重写onDraw,画个圆”。嗯,这没错,但太浅了。我在项目里见过太多自定义View写得“一塌糊涂”的例子——动不动就卡顿,动不动就内存泄漏。
我个人习惯把自定义View分成三类:
- 自绘型:完全自己画,比如图表、进度条。
- 组合型:把系统控件拼起来,比如带清除按钮的EditText。
- 继承型:在系统控件上做扩展,比如支持自动换行的TextView。
不管哪一类,有几个核心原则你得记住:
核心原则:
- 不要在onDraw里创建对象——onDraw每帧都可能调用,频繁GC会让你卡成PPT。
- 使用invalidate()触发重绘,而不是requestLayout()——后者会重新走measure和layout,开销大得多。
- 处理好wrap_content和padding——这是新手最容易忽略的坑。
举个例子,一个简单的自定义View,画一个带边框的圆:
public class CircleView extends View {
private Paint paint;
private float radius;
public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.BLUE);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(4f);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 处理wrap_content
int defaultSize = 200; // 默认200px
int width = resolveSize(defaultSize, widthMeasureSpec);
int height = resolveSize(defaultSize, heightMeasureSpec);
int size = Math.min(width, height);
setMeasuredDimension(size, size);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
int contentWidth = getWidth() - paddingLeft - getPaddingRight();
int contentHeight = getHeight() - paddingTop - getPaddingBottom();
radius = Math.min(contentWidth, contentHeight) / 2f;
canvas.drawCircle(paddingLeft + contentWidth / 2f,
paddingTop + contentHeight / 2f,
radius - paint.getStrokeWidth() / 2f,
paint);
}
}
你看,这里我处理了wrap_content,也处理了padding。我曾经在一个项目里看到同事没处理padding,结果在带padding的布局里,圆直接画到外面去了……嗯,这种bug排查起来还挺费劲的。
15.2 硬件加速:到底加速了什么?
从Android 3.0开始,硬件加速就默认开启了。但很多人其实没搞懂它到底干了什么。
说白了,硬件加速就是把View的绘制工作从CPU搬到了GPU上。CPU擅长逻辑控制,GPU擅长并行计算和图形渲染。你想想看,一个复杂的界面,可能有几十上百个View,每个View都要画背景、画文字、画图片……如果全让CPU干,它得累死。
硬件加速的核心变化在于:
- 软件绘制:CPU直接往Bitmap上画,画完再交给SurfaceFlinger。
- 硬件加速:CPU把绘制指令(DisplayList)记录下来,然后交给GPU去渲染。
DisplayList是什么?你可以把它理解成一份“绘制清单”。比如“画一个红色矩形,从(10,10)到(100,100)”。这份清单可以被GPU高效地并行处理。
小提示:硬件加速不是万能的。有些Canvas操作在硬件加速下不支持,比如clipPath、drawPicture等。如果你用了这些API,系统会默默走软件绘制,反而更慢。我建议你在开发时打开“过度绘制”和“GPU渲染模式”来排查问题。
还有一个常见的误区:很多人以为硬件加速能自动让所有View变快。其实不然。如果你的View频繁调用invalidate(),每次都会重建DisplayList,GPU的优势就体现不出来了。所以,减少不必要的重绘,才是优化的根本。
15.3 SurfaceFlinger合成原理:最后一公里的奥秘
好,现在View画完了,DisplayList也提交了,接下来呢?
接下来就是SurfaceFlinger的舞台了。它是Android系统的“图像合成器”,负责把所有应用的Surface合成到一起,然后输出到屏幕。
我画了一张图,帮你理解整个流程:
流程其实不复杂:
- 每个App有一个Surface,它背后对应一个BufferQueue。
- App把绘制好的图形数据(其实是图形缓冲区)放入BufferQueue。
- SurfaceFlinger在Vsync信号到来时,从所有BufferQueue中取出最新的缓冲区。
- SurfaceFlinger把这些缓冲区合成到一起,交给Hardware Composer(HWC)或直接写入Framebuffer。
- 屏幕从Framebuffer中读取数据,显示出来。
注意:SurfaceFlinger的合成策略有两种:
- GPU合成:所有图层都交给GPU去合成,灵活性高,但功耗大。
- HWC合成:把部分图层交给硬件合成器(HWC),功耗低,但受硬件限制。
系统会根据图层数量、透明度、叠加情况等动态选择合成方式。我曾经在一个低端设备上遇到过因为HWC不支持某些图层叠加,导致界面闪烁的问题。最后通过调整图层层级解决了。
15.4 从View到屏幕:一次完整的绘制旅程
好了,我们把前面几章和这一章串起来,看看一次完整的绘制旅程是什么样的:
| 阶段 | 发生位置 | 核心工作 |
|---|---|---|
| 1. 测量 | App进程 (UI线程) | measure() 确定每个View的尺寸 |
| 2. 布局 | App进程 (UI线程) | layout() 确定每个View的位置 |
| 3. 绘制 | App进程 (UI线程) | draw() 生成DisplayList |
| 4. 渲染 | App进程 (RenderThread) | GPU执行DisplayList,渲染到图形缓冲区 |
| 5. 提交 | App进程 → SurfaceFlinger | 图形缓冲区入队到BufferQueue |
| 6. 合成 | SurfaceFlinger进程 | 合成所有Surface的缓冲区 |
| 7. 显示 | 硬件层 | 屏幕刷新,显示最终画面 |
你看,从App的UI线程到SurfaceFlinger,再到硬件,每一步都有明确的分工。这也是Android系统设计精妙的地方——各司其职,互不干扰。
个人经验:我在优化一个列表滑动卡顿的问题时,发现瓶颈不在绘制,而在SurfaceFlinger合成阶段——因为列表项用了太多半透明图层,导致HWC无法参与合成,全部走GPU合成,功耗和延迟都上去了。后来我把半透明图层合并,问题就解决了。所以,优化不能只看App内部,还要看整个流水线。
15.5 总结
这一章我们聊了三件事:
- 自定义View:核心是处理好onMeasure、onDraw,以及避免在绘制中做耗时操作。
- 硬件加速:本质是CPU记录指令,GPU执行渲染。不是所有操作都支持硬件加速,要注意兼容性。
- SurfaceFlinger合成:它是所有App画面的“汇合点”,通过BufferQueue和Vsync机制,保证画面流畅合成。
嗯,内容不少,但都是干货。你如果能把这三块吃透,Android图形系统这块基本就没什么能难住你的了。