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合成到一起,然后输出到屏幕。

我画了一张图,帮你理解整个流程:

Android 图形显示流水线 App 1 (Surface) App 2 (Surface) App 3 (Surface) BufferQueue (每个App对应一个) SurfaceFlinger (合成 + 合成器) Hardware Composer / Framebuffer → 屏幕 Vsync 信号驱动

流程其实不复杂:

  1. 每个App有一个Surface,它背后对应一个BufferQueue。
  2. App把绘制好的图形数据(其实是图形缓冲区)放入BufferQueue。
  3. SurfaceFlinger在Vsync信号到来时,从所有BufferQueue中取出最新的缓冲区。
  4. SurfaceFlinger把这些缓冲区合成到一起,交给Hardware Composer(HWC)或直接写入Framebuffer。
  5. 屏幕从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图形系统这块基本就没什么能难住你的了。