编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

  • iOS开发和进阶

  • Web开发和进阶

  • Linux应用开发

    • Linux应用开发
    • QML基础入门

      • QML基础入门
      • 嵌入式GUI技术全景
      • QML引擎与渲染原理
        • 2.1 案例引入
          • 2.1.1 一个让人困惑的性能现象
          • 2.1.2 揭开谜底:批处理被打断
          • 2.1.3 本章要回答的三个问题
        • 2.2 QML 引擎到底做了什么
          • 2.2.1 三大引擎角色分工
          • 2.2.2 从 QML 文本到 C++ 对象树
          • 2.2.3 对象树 vs 渲染树
          • 2.2.4 属性绑定的依赖图机制
          • 2.2.5 编译时 vs 运行时处理
          • 2.2.6 综合案例与思考
        • 2.3 Scene Graph 渲染管线
          • 2.3.1 Scene Graph 是什么
          • 2.3.2 渲染线程的四阶段循环
          • 2.3.3 批处理渲染:性能命门
          • 2.3.4 Draw Call 的真实代价
          • 2.3.5 综合案例与思考
        • 2.4 渲染线程与 GUI 线程
          • 2.4.1 双线程模型总览
          • 2.4.2 同步阶段如何工作
          • 2.4.3 阻塞场景的实测对比
          • 2.4.4 三种渲染循环策略
        • 2.5 嵌入式渲染后端
          • 2.5.1 四种渲染后端对比
          • 2.5.2 EGLFS 运行原理
          • 2.5.3 软件渲染降级策略
          • 2.5.4 选型决策树
        • 2.6 性能诊断工具
          • 2.6.1 QSG_VISUALIZE 可视化调试
          • 2.6.2 Qt Quick Profiler
          • 2.6.3 命令行环境变量速查
        • 2.7 QML 引擎底层原理
          • 2.7.1 QQmlEngine 启动流程
          • 2.7.2 QQmlComponent 的对象树构造
          • 2.7.3 V4 JIT 与 QML 编译缓存
          • 2.7.4 Scene Graph 的 QSGNode 树
        • 2.8 性能优化实践
          • 2.8.1 减少 Draw Call
          • 2.8.2 异步加载与对象池
          • 2.8.3 绑定与 JS 优化
          • 2.8.4 综合案例:500 Item ListView 优化
        • 2.9 本章新手陷阱 Top 5
        • 2.10 训练题
        • 2.11 综合思考题
        • 2.12 速查表
      • QML语法与类型系统
      • 属性绑定与响应式原理
      • 可视元素与布局原理
      • 事件处理与传播机制
      • 模型视图架构原理
      • 动画与状态机原理
      • Canvas与自定义渲染
      • QML与C++集成原理
      • 自定义SceneGraph节点
      • 交叉编译与部署
      • 嵌入式渲染后端
      • 性能优化与真机调试
    • QT核心库实践

  • IoT智能硬件开发

  • Apps
  • Linux应用开发
  • QML基础入门
杨充
2025-06-24
目录

QML引擎与渲染原理

# 第 2 章 QML 引擎与渲染原理

本章定位:承上启下——上一章你已经知道"QML 是嵌入式 GUI 的最优解",本章解答"它为什么快,又为什么会突然变慢"。前者是 QQmlEngine + V4 + Scene Graph 三大引擎的协作;后者是 Draw Call、批处理、双线程模型这些底层概念。理解本章,才能在 ARM 板子上写出 60fps 不掉帧的界面。

# 目录介绍

  • 2.1 案例引入
    • 2.1.1 一个让人困惑的性能现象
    • 2.1.2 揭开谜底:批处理被打断
    • 2.1.3 本章要回答的三个问题
  • 2.2 QML 引擎到底做了什么
    • 2.2.1 三大引擎角色分工
    • 2.2.2 从 QML 文本到 C++ 对象树
    • 2.2.3 对象树 vs 渲染树
    • 2.2.4 属性绑定的依赖图机制
    • 2.2.5 编译时 vs 运行时处理
    • 2.2.6 综合案例与思考
  • 2.3 Scene Graph 渲染管线
    • 2.3.1 Scene Graph 是什么
    • 2.3.2 渲染线程的四阶段循环
    • 2.3.3 批处理渲染:性能命门
    • 2.3.4 Draw Call 的真实代价
    • 2.3.5 综合案例与思考
  • 2.4 渲染线程与 GUI 线程
    • 2.4.1 双线程模型总览
    • 2.4.2 同步阶段如何工作
    • 2.4.3 阻塞场景的实测对比
    • 2.4.4 三种渲染循环策略
  • 2.5 嵌入式渲染后端
    • 2.5.1 四种渲染后端对比
    • 2.5.2 EGLFS 运行原理
    • 2.5.3 软件渲染降级策略
    • 2.5.4 选型决策树
  • 2.6 性能诊断工具
    • 2.6.1 QSG_VISUALIZE 可视化调试
    • 2.6.2 Qt Quick Profiler
    • 2.6.3 命令行环境变量速查
  • 2.7 QML 引擎底层原理
    • 2.7.1 QQmlEngine 启动流程
    • 2.7.2 QQmlComponent 的对象树构造
    • 2.7.3 V4 JIT 与 QML 编译缓存
    • 2.7.4 Scene Graph 的 QSGNode 树
  • 2.8 性能优化实践
    • 2.8.1 减少 Draw Call
    • 2.8.2 异步加载与对象池
    • 2.8.3 绑定与 JS 优化
    • 2.8.4 综合案例:500 Item ListView 优化
  • 2.9 本章新手陷阱 Top 5
  • 2.10 训练题
  • 2.11 综合思考题
  • 2.12 速查表

# 2.1 案例引入

# 2.1.1 一个让人困惑的性能现象

某工程师在 ARM 板(Cortex-A53 + Mali-G31)上写了下面这段 ListView:

import QtQuick

ListView {
    width: 800; height: 480
    model: 500
    delegate: Rectangle {
        width: 100; height: 100
        color: "blue"
        Rectangle {
            anchors.centerIn: parent
            width: 50; height: 50
            color: "red"
            rotation: 45              // ⚠️ 罪魁祸首在这里
        }
    }
}

测得现象:

指标 数据
FPS 滑动到第 50 个 Item 后从 60 掉到 22
CPU 占用 90% +(单核)
GPU 占用 仅 28%
Draw Call/帧 约 1000 次

疑惑:

  • 不是 GPU 瓶颈——为什么还卡?
  • QML 不是号称"硬件加速"吗?
  • 问题到底出在 QML 引擎还是 Scene Graph?

