21、WebKit 的 GPU 加速:GPU 进程架构、纹理上传与合成、Canvas 2D/WebGL 加速、Overlay 与 SurfaceFlinger 交互
聊到浏览器渲染,很多人第一反应是「HTML 解析 -> CSS 计算 -> 布局 -> 绘制 -> 合成」。嗯,这个流程没错,但到了 Android 平台上,事情就没那么简单了。你想想看,一个网页里可能有几十个图层、视频、Canvas 动画、WebGL 游戏……如果全靠 CPU 去画,帧率根本撑不住。
所以,WebKit 必须把活儿甩给 GPU。今天我们就来拆解一下,WebKit 在 Android 上到底是怎么跟 GPU 打交道的。
GPU 进程架构:为什么需要独立进程?
我记得早期 Android WebView 是跑在应用进程里的,GPU 操作也直接走应用进程。结果呢?一旦 GPU 驱动崩了,整个 App 直接挂掉。我在项目中遇到过好几次,用户反馈「打开网页就闪退」,查了半天发现是某款手机的 GPU 驱动有 bug。
所以后来 Chromium 和 WebKit 都引入了 GPU 进程架构。说白了,就是把所有 GPU 相关的操作——纹理上传、合成、Canvas 渲染——全部隔离到一个独立的 GPU 进程里。
核心思路:
- GPU 进程负责所有 OpenGL ES / Vulkan 调用
- 浏览器主进程通过 IPC 与 GPU 进程通信
- GPU 进程崩溃不影响主进程,WebView 可以优雅降级
这样做的好处很明显:
- 稳定性提升:GPU 驱动 bug 不会拖垮整个 App
- 资源隔离:不同 Tab 的 GPU 资源互不干扰
- 生命周期独立:GPU 进程可以按需创建和销毁
但代价也不小——IPC 通信有开销。每次纹理上传、每次合成指令,都要走一次跨进程调用。所以 WebKit 做了个优化:共享内存。纹理数据通过 Android 的 SharedMemory 机制直接映射到 GPU 进程,省掉数据拷贝。
纹理上传与合成:从 Bitmap 到 GPU 纹理
网页里的每一张图片、每一个 Canvas 绘制结果,最终都要变成 GPU 纹理才能被合成。这个过程叫「纹理上传」。
你可能会问:直接把 Bitmap 扔给 GPU 不就行了?嗯,没那么简单。Android 的 Bitmap 是 CPU 内存里的数据,GPU 纹理是显存里的数据。两者之间需要一次 CPU -> GPU 的数据传输。
WebKit 的做法是:
- 先把 Bitmap 解码到 CPU 内存
- 通过
glTexImage2D或eglCreateImageKHR上传到 GPU - 上传完成后,CPU 内存就可以释放了
但这里有个坑。我曾经遇到过一个问题:大图上传导致掉帧。原因是纹理上传是同步操作,GPU 进程要等数据传完才能继续干活。后来我们用了 异步纹理上传——先把数据放到一个 PBO(Pixel Buffer Object)里,GPU 自己慢慢读,CPU 可以继续干别的事。
避坑指南:我曾经在低端机上遇到纹理上传卡顿,后来发现是纹理格式没选对。用 GL_RGB 代替 GL_RGBA,带宽直接省了 25%。对于不透明的图片,一定要用 GL_RGB。
合成阶段就更讲究了。WebKit 会把所有图层(Layer)合成一张最终的画面。这个过程叫「合成」或「Compositing」。合成器会遍历所有图层,计算它们的变换矩阵、透明度、裁剪区域,然后调用 OpenGL ES 绘制。
合成的关键数据结构是 CompositingLayer:
// 简化的 CompositingLayer 结构
struct CompositingLayer {
GLuint textureId; // 图层对应的纹理
float transform[16]; // 变换矩阵
float opacity; // 透明度
IntRect clipRect; // 裁剪区域
bool isOpaque; // 是否不透明
};
Canvas 2D / WebGL 加速
Canvas 2D 和 WebGL 是 GPU 加速的重头戏。先说 Canvas 2D。
Canvas 2D 的 API 是 CPU 友好的——画线、画圆、填充颜色,这些操作如果全用 CPU 做,性能还行。但一旦涉及大量绘制(比如游戏、数据可视化),CPU 就扛不住了。
WebKit 的做法是:把 Canvas 2D 的绘制指令缓存起来,批量提交给 GPU。具体来说:
- 每个 Canvas 2D 上下文对应一个 Skia GPU 表面
- 绘制指令先记录在 Skia 的指令列表中
- 等到需要显示时,一次性提交给 GPU 渲染
这样做的好处是减少了 CPU-GPU 交互次数。我见过一个极端案例:一个 Canvas 2D 动画每帧绘制 1000 个圆,如果不做批量提交,每帧要调用 1000 次 OpenGL 函数,帧率直接掉到 10fps。批量提交后,帧率回到了 60fps。
WebGL 就简单多了——它本身就是 OpenGL ES 的封装。WebKit 把 WebGL 的调用直接转发给 GPU 进程。但这里有个关键点:WebGL 的上下文创建。
WebGL 上下文需要跟 Android 的 EGL 环境绑定。WebKit 的做法是:
- 在 GPU 进程中创建一个 EGL 上下文
- 把 WebGL 的绘制命令映射到 OpenGL ES 调用
- 渲染结果直接输出到纹理,供合成器使用
注意:WebGL 上下文是昂贵的资源。每个 WebGL 上下文都会占用 GPU 内存和 EGL 资源。我建议在页面销毁时及时释放 WebGL 上下文,否则会出现 GPU 内存泄漏。
Overlay 与 SurfaceFlinger 交互
最后一步,合成器把画面交给 Android 的 SurfaceFlinger。SurfaceFlinger 是 Android 的窗口合成系统,它负责把各个应用的窗口合成到屏幕上。
WebKit 跟 SurfaceFlinger 的交互方式有两种:
- 传统方式:WebKit 合成完成后,把结果通过
Surface.lockCanvas提交给 SurfaceFlinger - Overlay 方式:WebKit 直接把图层作为 Overlay 提交给 SurfaceFlinger,由 SurfaceFlinger 硬件合成
Overlay 方式的好处是:省掉了一次 GPU 合成。如果网页只有一个视频层和一个背景层,WebKit 可以把视频层作为 Overlay 提交,背景层作为普通窗口提交。SurfaceFlinger 用硬件直接合成,效率极高。
但 Overlay 不是万能的。Android 对 Overlay 的数量有限制(通常是 2-4 个),而且 Overlay 图层必须是 不透明 的。如果网页有透明图层,Overlay 就用不了。
我在项目中遇到过一个问题:某个视频网站播放视频时,Overlay 总是用不上,导致帧率只有 30fps。查了半天发现,视频层上面有一个半透明的控制条,破坏了 Overlay 的条件。后来我们把控制条和视频层合并成一个图层,Overlay 才生效。
整体架构图
下面这张图展示了 WebKit GPU 加速的整体流程:
性能优化要点
最后,我总结几个 GPU 加速的优化要点,都是我在实际项目中踩过的坑:
| 优化点 | 说明 | 效果 |
|---|---|---|
| 纹理格式选择 | 不透明图片用 GL_RGB,透明图片用 GL_RGBA | 带宽节省 25% |
| 异步纹理上传 | 使用 PBO 避免 CPU 等待 | 帧率提升 20-30% |
| Overlay 优化 | 合并透明图层,满足 Overlay 条件 | 功耗降低 40% |
| Canvas 批量提交 | 缓存绘制指令,减少 GPU 调用次数 | 帧率提升 50%+ |
| WebGL 上下文复用 | 避免频繁创建/销毁 EGL 上下文 | 内存节省 30% |
嗯,GPU 加速这块内容确实不少。但说白了,核心就三件事:怎么把数据传给 GPU、怎么让 GPU 高效合成、怎么跟 SurfaceFlinger 配合。搞懂了这三件事,WebKit 的 GPU 加速你就拿下了。