Android 渲染体系设计中,硬件渲染是主流方案。但很多人认为软件渲染性能差、只能用于简单场景。事实并非如此——在某些轻量级场景下,软件渲染的性能完全可以接受,甚至不需要动用硬件渲染。本文将分享一个实际案例:我在轻量级渲染场景中使用了软件渲染,发现效果很好,但也遇到了一个容易被忽视的问题——帧同步(Frame Synchronization)导致的丢帧现象。
有没有想过直接构建一个使用系统 Skia 进行 GPU 渲染的 APK 来快速验证项目?
在做轻量级硬件渲染引擎的 POC 时,我们希望使用 Skia 的 API 来完成渲染工作。AOSP 中自带了 Skia 源码,通常以静态库形式存在。我们可以直接从 AOSP 编译出 libskia.so,跳过单独编译 Skia 仓库的繁琐过程,快速验证项目可行性。
在深入具体步骤之前,先理解整个方案的原理:
┌─────────────────────────────────────────────────────────────────┐
│ 应用进程 │
├─────────────────────────────────────────────────────────────────┤
│ Java/Kotlin 代码 │
│ ↓ │
│ JNI 调用原生渲染引擎 │
│ ↓ │
│ libskia.so (我们引用的系统库) │
│ ↓ GPU 渲染命令 │
│ libGLESv2.so ←→ libEGL.so │
│ ↓ ↓ │
│ GPU 驱动 与 SurfaceFlinger 通信 │
│ ↓ │
│ SurfaceFlinger 合成 │
│ ↓ │
│ 屏幕显示 (CRTC) │
└─────────────────────────────────────────────────────────────────┘
核心原理:
整体方案分为五个步骤:
默认情况下,AOSP 编译的是 libskia.a(静态库)。我们需要修改编译配置,生成动态库。
source build/envsetup.sh
lunch <target>
make libskia -j8
编译完成后,在 out/target/product/<device>/system/lib/ 目录下找到 libskia.so。
从 AOSP 源码中拷贝 Skia 头文件到工程:
your_project/
├── jni/
│ ├── libskia.so
│ └── include/
│ ├── core/
│ ├── gpu/
│ ├── config/
│ └── ...
└── app/src/main/cpp/
关键头文件目录:
include/core/ - 核心 APIinclude/gpu/ - GPU 渲染 APIinclude/config/ - 平台配置在 CMakeLists.txt 中:
add_library(skia SHARED IMPORTED)
set_target_properties(skia PROPERTIES
IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/jni/libskia.so
)
include_directories(${CMAKE_SOURCE_DIR}/jni/include)
target_link_libraries(your_native_lib skia libGLESv2 libEGL)
默认情况下,非系统应用无法链接系统级的动态库。我们需要在系统中”开绿灯”。
正确路径是 /system/etc/public.libraries.txt(注意不是 public.librdroid.txt):
libskia.so
libskia.so 可能依赖其他系统库,需要确保这些依赖也被暴露。检查依赖:
readelf -d libskia.so | grep NEEDED
常见的依赖包括:
如果依赖的系统库不在 public.libraries.txt 中,需要一并添加。
修改后需要重新编译系统镜像并刷机,这一步是必须的。
这是整个方案的核心。我们需要将 Skia 绑定到 OpenGL 上下文,让 Skia 的 GPU 后端能够正常工作。
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES2/gl2.h>
class SkiaRenderer {
private:
EGLDisplay display = EGL_NO_DISPLAY;
EGLSurface surface = EGL_NO_SURFACE;
EGLContext context = EGL_NO_CONTEXT;
EGLConfig config;
public:
bool init(ANativeWindow* window) {
// 1. 获取 EGL 显示
display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (display == EGL_NO_DISPLAY) {
return false;
}
// 2. 初始化 EGL
EGLint major, minor;
if (!eglInitialize(display, &major, &minor)) {
return false;
}
// 3. 选择配置
EGLint configAttribs[] = {
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_BLUE_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_RED_SIZE, 8,
EGL_ALPHA_SIZE, 8,
EGL_DEPTH_SIZE, 16,
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
EGL_NONE
};
EGLint numConfigs;
if (!eglChooseConfig(display, configAttribs, &config, 1, &numConfigs)) {
return false;
}
// 4. 创建窗口表面
surface = eglCreateWindowSurface(display, config, window, nullptr);
if (surface == EGL_NO_SURFACE) {
return false;
}
// 5. 创建 OpenGL ES 2.0 上下文
EGLint contextAttribs[] = {
EGL_CONTEXT_CLIENT_VERSION, 2,
EGL_NONE
};
context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs);
if (context == EGL_NO_CONTEXT) {
return false;
}
// 6. 绑定上下文到当前线程
if (!eglMakeCurrent(display, surface, surface, context)) {
return false;
}
return true;
}
};
有了 EGL 上下文后,需要创建 Skia 的 GPU 渲染上下文:
#include <include/gpu/GrContext.h>
#include <include/gpu/gl/GrGLInterface.h>
class SkiaRenderer {
// ... 前面的成员变量
GrContext* grContext = nullptr;
public:
bool initGrContext() {
// 获取当前的 EGL 函数指针
const GrGLInterface* interface = GrGLInterfaceCreateEglANativeWindow();
if (!interface) {
return false;
}
// 创建 Skia 的 GrContext
grContext = GrContext::MakeGL(interface).release();
if (!grContext) {
return false;
}
return true;
}
};
bool initialize(ANativeWindow* window, int width, int height) {
// 1. 初始化 EGL 和 OpenGL 上下文
if (!init(window)) {
return false;
}
// 2. 创建 Skia GrContext
if (!initGrContext()) {
return false;
}
// 3. 创建 Skia Surface(用于绑定的 GPU 渲染目标)
SkImageInfo info = SkImageInfo::MakeN32Premul(width, height);
SkSurfaceProps props(0, kUnknown_SkPixelGeometry);
surface = SkSurface::MakeRenderTarget(grContext, info, &props);
canvas = surface->getCanvas();
return true;
}
理解 EGL 在整个渲染管线中的作用至关重要:
┌────────────────────────────────────────────────────────────────┐
│ EGL 的双重角色 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 角色一:OpenGL ES 容器 │
│ ┌──────────────────────────────────┐ │
│ │ eglCreateContext() │ │
│ │ eglMakeCurrent() │ ──→ GPU 渲染命令执行 │
│ │ │ (libGLESv2.so) │
│ └──────────────────────────────────┘ │
│ │
│ 角色二:与 SurfaceFlinger 通信 │
│ ┌──────────────────────────────────┐ │
│ │ eglCreateWindowSurface() │ │
│ │ ↓ │ │
│ │ 创建 NativeWindow (BufferQueue) │ │
│ │ ↓ │ │
│ │ dequeueBuffer / queueBuffer │ │
│ │ ↓ │ │
│ │ SurfaceFlinger 合成 │ │
│ └──────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘
void render() {
if (!canvas || !surface) return;
// 1. 清屏
canvas->clear(SK_ColorWHITE);
// 2. 使用 Skia API 绘制
SkPaint paint;
paint.setColor(SK_ColorBLUE);
paint.setStyle(SkPaint::kFill_Style);
canvas->drawRect(SkRect::MakeXYWH(100, 100, 200, 200), paint);
// 3. 提交渲染到 GPU
surface->flush();
}
这是最关键的一步:swapBuffers 不仅交换前后缓冲区,还会将渲染好的 Buffer 提交给 SurfaceFlinger 进行合成。
void present() {
// surface->flush() 只是将渲染命令提交到 GPU
// 真正的"上屏"是通过 eglSwapBuffers 完成的
eglSwapBuffers(display, surface);
}
eglSwapBuffers 的内部流程:
eglSwapBuffers()
│
├── 1. eglSwapBuffers 是 libEGL.so 的函数
│
├── 2. 获取当前 surface 关联的 BufferQueue
│ (这是 eglCreateWindowSurface 时创建的)
│
├── 3. queueBuffer()
│ 将渲染好的 Buffer 入队
│ 告诉 SurfaceFlinger:"这一帧准备好了"
│
├── 4. SurfaceFlinger 收到通知后
│ ├── 从 BufferQueue 取帧
│ ├── 与其他层合成
│ └── 提交给 CRTC 显示
│
└── 5. 函数返回,应用可以开始渲染下一帧
整个过程对应用是透明的:我们只需要调用 eglSwapBuffers(),剩下的由 libEGL.so 内部处理。
void renderLoop() {
while (running) {
// 1. 等待 VSYNC 信号(或按需渲染)
// ...
// 2. 使用 Skia 绘制
render();
// 3. 提交到 SurfaceFlinger(真正上屏)
eglSwapBuffers(display, surface);
}
}
上述方案的完整实现可以参考开源项目:
参考项目:https://github.com/ngocdaothanh/SkiaOpenGLESAndroid
该项目展示了如何在 Android 上将 Skia 与 OpenGL ES 结合使用,基本思路与本文一致。
本文介绍了一种在 Android Studio 中引用系统 libskia.so 进行硬件渲染的 POC 方案:
/system/etc/public.libraries.txt 暴露库核心原理:
这个方案适用于 POC 验证和轻量级渲染场景。如果项目需要长期维护,建议评估从 libhwui 导出 Skia 符号的方案。
参考资料:
本文记录了将 OpenClaw 接入飞书的完整过程,包括安装、配置向导、大模型 API Key 设置、飞书应用创建,以及遇到的坑和解决方案,帮助你快速搭建自己的 AI 助手机器人。
本文做了一些常见知识点归纳,方便对知识的梳理和应用,比如:Rust路径运算符,其中就涉及turbofish运算符,是Rust中一种特殊做法。
本文使用Skia Debugger来分析图层合成的方法,可以评估App或者系统的图层参数设置是否正确,是否存在多余的图层。特别是在座舱、屏幕比较多、自定义UI和图层比较多的场景下,用途是非常大的。(它也是可以用于分析App绘制的Skia过程哦。)
本文从大的视图给出一个Android图层合成的技术描述,从App端开始,直到在屏幕上显示。本文侧重于SurfaceFlinger端,包括了HWC合成与GPU合成,大致涵盖了各个模块的作用和关联。由于是技术栈的概述性文章,目的是一览技术栈,明白Android图形是怎么显示的,会省略很多基础性的内容,以及详细的描述。