# 2.1.2 揭开谜底:批处理被打断

每个 delegate 内部那个 rotation: 45 是罪魁祸首。Scene Graph 的批处理渲染要求相同材质 + 相同状态才能合并 Draw Call。rotation: 45 修改了 transform 矩阵,每个旋转 Rectangle 都打断了批处理:

不加 rotation(同材质组):
  Draw Call 1: 一次画 500 个蓝色 Rectangle  ✓
  Draw Call 2: 一次画 500 个红色 Rectangle  ✓
  总共 2 个 Draw Call → CPU 几乎不动,GPU 满载

加了 rotation(每个红色 Rect 各自有不同 transform):
  Draw Call 1: 画蓝色 Rect 1
  Draw Call 2: 画红色旋转 Rect 1   ← 状态切换打断
  Draw Call 3: 画蓝色 Rect 2
  Draw Call 4: 画红色旋转 Rect 2   ← 又打断
  ... 重复 500 次
  总共 ~1000 个 Draw Call → CPU 提交命令爆炸

所以,"GPU 利用率只有 28%"不是 GPU 慢——是 CPU 根本没能把命令喂到 GPU。

# 2.1.3 本章要回答的三个问题

问题 在哪节回答
QML 文本是怎样变成屏幕上的像素的? §2.2 + §2.7
为什么 rotation: 45 会让性能崩掉? §2.3 批处理
嵌入式板子上如何稳定跑 60fps? §2.5 后端 + §2.8 优化

读完本章你会亲手用 QSG_VISUALIZE=batches 看见批处理分组,并把 1000 个 Draw Call 降到 5 个。


# 2.2 QML 引擎到底做了什么

# 2.2.1 三大引擎角色分工

Qt Quick 看似是一个"QML 引擎",其实是三个独立引擎协作:

引擎 全名 职责 线程归属
QQmlEngine QML 解析与对象管理引擎 QML 文本 → AST → C++ 对象树;模块导入;绑定求值调度 GUI 线程
V4 内嵌 JavaScript 引擎 执行 QML 内的 JS 表达式与函数;JIT 编译热点代码 GUI 线程
Scene Graph GPU 渲染引擎 QSGNode 树管理;批处理;OpenGL/Vulkan 调用 独立的渲染线程

这就是为什么 QML 同时有"声明式属性绑定"(QQmlEngine 负责)、"内嵌 JavaScript"(V4 负责)、"硬件加速渲染"(Scene Graph 负责)——三件事由三个引擎分头干。

# 2.2.2 从 QML 文本到 C++ 对象树

QML 不是"解释执行的 HTML"——它在加载时被编译为 C++ 对象树。整个过程三步:

Step 1: 词法/语法分析(QQmlEngine)
  QML 文本 → Token 流 → AST(抽象语法树)

  例: Rectangle { width: 100; color: "red" }
      ↓
  AST:
    ObjectLiteral(type="Rectangle")
      ├── PropertyBinding(name="width",  value=100)
      └── PropertyBinding(name="color",  value="red")

Step 2: 对象实例化(QQmlComponent)
  AST → 创建真实的 C++ QObject 对象树
  Rectangle  → new QQuickRectangle()
  width: 100 → obj->setProperty("width", 100)

  ⚠️ 关键:每个 QML 元素的背后都是一个真实的 C++ 类
  Rectangle  → QQuickRectangle
  Text       → QQuickText
  ListView   → QQuickListView
  MouseArea  → QQuickMouseArea

Step 3: 属性绑定求值(QQmlBinding)
  建立"依赖图"——记下每个绑定依赖哪些属性
  当依赖变化 → 自动重新求值

# 2.2.3 对象树 vs 渲染树

很多新手会以为"我看到的 QML 树 = GPU 画的树",这是错的。Qt Quick 内部维护两棵树:

QML 对象树(C++ QObject Tree,逻辑层):
  Window
    └── Rectangle (id: root)
         ├── Text     (id: title)
         ├── MouseArea (id: clickArea)   ← 看不到,但在对象树里
         └── Rectangle (id: button)
              └── Text (id: btnText)

Scene Graph 渲染树(QSGNode Tree,渲染层):
  WindowNode
    └── RectangleNode  (root)
         ├── TextNode  (title)
         └── RectangleNode (button)
              └── TextNode (btnText)
                                              ↑
              MouseArea 没有视觉表现 → 不进入渲染树
维度 QML 对象树 Scene Graph 渲染树
类型 QObject 派生 QSGNode 派生
含什么 属性、信号、绑定、JS 上下文 顶点缓冲、材质、变换矩阵
含 MouseArea 等无视觉元素 ✅ 有 ❌ 没有
所在线程 GUI 线程 渲染线程
谁创建 QQmlEngine Scene Graph 通过 updatePaintNode()

核心要点:对象树管"逻辑",渲染树管"GPU 命令"——这是 Qt Quick 性能远超 Qt Widget 的根本原因。

# 2.2.4 属性绑定的依赖图机制

QML 里写 width: parent.width / 2 不是"赋一次值",而是注册了一个绑定:

Rectangle {
    id: child
    width: parent.width / 2     // 依赖 parent.width
    height: child.width         // 依赖 child.width
    color: pressed ? "red" : "blue"  // 依赖 pressed
}

QQmlEngine 内部维护的依赖图大致如下:

parent.width  ──── 变化时 ────→ child.width 绑定求值
                                       │
                                       ▼
child.width   ──── 变化时 ────→ child.height 绑定求值

pressed       ──── 变化时 ────→ child.color 绑定求值

每个被依赖的属性都持有一份"反向通知列表",被修改时通知所有依赖者重新求值。这就是为什么 QML 的"声明式"看起来这么神奇——本质是一个事件驱动的依赖图。

// 等价的 C++ 信号槽伪代码:
connect(parent, &QQuickItem::widthChanged, child, [child](){
    child->setWidth(child->parent()->width() / 2);
});
connect(child,  &QQuickItem::widthChanged, child, [child](){
    child->setHeight(child->width());
});
connect(child,  &QQuickItem::pressedChanged, child, [child](){
    child->setColor(child->pressed() ? "red" : "blue");
});

设计哲学:QML 帮你写好了这些 connect——这就是"声明式 UI"的本质。

# 2.2.5 编译时 vs 运行时处理

