第十四章: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机制分为两部分:
- 硬件VSync:由显示控制器产生,通过
/sys/class/graphics/fb0/vsync_event或DRM接口获取 - 软件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。最后通过合并图层解决了。
合成流程大致如下:
- SurfaceFlinger收集所有可见Layer
- 调用HWC的prepare(),协商合成策略
- 对于需要GPU合成的图层,调用OpenGL ES进行渲染
- 调用HWC的set(),提交最终显示帧
- 等待VSync,开始下一轮
这里有个优化点:尽量减少GPU合成的图层数。GPU合成不仅耗电,还会增加延迟。我建议在系统层面做图层合并,比如把状态栏、导航栏这些静态图层提前合成好,减少每帧的图层数量。
个人经验:我在优化启动速度时发现,开机动画阶段SurfaceFlinger的图层数特别多。后来把开机动画改成了直接由HWC合成,启动时间缩短了200ms。这种优化思路可以举一反三。
这张图把整个显示系统的数据流和同步关系都画出来了。你可以看到,App和SurfaceFlinger通过BufferQueue解耦,VSync信号协调着双方的节奏,最终由HWC或GPU完成合成,送到显示设备。
总结一下今天的内容:SurfaceFlinger的启动是系统初始化的关键一步;BufferQueue是App和SF之间的数据通道;VSync保证了画面的流畅和完整;HWC则把合成工作交给了最合适的硬件模块。这四个知识点环环相扣,构成了Android显示系统的核心骨架。