第十四章:SurfaceFlinger与显示系统

SurfaceFlinger,这个名字在Android系统里如雷贯耳。说白了,它就是整个显示系统的“大管家”。你手机屏幕上每一帧画面,从App绘制完成到最终显示出来,中间所有环节都由它掌控。今天我们就来彻底搞懂它。

14.1 SurfaceFlinger的启动流程

SurfaceFlinger是一个系统服务,它在Android启动早期就被拉起来了。我记得第一次看它的启动代码时,被init.rc里那一长串参数搞得有点懵。后来才明白,其实核心逻辑并不复杂。

启动入口在main_surfaceflinger.cpp里。大致流程是这样的:

// 伪代码,展示核心流程
int main() {
    // 1. 初始化进程
    ProcessState::self()->startThreadPool();
    
    // 2. 创建SurfaceFlinger实例
    sp<SurfaceFlinger> flinger = new SurfaceFlinger();
    
    // 3. 初始化硬件 composer
    flinger->init();
    
    // 4. 注册到ServiceManager
    sp<IServiceManager> sm = defaultServiceManager();
    sm->addService(String16("SurfaceFlinger"), flinger);
    
    // 5. 进入主循环
    flinger->run();
    
    return 0;
}

这里有个关键点:SurfaceFlinger必须在HAL层的Composer初始化完成后,才能对外提供服务。我在项目中遇到过一个问题,就是某个厂商的HAL实现有bug,导致SurfaceFlinger启动时卡在了init阶段,整个系统黑屏。排查了很久才发现是Composer的回调没注册成功。

核心要点:SurfaceFlinger启动后,会创建一个EventThread,专门负责VSync信号的派发。这个线程是整个显示系统的“心跳”。

14.2 BufferQueue与GraphicBuffer

接下来聊聊BufferQueue。这是SurfaceFlinger和App之间传递图像数据的“管道”。你想想看,App绘制一帧画面,总不能直接往屏幕上写吧?那不乱套了。所以Android设计了BufferQueue这套机制。

BufferQueue的核心是生产者-消费者模型:

  • 生产者:App端的Surface,通过dequeueBuffer获取空闲缓冲区
  • 消费者:SurfaceFlinger,通过acquireBuffer获取已填充的缓冲区
  • 缓冲区:GraphicBuffer,实际存储像素数据的地方

GraphicBuffer的分配是个值得注意的点。它通过Gralloc模块从硬件层面分配内存。不同的GPU厂商实现不同,有的用ION,有的用DMA-BUF。我曾经踩过一个坑:某个低端设备上,频繁分配和释放GraphicBuffer导致内存碎片化严重,最终SurfaceFlinger进程OOM了。

避坑指南:我曾经在优化视频播放卡顿时发现,BufferQueue的深度设置很关键。默认是3个buffer,但如果生产者生产速度远快于消费者消费速度,建议适当增加深度。不过也别太大,否则内存占用会飙升。

来看看BufferQueue的核心操作流程:

// 生产者端
status_t err = mSurface->dequeueBuffer(&buf, &fence);
// 填充数据...
err = mSurface->queueBuffer(buf, &fence);

// 消费者端(SurfaceFlinger内部)
sp<GraphicBuffer> buffer;
sp<Fence> acquireFence;
status_t err = mConsumer->acquireBuffer(&item, 0);
// 合成处理...
err = mConsumer->releaseBuffer(item.mSlot, item.mGraphicBuffer, releaseFence);

14.3 VSync与帧率控制

VSync,垂直同步信号。这是显示系统里最基础也最重要的概念。为什么需要VSync?因为屏幕刷新是有固定节奏的,比如60Hz就是每16.6ms刷新一次。如果App在屏幕刷新到一半时提交了新数据,就会出现画面撕裂。

Android的VSync机制分为两部分:

  1. 硬件VSync:由显示控制器产生,通过/sys/class/graphics/fb0/vsync_event或DRM接口获取
  2. 软件VSync:由SurfaceFlinger的EventThread模拟,用于调度App的绘制和合成

嗯,这里要注意。Android 4.1引入了Project Butter,核心就是通过VSync来同步App绘制和SurfaceFlinger合成。具体来说,EventThread会周期性地唤醒App的UI线程和SurfaceFlinger的合成线程。

帧率控制的关键参数:

参数 含义 典型值
mRefreshPeriod 屏幕刷新周期 16.6ms (60Hz)
mPhaseOffset App与SF的相位偏移 2ms (App领先SF)
mLastVsyncTime 上次VSync时间戳 系统启动后的纳秒数

为什么App要领先SF 2ms?因为App绘制需要时间。如果App和SF同时响应VSync,App还没画完,SF就开始合成了,那拿到的就是上一帧的数据,白白浪费一个周期。

14.4 窗口合成(HWC)

最后说说窗口合成。SurfaceFlinger把所有App的图层(Layer)合成一帧,然后交给显示硬件。这个活儿可以纯软件做(GPU合成),也可以硬件做(HWC合成)。

HWC,全称Hardware Composer,是显示硬件厂商提供的HAL模块。它的核心接口是prepare()set()

// HWC 2.0 接口示例
class HWC2::Device {
    virtual Error prepare(Display* display, 
                          std::vector<Layer*> layers) = 0;
    virtual Error set(Display* display,
                      std::vector<Layer*> layers) = 0;
};

SurfaceFlinger会先调用prepare(),告诉HWC有哪些图层需要合成。HWC会检查这些图层能否由硬件直接合成。能的话,就标记为HWC_LAYER;不能的话,标记为HWC_FRAMEBUFFER,由GPU先合成到FBO,再交给HWC。

注意:HWC的合成能力因硬件而异。有些低端芯片只能合成2-3个图层,超过的就必须走GPU。我在一个项目里遇到过,某个设备上开了画中画功能,图层数突然变成4个,结果HWC处理不过来,帧率直接掉到30fps。最后通过合并图层解决了。

合成流程大致如下:

  1. SurfaceFlinger收集所有可见Layer
  2. 调用HWC的prepare(),协商合成策略
  3. 对于需要GPU合成的图层,调用OpenGL ES进行渲染
  4. 调用HWC的set(),提交最终显示帧
  5. 等待VSync,开始下一轮

这里有个优化点:尽量减少GPU合成的图层数。GPU合成不仅耗电,还会增加延迟。我建议在系统层面做图层合并,比如把状态栏、导航栏这些静态图层提前合成好,减少每帧的图层数量。

个人经验:我在优化启动速度时发现,开机动画阶段SurfaceFlinger的图层数特别多。后来把开机动画改成了直接由HWC合成,启动时间缩短了200ms。这种优化思路可以举一反三。

SurfaceFlinger显示系统架构图 App (生产者) dequeueBuffer/queueBuffer BufferQueue GraphicBuffer池 SurfaceFlinger acquireBuffer/releaseBuffer EventThread VSync信号派发 HWC prepare/set GPU合成 OpenGL ES渲染 显示设备 屏幕刷新 图例: 数据流 VSync信号

这张图把整个显示系统的数据流和同步关系都画出来了。你可以看到,App和SurfaceFlinger通过BufferQueue解耦,VSync信号协调着双方的节奏,最终由HWC或GPU完成合成,送到显示设备。

总结一下今天的内容:SurfaceFlinger的启动是系统初始化的关键一步;BufferQueue是App和SF之间的数据通道;VSync保证了画面的流畅和完整;HWC则把合成工作交给了最合适的硬件模块。这四个知识点环环相扣,构成了Android显示系统的核心骨架。

公众号:蓝海资料掘金营,微信deep3321