import QtQuick               // ── 编译时 ──:模块导入解析到 C++ 类型
Item {                        // ── 编译时 ──:类型查找(必须存在 QQuickItem)
    width: 100                // ── 编译时 ──:属性名校验(width 是 Item 合法属性吗)
    
    property int counter: 0   // ── 运行时 ──:用户动态属性
    Component.onCompleted: {  // ── 运行时 ──:V4 JIT 执行 JS
        for (let i = 0; i < 10; ++i) counter++
    }
    
    Loader {
        source: "Page.qml"    // ── 运行时 ──:动态加载、再走一遍 §2.2.2 三步
    }
}
在哪个阶段 处理什么 出错时
编译时 模块解析、类型查找、属性名校验、信号匹配 启动时报错 / qmlcachegen 报错
运行时 绑定求值、JS 执行、Loader 加载、动态对象创建 控制台 warning / 运行时 throw

Qt 5.15+ 提供 qmlcachegen 工具,可以把 QML 提前编译为字节码(.qmlc),启动速度提升 20%~50%——这就是把"运行时"工作前移到"构建时"的典型例子。

# 2.2.6 综合案例与思考

下面这个最小工程,把"QML 文本 → C++ 对象树"这条路径亲手走通——主程序用 C++ 启动 QML,并演示如何从 C++ 反向访问 QML 创建出来的 QObject:

// main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQuickItem>
#include <QDebug>

int main(int argc, char* argv[]) {
    QGuiApplication app(argc, argv);
    
    QQmlApplicationEngine engine;                 // ← QQmlEngine 子类
    engine.load(QUrl("qrc:/main.qml"));
    if (engine.rootObjects().isEmpty()) return -1;
    
    // 反向访问 QML 创建的对象树
    QObject* root = engine.rootObjects().first();    // 顶层 Window
    QObject* rect = root->findChild<QObject*>("myRect"); // 按 objectName 找
    if (rect) {
        qDebug() << "QML 的 Rectangle 其实是 C++ 对象:" << rect;
        qDebug() << "类型:" << rect->metaObject()->className();
        qDebug() << "width:" << rect->property("width").toReal();
    }
    return app.exec();
}
// main.qml
import QtQuick
import QtQuick.Window
Window {
    visible: true; width: 400; height: 300; title: "obj-tree-demo"
    Rectangle {
        objectName: "myRect"        // ← 给 C++ 一个抓手
        anchors.fill: parent
        color: "lightblue"
        Text { anchors.centerIn: parent; text: "Hello QML" }
    }
}

控制台典型输出:

QML 的 Rectangle 其实是 C++ 对象: QQuickRectangle(0x55c4...)
类型: QQuickRectangle
width: 400

案例知识融合:本案例把 §2.2 的三个核心点串起来:①QML 加载流程(engine.load)走完了"文本 → AST → 对象树"三步;②每个 QML 元素背后都是真实 C++ 类(QQuickRectangle);③ C++ 可以通过 findChild + objectName 反向访问 QML 对象——这就是 QML/C++ 互操作的根基。

思考题:

  1. 如果把 qrc:/main.qml 写成不存在的路径,engine.load 会怎样?为什么 rootObjects() 是空集合而不是 throw?
  2. rect->property("width") 返回的是 QVariant——为什么 QML 属性必须通过这种"动态类型"接口暴露?它和直接调 QQuickRectangle::width() 比有什么代价?
  3. 把 objectName 改成 id: myRect 后用 findChild 还找得到吗?为什么 id 不能跨 C++/QML 边界?

# 2.3 Scene Graph 渲染管线

# 2.3.1 Scene Graph 是什么

Scene Graph 是 Qt Quick 的GPU 渲染引擎——也是 Qt Quick 相对 Qt Widget 性能优势的根本来源。

没有 Scene Graph(Qt Widget):
  每个 Widget 在自己的 paintEvent() 里用 QPainter 画
  → CPU 提交各自的绘制命令,没有批处理
  → Draw Call 数 = 控件数(爆炸)

有 Scene Graph(Qt Quick):
  QML 对象树 ──同步──→ Scene Graph 节点树
                              │
                              ▼
                       遍历 + 排序 + 批处理
                              │
                              ▼
                       生成最优 GPU 命令
                              │
                              ▼
              提交到 OpenGL ES / Vulkan / Metal / D3D11

# 2.3.2 渲染线程的四阶段循环

渲染线程独立于 GUI 线程,每帧(≤16.6ms@60Hz)循环执行四个阶段:

┌──────────────────────────────────────────────────┐
│ Phase 1: 同步(Synchronization)                 │
│  GUI Thread 临时停摆,渲染线程把"变化"拷过来      │
│  仅同步 dirty 节点(不是整棵树)                  │
└────────────────────┬─────────────────────────────┘
                     │
                     ▼
┌──────────────────────────────────────────────────┐
│ Phase 2: 预处理(Preprocessing)                  │
│  为 Layer / ShaderEffect / opacity<1 的组合       │
│  分配 FBO(离屏帧缓冲区)                          │
└────────────────────┬─────────────────────────────┘
                     │
                     ▼
┌──────────────────────────────────────────────────┐
│ Phase 3: 渲染(Rendering)                        │
│  ① 遍历 QSGNode 树                                │
│  ② 按材质+纹理+blend 排序                          │
│  ③ 合并批次                                        │
│  ④ 生成 OpenGL/Vulkan 命令                         │
└────────────────────┬─────────────────────────────┘
                     │
                     ▼
┌──────────────────────────────────────────────────┐
│ Phase 4: 交换缓冲区(Swap)                       │
│  eglSwapBuffers() / vkQueuePresentKHR()           │
│  阻塞等待 VSync,然后显示新帧                       │
└──────────────────────────────────────────────────┘

关键细节:只有 Phase 1 会短暂阻塞 GUI 线程(典型 < 1ms),其余三阶段渲染线程独立工作——这就是 QML 即使 JS 卡顿,动画仍能继续插值的原因(动画是 Render Thread 自己驱动)。

# 2.3.3 批处理渲染:性能命门

没有批处理时,逐节点画:

// 伪代码:朴素遍历
for (auto* node : sceneGraph) {
    glUseProgram(node->shader);    // 切换着色器(慢)
    glBindTexture(node->texture);  // 绑定纹理(慢)
    glUniformMatrix4fv(node->transform);
    glDrawArrays(...);             // 一次画一个节点
}
// 1000 个节点 → 1000 × (切 shader + 切 texture + draw) = 极慢

Scene Graph 的批处理算法:

Step 1: 排序——按 (shader, texture, blendMode) 排
  原始: [圆 A, 文字 B, 圆 C, 矩形 D, 文字 E, 圆 F]
  排序: [圆 A, 圆 C, 圆 F | 矩形 D | 文字 B, 文字 E]

