第十二章:跨平台实战¶
本章目标:学习将 WhatsCanvas 集成到 Android(JNI + GLSurfaceView)、iOS(Metal + CoreText)和 Web(Emscripten + WebGL 2)平台的完整流程与要点。
12.1 跨平台架构概述¶
WhatsCanvas 的跨平台设计遵循"库只负责渲染"原则:
┌─────────────────────────────────────────────┐
│ 应用逻辑 (C++17) │
├─────────────────────────────────────────────┤
│ WhatsCanvas API (wsc::) │
├──────┬──────┬──────┬──────┬─────────────────┤
│ SW │ GL │ GLES │ VK │ Metal │
├──────┴──────┴──────┴──────┴─────────────────┤
│ 平台宿主 (Host) │
│ Desktop: GLFW │
│ Android: GLSurfaceView + JNI │
│ iOS: UIKit + CAMetalLayer │
│ Web: Emscripten + WebGL 2 │
└─────────────────────────────────────────────┘
核心绘制代码写一份 C++,各平台只需实现"宿主"层(创建窗口/surface、管理生命周期、处理输入)。
12.2 Android 集成¶
概述¶
Android 通过 JNI 调用 C++ 渲染代码,使用 GLSurfaceView 提供 OpenGL ES 3.0 上下文。
项目结构¶
app/
├── src/main/
│ ├── java/.../MainActivity.java
│ ├── java/.../CanvasGLSurfaceView.java
│ ├── java/.../CanvasRenderer.java
│ └── cpp/
│ ├── CMakeLists.txt
│ ├── native_renderer.cpp
│ └── scene.cpp / scene.h
└── libs/
└── whatscanvas-android-release-1.2.1.aar
Gradle 配置¶
// build.gradle (app)
android {
defaultConfig {
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86_64'
}
}
}
dependencies {
implementation files('libs/whatscanvas-android-release-1.2.1.aar')
}
CMakeLists.txt (JNI 层)¶
cmake_minimum_required(VERSION 3.16)
project(myapp_native LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
# Prefab AAR 提供的 WhatsCanvas
find_package(WhatsCanvas CONFIG REQUIRED)
add_library(myapp_native SHARED
native_renderer.cpp
scene.cpp
)
target_link_libraries(myapp_native PRIVATE
WhatsCanvas::OpenGLES
android
log
EGL
GLESv3
)
JNI 渲染代码¶
// native_renderer.cpp
#include <jni.h>
#include <android/log.h>
#include <wsc/wsc.h>
#include <memory>
static std::unique_ptr<wsc::Canvas> g_canvas;
extern "C" {
JNIEXPORT void JNICALL
Java_com_example_CanvasRenderer_nativeInit(JNIEnv*, jobject, jint w, jint h)
{
// Android GLSurfaceView 已创建好 GLES 上下文
g_canvas = wsc::Canvas::create(wsc::Canvas::Backend::OpenGLES, w, h);
if (g_canvas) {
g_canvas->initializeContext();
// 注册字体
for (const auto& face : wsc::FontSystem::defaultSystemFontFaces()) {
g_canvas->registerFontFace(face);
}
g_canvas->setFontFallbackChain(wsc::FontSystem::defaultFallbackChain());
}
}
JNIEXPORT void JNICALL
Java_com_example_CanvasRenderer_nativeResize(JNIEnv*, jobject, jint w, jint h)
{
if (g_canvas) {
g_canvas->setSize(w, h);
}
}
JNIEXPORT void JNICALL
Java_com_example_CanvasRenderer_nativeRender(JNIEnv*, jobject, jfloat time)
{
if (!g_canvas) return;
g_canvas->beginFrame();
// 绘制内容(与桌面完全相同的代码)
wsc::Paint bg;
bg.setLinearGradient(0, 0, g_canvas->getWidth(), g_canvas->getHeight(),
wsc::Color(30, 40, 60), wsc::Color(10, 15, 25));
g_canvas->drawRect(wsc::RectF(0, 0,
g_canvas->getWidth(), g_canvas->getHeight()), bg);
// ... 更多绘制 ...
g_canvas->endFrame();
}
JNIEXPORT void JNICALL
Java_com_example_CanvasRenderer_nativeDestroy(JNIEnv*, jobject)
{
if (g_canvas) {
g_canvas->finalizeContext();
g_canvas.reset();
}
}
}
Java GLSurfaceView.Renderer¶
public class CanvasRenderer implements GLSurfaceView.Renderer {
private long startTime;
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
startTime = System.nanoTime();
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
nativeInit(width, height);
nativeResize(width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
float elapsed = (System.nanoTime() - startTime) / 1_000_000_000f;
nativeRender(elapsed);
}
private native void nativeInit(int width, int height);
private native void nativeResize(int width, int height);
private native void nativeRender(float time);
private native void nativeDestroy();
}
要点¶
- AAR 内含 Prefab 元数据,Gradle 4.1+ 自动识别
GLSurfaceView在 GL 线程调用回调,Canvas 操作自然在正确线程- 生命周期:
onPause时不要销毁 Canvas,onSurfaceCreated可能重新调用 - 字体:Android 无 fontconfig,使用 WhatsCanvas 内置 FreeType + HarfBuzz
12.3 iOS 集成¶
概述¶
iOS 使用 Metal 后端 + CoreText 文本引擎,通过 XCFramework 分发。
项目结构¶
MyApp.xcodeproj/
├── MyApp/
│ ├── AppDelegate.swift
│ ├── ViewController.swift
│ ├── MetalCanvasView.swift
│ └── Bridging-Header.h
└── Frameworks/
└── WhatsCanvas.xcframework/
Xcode 配置¶
- 将
WhatsCanvas.xcframework拖入项目的 Frameworks - 设置 "Embed & Sign"
- 在 Build Settings 中添加 Header Search Paths 指向 XCFramework 的 headers
Metal 渲染视图¶
// MetalCanvasView.swift
import UIKit
import MetalKit
class MetalCanvasView: MTKView {
private var canvas: UnsafeMutablePointer<WscCanvas>?
private var startTime: CFTimeInterval = 0
override func didMoveToWindow() {
super.didMoveToWindow()
guard let metalDevice = MTLCreateSystemDefaultDevice() else { return }
self.device = metalDevice
self.delegate = self
setupCanvas()
}
private func setupCanvas() {
let scale = UIScreen.main.scale
let w = Int(bounds.width * scale)
let h = Int(bounds.height * scale)
// 通过 C++ 桥接创建 Metal Canvas
canvas = wsc_canvas_create_metal(Int32(w), Int32(h))
wsc_canvas_set_device_pixel_ratio(canvas, Float(scale))
wsc_canvas_set_text_backend_coretext(canvas)
startTime = CACurrentMediaTime()
}
}
extension MetalCanvasView: MTKViewDelegate {
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
wsc_canvas_set_size(canvas, Int32(size.width), Int32(size.height))
}
func draw(in view: MTKView) {
let elapsed = Float(CACurrentMediaTime() - startTime)
wsc_canvas_begin_frame(canvas)
// 绘制(通过 C 桥接调用 C++ 渲染代码)
wsc_scene_render(canvas, elapsed)
wsc_canvas_end_frame(canvas)
wsc_canvas_present(canvas)
}
}
要点¶
- 使用
WhatsCanvas::Metaltarget(独立,不依赖 OpenGL) - 文本使用 CoreText 后端,无需 FreeType/HarfBuzz 依赖
- 支持
CAMetalLayer窗口呈现 - 需要处理 orientation 变化和安全区域
- 真机和模拟器使用同一个 XCFramework(arm64 + x86_64 切片)
12.4 Web (Emscripten + WebGL 2)¶
概述¶
通过 Emscripten 将 C++ 代码编译为 WebAssembly,使用 OpenGL ES 路径(映射为 WebGL 2)。
编译命令¶
emcmake cmake -S . -B build-wasm \
-DWHATSCANVAS_BUILD_OPENGLES=ON \
-DWHATSCANVAS_BUILD_OPENGL=OFF \
-DWHATSCANVAS_BUILD_SOFTWARE=OFF \
-DWHATSCANVAS_BUILD_DEMO=OFF
emmake cmake --build build-wasm
C++ 入口¶
// wasm_main.cpp
#include <emscripten.h>
#include <emscripten/html5.h>
#include <wsc/wsc.h>
#include <algorithm>
#include <cmath>
#include <memory>
static std::unique_ptr<wsc::Canvas> g_canvas;
static double g_startTime = 0;
static int g_physicalWidth = 0;
static int g_physicalHeight = 0;
static float g_logicalWidth = 0.0f;
static float g_logicalHeight = 0.0f;
static float g_dpr = 1.0f;
bool resizeSurface() {
double cssWidth = 0.0;
double cssHeight = 0.0;
if (emscripten_get_element_css_size(
"#canvas", &cssWidth, &cssHeight) != EMSCRIPTEN_RESULT_SUCCESS) {
return false;
}
const double rawDpr = emscripten_get_device_pixel_ratio();
const float dpr = std::isfinite(rawDpr) && rawDpr > 0.0
? static_cast<float>(rawDpr) : 1.0f;
const int physicalWidth = std::max(1, static_cast<int>(std::lround(cssWidth * dpr)));
const int physicalHeight = std::max(1, static_cast<int>(std::lround(cssHeight * dpr)));
if (physicalWidth == g_physicalWidth
&& physicalHeight == g_physicalHeight
&& std::abs(dpr - g_dpr) <= 0.001f) {
return true;
}
emscripten_set_canvas_element_size("#canvas", physicalWidth, physicalHeight);
g_physicalWidth = physicalWidth;
g_physicalHeight = physicalHeight;
g_logicalWidth = static_cast<float>(cssWidth);
g_logicalHeight = static_cast<float>(cssHeight);
g_dpr = dpr;
if (g_canvas) {
g_canvas->setSize(g_physicalWidth, g_physicalHeight);
g_canvas->setDevicePixelRatio(g_dpr);
}
return true;
}
void mainLoop() {
if (!resizeSurface()) return;
double now = emscripten_get_now() / 1000.0;
float elapsed = static_cast<float>(now - g_startTime);
g_canvas->beginFrame();
// 绘制内容
wsc::Paint bg;
bg.setLinearGradient(0, 0, g_logicalWidth, g_logicalHeight,
wsc::Color(20, 30, 50), wsc::Color(60, 20, 80));
g_canvas->drawRect(wsc::RectF(0, 0,
g_logicalWidth, g_logicalHeight), bg);
wsc::Paint text;
text.setColor(wsc::Color::WHITE);
text.setTextSize(32.0f);
text.setTextAlign(wsc::Paint::TextAlign::CENTER);
g_canvas->drawText("WhatsCanvas on Web!",
g_logicalWidth / 2.0f, g_logicalHeight / 2.0f, text);
g_canvas->endFrame();
}
int main() {
// 创建 WebGL 2 上下文
EmscriptenWebGLContextAttributes attrs;
emscripten_webgl_init_context_attributes(&attrs);
attrs.majorVersion = 2;
attrs.minorVersion = 0;
attrs.antialias = true;
attrs.stencil = true;
auto ctx = emscripten_webgl_create_context("#canvas", &attrs);
emscripten_webgl_make_context_current(ctx);
// CSS 尺寸是逻辑尺寸,drawing buffer 尺寸是 CSS 尺寸乘以 DPR。
if (!resizeSurface()) return 1;
// 创建 WhatsCanvas (OpenGL ES → WebGL 2)
g_canvas = wsc::Canvas::create(
wsc::Canvas::Backend::OpenGLES, g_physicalWidth, g_physicalHeight);
g_canvas->setDevicePixelRatio(g_dpr);
if (!g_canvas->initializeContext()) return 1;
g_startTime = emscripten_get_now() / 1000.0;
// 设置主循环
emscripten_set_main_loop(mainLoop, 0, true);
return 0;
}
HTML 模板¶
<!DOCTYPE html>
<html>
<head>
<style>
canvas { width: 100vw; height: 100vh; display: block; }
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script src="whatscanvas_app.js"></script>
</body>
</html>
要点¶
- Emscripten 将 OpenGL ES 3.0 调用映射到 WebGL 2
- 使用
emscripten_set_main_loop代替传统 while 循环 - CSS 尺寸用于逻辑布局,drawing buffer 按
CSS 尺寸 × DPR分配 - DPR 通过
emscripten_get_device_pixel_ratio()获取,并传给setDevicePixelRatio getWidth()/getHeight()是物理尺寸,不能用来计算设置 DPR 后的页面中心- 字体需要预加载或从 URL 异步加载
- 尚无 WebGPU 后端和预编译 Web 发布包
12.5 共享绘制代码¶
跨平台的关键是绘制代码只写一份。推荐使用 Scene 接口模式:
// scene.h — 跨平台共享
#pragma once
#include <wsc/wsc.h>
class IScene {
public:
virtual ~IScene() = default;
virtual void onInit(wsc::Canvas& canvas) = 0;
virtual void onResize(wsc::Canvas& canvas, float w, float h) = 0;
virtual void onFrame(wsc::Canvas& canvas, float elapsed) = 0;
virtual void onDestroy() = 0;
};
// 具体 Scene 实现(纯 C++ 绘制逻辑)
class MyAppScene : public IScene {
public:
void onInit(wsc::Canvas& canvas) override {
// 注册字体、加载图片
}
void onResize(wsc::Canvas& canvas, float w, float h) override {
width_ = w;
height_ = h;
}
void onFrame(wsc::Canvas& canvas, float elapsed) override {
// 全部绘制代码在这里
// 在所有平台上执行相同的代码
}
void onDestroy() override {
// 释放资源
}
private:
float width_ = 0, height_ = 0;
};
各平台宿主只需要:
Desktop: GlfwHost 调用 scene->onFrame()
Android: JNI 层转发 GLSurfaceView 回调
iOS: MTKViewDelegate 调用 scene->onFrame()
Web: emscripten_set_main_loop 调用 scene->onFrame()
12.6 平台差异处理¶
| 差异点 | 处理方式 |
|---|---|
| 字体 | 各平台注册不同的系统字体或内嵌字体 |
| DPR | 各平台 API 不同,统一设置 setDevicePixelRatio |
| 安全区域 | iOS 需要 safeAreaInsets,其他平台全屏 |
| 生命周期 | Android pause/resume,iOS background/foreground |
| 触摸 vs 鼠标 | 输入层抽象为统一的事件模型 |
| 文件路径 | Android assets、iOS bundle、Web URL |
字体加载的平台适配¶
void loadPlatformFonts(wsc::Canvas& canvas) {
#if defined(__ANDROID__)
// Android: 从 assets 加载
auto fontData = loadAsset("fonts/NotoSansSC-Regular.otf");
wsc::FontFace face = wsc::FontFace::fromMemory(
wsc::FontDescriptor("Noto Sans SC"), std::move(fontData));
canvas.registerFontFace(face);
#elif defined(__APPLE__)
// iOS/macOS: 使用 CoreText 后端,系统字体自动可用
canvas.setTextBackend(wsc::Canvas::TextBackend::CoreText);
#elif defined(__EMSCRIPTEN__)
// Web: 预加载字体或使用默认 fallback
// 字体文件需要通过 --preload-file 嵌入
#else
// Desktop: 发现系统字体
for (const auto& face : wsc::FontSystem::discoverInstalledFontFaces()) {
canvas.registerFontFace(face);
}
#endif
canvas.setFontFallbackChain(wsc::FontSystem::defaultFallbackChain());
}
12.7 发布包形式汇总¶
| 平台 | 发布形式 | 后端 | 特殊说明 |
|---|---|---|---|
| Windows x64 | ZIP (shared) | OpenGL, GLES, Software | DLL 需随 exe 分发 |
| Linux x64 | ZIP (static) | OpenGL, Software | 需要 Mesa GL 驱动 |
| macOS universal | ZIP (static) | OpenGL, Metal, Software | x86_64 + arm64 |
| Android | AAR (Prefab) | OpenGL ES | 3 ABI: armeabi-v7a, arm64-v8a, x86_64 |
| iOS | XCFramework (static) | Metal | arm64 真机 + 模拟器 |
| Web | 源码构建 | OpenGL ES (WebGL 2) | 无预编译包 |
12.8 CI/CD 跨平台验证¶
WhatsCanvas 仓库的 CI 覆盖:
# .github/workflows/cross-platform-validation.yml 概要
jobs:
windows:
- MSVC 单元测试
- OpenGL/Software 像素回归
- Package consumer 集成
linux:
- GCC 构建 + 单元测试
- Mesa/Xvfb OpenGL 像素门禁
- OpenGL ES 滤镜像素对齐
macos:
- Clang universal 构建
- Metal 像素/契约门禁
android:
- NDK 三 ABI 构建
- AAR 打包
ios:
- XCFramework 构建
- 模拟器 UI 测试
web:
- Emscripten 构建
- 浏览器 headless 测试
12.9 小结¶
本章学习了:
- [x] 跨平台架构设计(Host + Scene 模式)
- [x] Android 集成(JNI + GLSurfaceView + AAR)
- [x] iOS 集成(Metal + CoreText + XCFramework)
- [x] Web 集成(Emscripten + WebGL 2)
- [x] 共享绘制代码的 Scene 接口模式
- [x] 平台差异处理(字体、DPR、生命周期)
- [x] 各平台发布包形式
- [x] CI/CD 跨平台验证
教程完结¶
恭喜你完成了 WhatsCanvas 全部 12 章教程!你现在掌握了:
- 基础绘制 — 图形、Paint、Path、变换
- 进阶能力 — 图片、文本、图层滤镜
- 工程实践 — 窗口呈现、多后端、性能优化、跨平台
更多资源: - API Reference - Visual API Gallery - 仓库示例代码 - WhatsUI — 基于 WhatsCanvas 的 UI 框架