Step 2: 分组——相邻同状态节点合并到一个 batch
  Batch 1: [A, C, F]  ← 同 shader 同 texture
  Batch 2: [D]
  Batch 3: [B, E]

Step 3: 单批一次绘制
  glUseProgram(circle_shader);
  glDrawArrays(VBO_batch1, 0, A.verts + C.verts + F.verts);  // 一次画三个
  glUseProgram(rect_shader);
  glDrawArrays(VBO_batch2, ...);
  glUseProgram(text_shader);
  glDrawArrays(VBO_batch3, ...);
  
  ▶ 1000 个节点 → 3~5 个 Draw Call

回到 §2.1 的案例——为什么 rotation: 45 打断批处理?看下面这张"批处理条件矩阵":

节点属性 改变后是否打断批处理 原因
仅位置不同(x, y) ❌ 不打断 transform 是顶点属性,可批量
仅大小不同(width, height) ❌ 不打断 同上
color 不同 ❌ 不打断 color 是顶点属性
rotation、scale 不同 ✅ 打断 transform 矩阵不同
opacity < 1 不同 ✅ 打断 需要 alpha blend 状态
纹理(source)不同 ✅ 打断 必须 rebind texture
layer.enabled: true ✅ 打断 独立 FBO

铁律:在嵌入式上做 ListView 的 delegate,能不旋转就不旋转、能不 opacity 就不 opacity、能合并 image atlas 就合并——每一条都直接换成帧率。

# 2.3.4 Draw Call 的真实代价

为什么 Draw Call 这么贵?答案是它不只是一次函数调用——它经过 OpenGL driver → kernel → GPU 命令队列,过程中要做大量状态校验。

操作 典型耗时(量级) 主要开销
同 batch 内多画一个三角形 ~ 1 ns 仅多算几个顶点
一次 glDrawArrays 切批次 ~ 1-10 µs 用户态/内核态切换 + 状态校验
一次 glUseProgram(切 shader) ~ 10-50 µs shader 重链接 + uniform 重新上传
一次 glBindTexture(切纹理) ~ 5-20 µs 纹理元数据 + cache 失效
一次 eglSwapBuffers(VSync) ~ 16.6 ms 等待显示同步

每帧预算 16.6ms,Draw Call 上限大致只有 1000~2000 次——所以本章开头那个"1000 个 Draw Call/帧"实际是踩在悬崖边上的。

# 2.3.5 综合案例与思考

下面这个例子让你亲眼看见批处理被打断的过程。先看正例(同色矩阵):

// good.qml ── 同材质矩阵,1 个 Draw Call
import QtQuick
Window {
    width: 800; height: 600; visible: true
    Grid {
        rows: 20; columns: 20
        Repeater {
            model: 400
            Rectangle { width: 40; height: 40; color: "blue" }  // 同色!
        }
    }
}

跑:

export QSG_VISUALIZE=batches    # 显示批处理分组(每色一组)
./demo good.qml                 # 整张图 1 种颜色 → 1 个 batch

再看反例:

// bad.qml ── 每个 Rect 不同颜色 + 旋转 → 400 个 Draw Call
Repeater {
    model: 400
    Rectangle {
        width: 40; height: 40
        color: Qt.rgba(Math.random(), Math.random(), Math.random(), 1)
        rotation: Math.random() * 360     // ⚠️ 每个不同 → 打断
    }
}

QSG_VISUALIZE=batches 下你会看到屏幕被涂成 400 种不同色块——每种色代表一个独立批次。

案例知识融合:本案例验证了批处理的两个核心铁律:①相同 shader + 相同 texture + 相同 transform 矩阵 = 同 batch;②rotation、opacity、独立 color 都会打断 batch。QSG_VISUALIZE 是嵌入式调试时必装的"眼睛"——它把抽象的"批处理"变成可看见的色块。

思考题:

  1. 上面 bad.qml 如果保留 rotation 但所有 Rect 用同一颜色,Draw Call 数会下降吗?为什么?
  2. 同样的 400 个 Rectangle,分别用 Repeater 和 Grid + Repeater 和 ListView 实现,渲染开销会一致吗?为什么 ListView 反而可能更快?
  3. 如果把 400 个 Rect 改成 400 个 Image(同一图片),还能批处理吗?(提示:Image Atlas 机制)

# 2.4 渲染线程与 GUI 线程

# 2.4.1 双线程模型总览

┌──────────────────────────┐       ┌──────────────────────────┐
│      GUI Thread           │       │     Render Thread         │
│      (主线程)              │  ←──→ │     (独立线程)             │
│                           │ 同步  │                           │
│  · 事件分发                │ 阶段  │  · Scene Graph 遍历        │
│  · 属性更新                │       │  · 排序 + 批处理            │
│  · 绑定求值                │       │  · OpenGL/Vulkan 调用      │
│  · V4 JavaScript           │       │  · eglSwapBuffers          │
│  · QQuickItem 树管理       │       │  · 动画插值(驱动)          │
│                           │       │                           │
│   ~ 几千 Hz 事件处理        │       │   60 / 90 / 120 Hz         │
└──────────────────────────┘       └──────────────────────────┘
            ↑                                    ↑
            │                                    │
    QObject Tree (共享)                  QSGNode Tree (共享)
    GUI 线程独占写                       Render 线程独占写
    Sync 阶段做一次拷贝同步

为什么要分两个线程?答案藏在嵌入式场景里:

场景 单线程会怎样 双线程怎样
用户拖动滑块,JS 计算 100ms 这 100ms 内完全不刷新 渲染仍跑,动画继续
加载大图,JS 阻塞 屏幕黑屏 最后一帧持续显示
GPU 绘制慢(fillrate 不足) GUI 线程也被拖死 GUI 线程继续接受输入

# 2.4.2 同步阶段如何工作

// 渲染线程伪代码(每帧调用一次)
void RenderLoop::frame() {
    // Phase 1: 同步——这一段会临时停 GUI Thread
    guiThread.lockForSync();
    {
        for (QQuickItem* item : dirtyItems) {
            QSGNode* node = item->updatePaintNode(...);  // 在 GUI 线程内执行
            // ↑ 由 QML 元素自己把"逻辑变化"翻译成"GPU 节点变化"
        }
    }
    guiThread.unlock();
    
    // Phase 2~4: 渲染线程独立工作,GUI 线程继续响应事件
    preprocess();
    renderSceneGraph();
    swapBuffers();
}

关键不变量:同步阶段(Phase 1)是唯一可以同时访问对象树和节点树的时刻;其余时间 GUI 线程只能写对象树、渲染线程只能写节点树。这种"读写分离"避免了昂贵的细粒度锁。

# 2.4.3 阻塞场景的实测对比

把下面两段代码放到 Button.onClicked 里跑:

// 场景 1: 阻塞 GUI 线程 —— 同步算法
Timer {
    repeat: true; interval: 16; running: true
    onTriggered: {
        // 用户点了按钮后 1 秒
        const t0 = Date.now()
        while (Date.now() - t0 < 1000) { /* 空转 1 秒 */ }
    }
}

测得:

GUI 线程: 100% 跑满,按钮按下后 1s 没响应(事件队列堆积)
Render 线程:仍以 60 fps 渲染最后一帧的内容
动画(NumberAnimation):照常播放(V4 是 GUI 线程,但动画驱动在 Render 线程)❓

⚠️ 注意:纯 JS 表达式驱动的属性变化(如 onTriggered 里改 width)
        在 GUI 线程被阻塞期间不会发生——因此"看上去动画在跑,
        但绑定不更新"的场景会出现
// 场景 2: 让重活下沉到 WorkerScript / 线程池
WorkerScript {
    id: worker
    source: "heavy.mjs"      // ── 在独立线程跑 JS
    onMessage: (msg) => result.text = msg.data
}
Button { onClicked: worker.sendMessage({ payload: input.text }) }

测得:GUI 线程几乎零阻塞,动画与绑定全程流畅。

# 2.4.4 三种渲染循环策略

Qt Quick 支持三种渲染循环(通过 QSG_RENDER_LOOP 环境变量选择):

策略 线程模型 适用场景 在嵌入式上
threaded GUI + 独立 Render 线程 双核以上、有 GPU ✅ 推荐(默认)
basic 单线程(GUI 内做渲染) 单核、调试 ⚠️ 性能差但可用
windows(Win 专属) 单线程 Windows 兼容 —

强制切换:

export QSG_RENDER_LOOP=basic        # 单线程,便于断点调试
export QSG_RENDER_LOOP=threaded     # 双线程,性能模式
./myapp -platform eglfs

嵌入式默认选 threaded——但在调试卡顿时改成 basic 能让"哪条 QML 语句导致渲染慢"的因果更清晰。


# 2.5 嵌入式渲染后端

# 2.5.1 四种渲染后端对比

后端 路径 启动速度 性能 适用平台
EGLFS OpenGL ES → DRM/KMS / fbdev 极快 ★★★★★ ARM Linux 无桌面(嵌入式首选)
LinuxFB QPainter → /dev/fb0 极快 ★★ 无 GPU 的廉价板
XCB X11 协议 慢 ★★★★ 桌面 Linux 开发调试
Wayland Wayland 协议 中等 ★★★★ 现代桌面 / 多窗口嵌入式
Offscreen 不输出 — — CI 自动化测试

# 2.5.2 EGLFS 运行原理

EGLFS = EGL + FullScreen:一个 QML 进程独占一个屏幕,不需要任何窗口管理器。

┌─────────────────────────────────────┐
│        QML 应用进程                  │
├─────────────────────────────────────┤
│        Qt Quick (Scene Graph)        │
├─────────────────────────────────────┤
│        QPA: eglfs 插件                │
│  ┌─────────────────────────────────┐ │
│  │ 1. eglGetDisplay(EGL_DEFAULT)   │ │  ← 打开 GPU
│  │ 2. eglInitialize / Config       │ │
│  │ 3. eglCreateWindowSurface       │ │  ← 绑定到 framebuffer
│  │      (target = DRM plane /        │ │
│  │       fbdev /dev/fb0)            │ │
│  │ 4. eglMakeCurrent                │ │
│  │ 5. glDraw* + eglSwapBuffers      │ │  ← 直接画到屏幕
│  └─────────────────────────────────┘ │
├─────────────────────────────────────┤
│        Linux Kernel                  │
│  ┌─────────────────────────────────┐ │
│  │  DRM / KMS  /  fbdev driver      │ │
│  │  → GPU → HDMI/LVDS/MIPI 输出     │ │
│  └─────────────────────────────────┘ │
└─────────────────────────────────────┘

EGLFS 的特点:

维度 说明
独占显示 一次只有一个 EGLFS 进程能用屏幕(除非用 eglfs_kms 多 plane)
无窗口管理器 没有标题栏、关闭按钮、最小化——节省所有开销
启动速度 < 1 秒(X11 + GNOME 起步 5 秒)
输入设备 直接读 /dev/input/event*(evdev)
多屏支持 QT_QPA_EGLFS_KMS_CONFIG 配置 JSON

启动命令:

export QT_QPA_PLATFORM=eglfs
export QT_QPA_EGLFS_HIDECURSOR=1           # 隐藏鼠标
export QT_QPA_EGLFS_PHYSICAL_WIDTH=155     # 物理尺寸(mm),决定 DPI
export QT_QPA_EGLFS_PHYSICAL_HEIGHT=86
./myapp

# 2.5.3 软件渲染降级策略

不是所有嵌入式板都有 GPU——比如 STM32MP1、低端 Allwinner H 系列。此时 Qt Quick 仍能跑,但走"软件渲染后端":

// main.cpp 启动前调用
#include <QQuickWindow>

int main(int argc, char* argv[]) {
    // 无 GPU 时回退到软件渲染
    QQuickWindow::setSceneGraphBackend(QSGRendererInterface::Software);
    
    QGuiApplication app(argc, argv);
    QQmlApplicationEngine engine;
    engine.load(QUrl("qrc:/main.qml"));
    return app.exec();
}

软件后端的特点:

  • 用 QPainter(CPU 光栅化)模拟 OpenGL 调用
  • 没有 Scene Graph 的 GPU 批处理优化
  • 性能比 GPU 后端慢 5~20 倍,但能跑
  • 不支持 ShaderEffect、ParticleSystem 等需要 GPU 的元素

一条经验:512×320 以下的低分屏 + 简单 UI(按钮、文本、列表),软件后端勉强够用;任何动画密集或全屏视频场景,必须有 GPU。

# 2.5.4 选型决策树

                   ┌── 板子有 GPU 吗?
                   │
        ┌──────────┴──────────┐
       有 GPU                无 GPU
        │                       │
   ┌────┴────┐               ┌──┴──┐
 有桌面?    无桌面?         屏幕 < 800×480?
   │           │               │
  XCB        EGLFS         ┌──┴──┐
  Wayland   (推荐)       是      否
                          │       │
                       LinuxFB   放弃 QML
                       软件渲染  考虑 LVGL

# 2.6 性能诊断工具

# 2.6.1 QSG_VISUALIZE 可视化调试

QSG_VISUALIZE 是 Scene Graph 内置的"X 光眼",把抽象的渲染过程画成可见的色块。

# 显示批处理分组——每色 = 一个 batch
export QSG_VISUALIZE=batches

# 显示 overdraw(过度绘制)热力图——颜色越红越糟
export QSG_VISUALIZE=overdraw

# 显示脏区域——红色 = 本帧需要重绘的部分
export QSG_VISUALIZE=changes

# 显示裁剪区域——绿色框 = 被裁剪掉的内容
export QSG_VISUALIZE=clip

./myapp -platform eglfs
模式 看什么 优化目标
batches 色块越少越好(同色 = 同 batch) 减少 Draw Call
overdraw 红色越少越好(红 = 多次重绘) 减少层叠
changes 红色越少越好(红 = 整帧重画) 减少绑定更新
clip 检查 clip: true 用得对不对 该裁的裁,不该裁的别裁

# 2.6.2 Qt Quick Profiler

Qt Creator → Analyze → QML Profiler
连接到运行中的进程(也可启动时挂上)
可视化时间线包含:
├── Painting           ← 渲染时间,应 < 16.6ms
├── Compiling          ← QML 编译时间(仅启动期)
├── Creating           ← 对象创建时间(Loader / Component.createObject)
├── Binding            ← 绑定求值时间
├── HandlingSignal     ← 信号处理时间
└── JavaScript         ← V4 执行时间

抓一帧后展开"Painting" 通常能直接定位是哪个 Item 拖累了 GPU。

# 2.6.3 命令行环境变量速查

# ===== Scene Graph =====
export QSG_RENDER_LOOP=threaded     # 渲染循环模式
export QSG_VISUALIZE=batches        # 可视化模式
export QSG_INFO=1                   # 打印 Scene Graph 启动信息

# ===== 性能日志 =====
export QSG_RENDERER_DEBUG=render    # 渲染细节
export QML_DISABLE_DISK_CACHE=1     # 禁用 qmlcache(调试用)

# ===== EGLFS =====
export QT_QPA_PLATFORM=eglfs
export QT_QPA_EGLFS_DEBUG=1         # 打印 EGLFS 初始化信息
export QT_QPA_EGLFS_HIDECURSOR=1    # 隐藏光标

# ===== 输入 =====
export QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS=rotate=180  # 触屏旋转

# ===== 启动时分析 =====
export QML_PROFILE_PIXMAP_CACHE=1   # 打印 PixmapCache 命中率

# 2.7 QML 引擎底层原理

这一节是"卷二"性质——前面 §2.2~§2.6 是"会用 QML"够了,本节回答"为什么这么设计"。

# 2.7.1 QQmlEngine 启动流程

QQmlApplicationEngine engine; 这一行代码背后究竟做了多少事?

// 简化版 QQmlEngine 启动流程(Qt 6.5)
QQmlEngine::QQmlEngine() {
    // 1. 创建 V4 JavaScript 引擎(每个 QQmlEngine 一个 V4)
    m_v4Engine = new QV4::ExecutionEngine();
    
    // 2. 注册内置类型
    qmlRegisterType<QQuickRectangle>("QtQuick", 2, 15, "Rectangle");
    qmlRegisterType<QQuickText>     ("QtQuick", 2, 15, "Text");
    // ... 数十个内置类型
    
    // 3. 初始化模块搜索路径
    m_importPaths << "qrc:/qt-project.org/imports"
                  << "/usr/lib/qt6/qml"
                  << QDir::currentPath();
    
    // 4. 创建网络访问管理器(用于 http:// QML 加载)
    m_networkAccessManager = new QNetworkAccessManager(this);
    
    // 5. 启用 qmlcache(如果存在)
    QQmlEnginePrivate::enableDiskCache(this);
}

这就是为什么"应用启动时 engine.load() 那一刻 CPU 占用突然飙高"——它做了大量解析、类型注册、模块查找的工作。优化手段是 qmlcachegen 预编译(见 §2.7.3)。

# 2.7.2 QQmlComponent 的对象树构造

engine.load(QUrl("qrc:/main.qml")) 内部的等价代码:

QQmlComponent component(&engine, QUrl("qrc:/main.qml"));
// ↑ 此时已经走完"Step 1: 词法/语法分析"
//   生成内部表示 QV4::CompiledData::CompilationUnit

QObject* root = component.create();
// ↑ "Step 2: 对象实例化"
//   遍历 CompilationUnit,对每个 ObjectLiteral:
//   ① 调用对应 C++ 类的默认构造器
//   ② 用 setProperty 注入属性
//   ③ 用 connect 注入信号处理器
//   ④ 建立绑定依赖图

if (!root) {
    qWarning() << component.errors();    // 错误回填到 component
}

编译后的内部表示 CompilationUnit 是 QML 的"字节码",大致结构:

CompilationUnit {
  objectTable: [
    { type: "QQuickRectangle", parent: -1, bindings: [0,1,2] },
    { type: "QQuickText",      parent: 0,  bindings: [3,4]   },
  ]
  bindingTable: [
    { property: "width",  value: 100 },
    { property: "color",  value: "red" },
    ...
  ]
  signalTable: [
    { signal: "clicked", handlerFunctionIndex: 0 },
  ]
  functionTable: [
    // V4 字节码,由 V4 JIT 编译为机器码
  ]
}

# 2.7.3 V4 JIT 与 QML 编译缓存

V4 是 Qt 自研的 JavaScript 引擎,带 JIT:

阶段 输入 输出 触发条件
Parser JS 源码 AST 加载时一次
Bytecode 编译 AST V4 字节码 加载时一次
JIT 编译 字节码 x86/ARM 机器码 函数被频繁调用(热点)

qmlcachegen 缓存机制:

# 构建时预编译——把 QML 提前编译为字节码
qmlcachegen --resource main.qrc -o main.qmlc

# 运行时跳过 parser,直接加载字节码
# 启动速度提升 20%~50%

缓存文件名规则:

main.qml          → ~/.cache/QtProject/qmlcache/main_xxxxxxxx.qmlc
SomeComponent.qml → ~/.cache/QtProject/qmlcache/SomeComponent_yyyy.qmlc

修改 QML 后缓存哈希变化、自动失效——所以开发期不需要担心缓存污染。

# 2.7.4 Scene Graph 的 QSGNode 树

每一个 QML 可视元素背后,最终都要变成一个或多个 QSGNode:

// QQuickItem 派生类暴露给 Scene Graph 的接口
class QQuickItem : public QObject {
protected:
    virtual QSGNode* updatePaintNode(QSGNode* oldNode,
                                     UpdatePaintNodeData* data);
    // ↑ 渲染线程在 Sync 阶段调用这个虚函数
    //   返回的 QSGNode 会挂到 Scene Graph 树上
};

// 比如 QQuickRectangle 大致这样实现:
QSGNode* QQuickRectangle::updatePaintNode(QSGNode* oldNode, ...) {
    QSGGeometryNode* node = static_cast<QSGGeometryNode*>(oldNode);
    if (!node) {
        node = new QSGGeometryNode();
        node->setGeometry(new QSGGeometry(...));
        node->setMaterial(new QSGFlatColorMaterial());
    }
    // 更新颜色、几何
    static_cast<QSGFlatColorMaterial*>(node->material())->setColor(m_color);
    node->markDirty(QSGNode::DirtyMaterial);
    return node;
}

QSGNode 的核心子类:

类 含义 例子
QSGGeometryNode 一组顶点+材质——核心绘制单元 Rectangle、Image
QSGTransformNode 变换矩阵节点 rotation / scale
QSGOpacityNode 透明度节点 opacity 属性
QSGClipNode 裁剪节点 clip: true
QSGRootNode 渲染树根 自动创建

设计精髓:Scene Graph 的 Node 是无状态的纯渲染描述——它不知道"自己是 Rectangle 还是 Text",只知道"我有这些顶点、用这个材质、套这个变换"。正是这种"统一抽象"让批处理算法能跨元素类型工作。


# 2.8 性能优化实践

# 2.8.1 减少 Draw Call

这是 §2.3 批处理铁律的实际落地。规则:

// ❌ 反例
Repeater {
    model: 100
    Image {
        source: "icon_" + index + ".png"   // 100 个不同纹理
        rotation: index * 3.6              // 100 个不同 transform
        opacity: 0.5 + index * 0.005       // 100 个不同 opacity
    }
}
// → 100+ Draw Call

// ✅ 正例
Repeater {
    model: 100
    Image {
        source: "icons_atlas.png"          // 同一张图集
        sourceClipRect: Qt.rect(
            (index % 10) * 32, (index / 10) * 32, 32, 32)   // 不同区域
        // 不旋转,不动 opacity
    }
}
// → 1 个 Draw Call

# 2.8.2 异步加载与对象池

异步加载 —— Loader 默认同步会卡 GUI 线程,改为异步:

Loader {
    source: "HeavyPage.qml"
    asynchronous: true               // ⚡ 在独立线程编译/创建
    visible: status === Loader.Ready
    
    onStatusChanged: {
        if (status === Loader.Error) console.log("加载失败")
    }
}

对象池 —— ListView 内置回收复用,但 Repeater 没有;大量动态创建用:

// ListView 的 reuseItems 是 Qt 5.15+ 的内置对象池
ListView {
    model: 10000
    reuseItems: true                 // ⚡ 滑动出去的 delegate 不销毁,复用
    delegate: MyDelegate { ... }
}

# 2.8.3 绑定与 JS 优化

铁律 1:避免每帧改属性

// ❌ 反例 ── 每帧重新求值整个绑定
Item {
    width: someFunc(x, y, z, w)   // someFunc 每次任一依赖变都重算
}

// ✅ 缓存中间值
Item {
    readonly property real ratio: someComputation()  // 仅依赖变时算一次
    width: parent.width * ratio
}

铁律 2:避免在绑定里 new 对象

// ❌ 每次求值都创建新 QRect ── GC 压力大
Item { clipRect: Qt.rect(x, y, w, h) }

// ✅ 仅在需要时创建
property rect cachedRect: Qt.rect(x, y, w, h)

铁律 3:JS 重活下放 WorkerScript

WorkerScript {
    id: hashWorker
    source: "compute.mjs"
    onMessage: (m) => result.text = m.hash
}
Button { onClicked: hashWorker.sendMessage({ data: input.text }) }
// compute.mjs ── 跑在独立线程
WorkerScript.onMessage = function(msg) {
    const hash = sha256(msg.data)        // 耗时计算
    WorkerScript.sendMessage({ hash })
}

# 2.8.4 综合案例:500 Item ListView 优化

把 §2.1 的反例案例一步步优化,看每一步的 FPS 变化:

// 版本 0(反例):1000 Draw Call,22 fps
ListView {
    model: 500
    delegate: Rectangle {
        width: 100; height: 100; color: "blue"
        Rectangle {
            anchors.centerIn: parent
            width: 50; height: 50; color: "red"; rotation: 45
        }
    }
}

优化 step 1:去掉 rotation → 5 Draw Call

delegate: Rectangle {
    width: 100; height: 100; color: "blue"
    Rectangle {
        anchors.centerIn: parent
        width: 50; height: 50; color: "red"
        // rotation 删了
    }
}
// FPS: 22 → 58

优化 step 2:开启 reuseItems → 内存占用 ÷ 3

ListView {
    reuseItems: true        // ⚡ Qt 5.15+
    cacheBuffer: 200        // 预渲染上下 200 像素
    model: 500
    ...
}
// FPS: 58 → 60(稳定)
// 内存: 500 个 delegate 实例 → 实际只创建 ~12 个并复用

优化 step 3:异步加载 delegate

delegate: Loader {
    asynchronous: true
    sourceComponent: ItemDelegate {}    // 大组件首次滑入时才加载
}
// 启动速度: 1.2s → 0.3s

优化 step 4:Image Atlas——如果 delegate 含图片,所有图打包到一张:

delegate: Image {
    source: "qrc:/atlas/items.png"
    sourceClipRect: Qt.rect((index%10)*64, (index/10)*64, 64, 64)
}
// Draw Call: 500 → 1(图片版本)

最终对比:

版本 Draw Call FPS 启动 内存
v0 反例 ~1000 22 1.2s 高
v1 去 rotation 5 58 1.2s 高
v2 + reuseItems 5 60 1.2s 低
v3 + asynchronous 5 60 0.3s 低
v4 + atlas(含图) 1 60 0.3s 低

案例知识融合:本案例把整个第 2 章的核心结论串起来——①减少 Draw Call(§2.3);②利用 reuseItems 走对象池(§2.8.2);③异步加载不阻塞 GUI(§2.4);④Image Atlas 把不同图变同 batch(§2.3.3)。这一组组合拳是嵌入式 QML 60 fps 的全部秘密。


# 2.9 本章新手陷阱 Top 5

# 陷阱 说明 修复
1 在 delegate 里随意加 rotation/opacity 每个 Item 各自打断批处理 能不动就不动;必须动用 layer.enabled 隔离
2 用 Loader 同步加载大组件 阻塞 GUI 线程,首屏白屏 加 asynchronous: true
3 在 onTriggered 写长时间循环 V4 在 GUI 线程,会卡死交互 用 WorkerScript 或拆 QTimer
4 clip: true 滥用 每个 clip 都加一个独立 batch + scissor 仅在内容确实超界时用
5 误用 Item { visible: false } 仍占用对象树和绑定计算 用 Loader 按需创建,或 active: false

# 2.10 训练题

训练题 1:用 QSG_VISUALIZE 验证批处理

要求:写两个 QML 文件,一个故意触发 100+ Draw Call,一个仅 1 个 Draw Call;分别用 QSG_VISUALIZE=batches 截图对比。

// bad.qml ── 100 个不同颜色 Rect,100 batches
import QtQuick
Window {
    width: 600; height: 600; visible: true
    Grid { columns: 10
        Repeater { model: 100
            Rectangle { width: 60; height: 60
                color: Qt.rgba(Math.random(), Math.random(), Math.random(), 1) }
        }
    }
}

// good.qml ── 100 个同色 Rect,1 batch
import QtQuick
Window {
    width: 600; height: 600; visible: true
    Grid { columns: 10
        Repeater { model: 100
            Rectangle { width: 60; height: 60; color: "steelblue" }
        }
    }
}

练习重点:用工具看见抽象的"batch"概念,建立"Draw Call = 性能"的肌肉记忆。


训练题 2:双线程模型实测

要求:构造一个故意阻塞 GUI 线程 2 秒的 onClicked,观察期间动画/Render 线程是否仍工作。

import QtQuick
Window {
    width: 400; height: 300; visible: true
    
    Rectangle {
        id: ball
        width: 50; height: 50; radius: 25; color: "tomato"
        NumberAnimation on x { from: 0; to: 350; duration: 2000; loops: Animation.Infinite }
        NumberAnimation on y { from: 0; to: 250; duration: 1500; loops: Animation.Infinite }
    }
    
    Rectangle {
        x: 150; y: 250; width: 100; height: 40; color: "lightgray"
        Text { anchors.centerIn: parent; text: "Block 2s" }
        MouseArea {
            anchors.fill: parent
            onClicked: {
                const t0 = Date.now()
                while (Date.now() - t0 < 2000) {}    // ⚠️ 阻塞 GUI 线程 2 秒
                console.log("done")
            }
        }
    }
}

观察点:

  • 点击按钮后,小球还在动吗?(动画在 Render 线程,还会动)
  • 点击后 2 秒内能再次点击按钮吗?(不能,事件被堵在 GUI 队列)
  • 控制台 done 什么时候打印?(2 秒后)

练习重点:亲手验证 GUI 线程和 Render 线程的独立性。


训练题 3:用 qmlcachegen 加速启动

要求:测量大型 QML 项目启动时间,再用 qmlcachegen 预编译,对比启动耗时。

# Step 1: 测原始启动时间
time ./myapp --quit-after-startup

# Step 2: 预编译
qmlcachegen --resource main.qrc -o main.qmlc

# Step 3: 部署 .qmlc 与原 QML 同目录,再次启动
time ./myapp --quit-after-startup

# 典型结果:1200ms → 700ms(提升 ~40%)

练习重点:理解"编译时 vs 运行时"的优化迁移——把运行时解析挪到构建时。


# 2.11 综合思考题

  1. QML 与 React 的本质区别:React 也是声明式 UI,也有"虚拟 DOM"做 diff 渲染。请对比 React 的"组件 → JSX → VDOM → 真实 DOM"流程与 QML 的"声明 → AST → QObject 树 → QSGNode 树"流程。它们在性能、内存、调试上各有什么优劣?

  2. 为什么不在 GUI 线程做渲染:理论上单线程串行执行更简单——绕过了所有同步问题。但 Qt 6 默认仍选了"GUI + Render 双线程"。从用户体验和硬件趋势两个角度论证这个决策是否正确?多核越来越普及对这个决策有什么影响?

  3. EGLFS 的独占性该不该消除:嵌入式上 EGLFS 默认独占整个屏幕,多个应用没法同时显示。Wayland 多窗口才解决这个问题。请思考:什么样的嵌入式产品形态适合 EGLFS 独占模型?什么样的更适合 Wayland 多客户端?车机、HMI、家电分别该选哪个?

  4. 批处理算法的极限:Scene Graph 的批处理是基于"同 shader + 同 texture + 同 transform"。但现代 GPU 引入了 Instanced Rendering 与 Bindless Texture——一次 Draw Call 就能画几千个独立 transform、独立纹理的物体。Qt 7 / Qt RHI 是否会引入这类优化?这会改变第 2.3.3 节的"批处理铁律"吗?


# 2.12 速查表

概念 一句话
QQmlEngine QML 解析 + 模块管理 + 绑定调度
QQmlComponent AST → QObject 对象树
V4 Qt 内嵌的 JavaScript 引擎(带 JIT)
QObject Tree 逻辑树(属性 / 信号 / 绑定)
Scene Graph GPU 渲染树(顶点 / 材质 / 变换)
QSGNode Scene Graph 的节点基类
GUI Thread 事件处理 / JS 执行 / 属性更新
Render Thread Scene Graph 遍历 / 批处理 / OpenGL 调用
Sync 阶段 两线程唯一同时访问对象树的时刻
EGLFS 嵌入式无窗口管理器的 OpenGL 后端
Draw Call 一次 GPU 绘制命令(越少越快)
Batch 同 shader + 同 texture + 同 transform 的节点组
vptr / vtable (C++ 多态机制)QML 元素本质是带 vptr 的 C++ 对象
qmlcachegen QML 预编译工具,启动速度 +20~50%
QSG_VISUALIZE Scene Graph 可视化调试环境变量

核心哲学:

QML 之所以快,不是因为"解释得快"——
而是它被"编译"为 C++ 对象树,
并由 Scene Graph 用 GPU 批处理渲染。

理解 Scene Graph 的批处理规则 = 等于掌握了 QML 性能的命门。

下一篇:03.QML 语法与类型系统

上次更新: 2026/06/25, 20:11:20
嵌入式GUI技术全景
QML语法与类型系统

← 嵌入式GUI技术全景 QML语法与类型系统→

最近更新
01
CSS选择器入门
06-23
02
CSS定位与层级
06-23
03
CSS盒模型详解
06-23
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式