文件与IO系统
# 第 3 章 文件与 IO 系统
Qt 的 IO 体系以
QIODevice为统一基类,不管是文件、网络套接字、还是内存缓冲区,都用同一套read()/write()接口。理解QDataStream的序列化版本机制、QSettings的跨平台存储策略、以及QFile::map()零拷贝读写——这是嵌入式读写大量日志和配置数据的命门。
# 目录介绍
- 3.1 IO 体系全景
- 3.2 QFile 文件读写
- 3.3 数据流详解
- 3.4 目录与文件信息
- 3.5 QSettings 配置存储
- 3.6 文件监控与临时文件
- 3.7 QIODevice 继承设计
- 3.8 嵌入式 IO 实战
- 3.9 综合案例
- 3.10 速查表
# 3.1 IO 体系全景
Qt 的 IO 类层次结构——统一接口、多种后端:
QIODevice ── 抽象基类(open/close/read/write/seek)
├── QFile → 磁盘文件
├── QTemporaryFile → 临时文件(自动命名、自动删除)
├── QProcess → 子进程的标准输入/输出
├── QBuffer → 内存中的 byte array
├── QTcpSocket → TCP 网络流
└── QLocalSocket → Unix Domain Socket
封装层:
QTextStream → QIODevice → 文本编码/解码(UTF-8/GBK/...)
QDataStream → QIODevice → 二进制序列化(平台无关格式)
关键设计:QFile file("data.bin"); QDataStream out(&file);——通过组合 QIODevice* 指针,一个 QDataStream 同时可以写文件、网络、或内存缓存,切换后端不需要改代码。
# 3.2 QFile 文件读写
# 3.2.1 基础读写
QFile file("/opt/app/config.ini");
if (!file.open(QIODevice::ReadWrite | QIODevice::Text)) {
qWarning() << file.errorString();
return;
}
QByteArray blob = file.read(4096); // 读 4KB
file.seek(0); // 回到文件开头
qint64 size = file.size(); // 获取文件大小
// 逐行读——大文件友好
while (!file.atEnd()) {
QByteArray line = file.readLine();
if (line.isEmpty()) continue;
}
file.write("key=value\n"); // 追加写入
file.flush(); // 强制刷盘
file.close();
最佳实践:嵌入式 eMMC 写入后必须 flush,否则断电丢数据。
# 3.2.2 内存映射
QFile::map() 把文件映射到进程地址空间——操作系统负责页缓存,零拷贝读取大文件:
QFile file("/var/log/sensor.log");
file.open(QIODevice::ReadOnly);
// 映射从 offset 0、长度 1MB
uchar* addr = file.map(0, 1024 * 1024);
if (addr) {
// addr[0]..addr[size-1] 就是文件内容——不需要 read() syscall
int lineCount = 0;
for (qint64 i = 0; i < file.size(); ++i)
if (addr[i] == '\n') ++lineCount;
file.unmap(addr);
}
// 只适合大文件顺序读取;小文件(< 64KB)直接 readAll 更简单
mmap 的内部原理:
┌─────────────────────────────────────────────────┐
│ 用户进程地址空间 │
│ ┌──────────────────────┐ │
│ │ addr = 0x7f800000 │ ← QFile::map() 返回 │
│ │ [文件内容映射区] │ │
│ │ addr[0] → 第1字节 │ │
│ │ addr[99]→ 第100字节 │ │
│ └──────────┬───────────┘ │
│ │ 页表映射(MMU) │
│ ┌──────────▼───────────┐ │
│ │ Page Cache (内核) │ ← 操作系统管理 │
│ │ ┌───┬───┬───┬───┐ │ │
│ │ │p0 │p1 │p2 │p3 │ │ 缺页时从磁盘加载 │
│ │ └───┴───┴───┴───┘ │ │
│ └──────────┬───────────┘ │
│ │ │
│ ┌──────────▼───────────┐ │
│ │ eMMC / SSD / HDD │ ← 物理存储 │
│ └──────────────────────┘ │
└─────────────────────────────────────────────────┘
对比:
read(4096) → user态↔kernel态拷贝 4KB → CPU L1 cache污染
mmap读 → 无需拷贝,直接访问 Page Cache → 零CPU拷贝
mmap 性能实测(100MB 日志文件顺序扫描,ARM Cortex-A53):
| 方式 | 耗时 | CPU 使用率 | 说明 |
|---|---|---|---|
read() 循环 | 820ms | 45% | 每次 read 触发一次 copy_to_user |
QFile::map() | 210ms | 12% | 零拷贝,仅首次缺页开销 |
readAll() | 980ms | 50% | 100MB 堆分配 + 拷贝 |
⚠️ mmap 陷阱:
- 不能映射比地址空间大的文件(32 位系统限制 ~3GB)
- 写入 mmap 区域后需要
msync确保持久化(或依赖 OS 回写) - 映射期间文件被
truncate→SIGBUS信号
# 3.2.3 QSaveFile 原子写入
问题:嵌入式设备在写入配置文件时突然断电 → 文件损坏(写了一半)。
原理:QSaveFile 先写临时文件 → commit() 原子 rename → 原文件永远完整:
QSaveFile file("/etc/app/config.json");
if (!file.open(QIODevice::WriteOnly)) {
qWarning() << "Cannot open:" << file.errorString();
return;
}
// ① 数据写入临时文件 /etc/app/config.json.XXXXXX
file.write(jsonData);
file.flush(); // fsync 到磁盘
// ② 调用 commit 执行原子 rename
// rename("/etc/app/config.json.XXXXXX", "/etc/app/config.json")
// POSIX 保证 rename 是原子操作——要么成功,要么旧文件还在
if (!file.commit()) {
qWarning() << "Commit failed:" << file.errorString();
// 临时文件自动删除
}
QSaveFile 内部状态机:
[构造] → open() → [Writing] → commit() → [Committed]
↘ cancelWriting() → 删除临时文件
为什么 rename 是原子的? 这是 POSIX 文件系统标准——inode 指针的原子交换,不涉及数据拷贝。即使断电,磁盘上的 inode 状态要么指向旧文件,要么指向新文件,不会出现"一半旧一半新"的半损坏状态。
# 3.2.4 QLockFile 文件锁
嵌入式设备上防止多实例启动:
QLockFile lockFile("/var/run/myapp.lock");
// 非阻塞尝试——如果已锁立即返回 false
if (!lockFile.tryLock(1000)) { // 等待 1 秒
qint64 pid;
QString host, app;
lockFile.getLockInfo(&pid, &host, &app);
qWarning() << "Another instance is running (PID:" << pid << ")";
return 1;
}
// 应用退出时自动解锁(析构 + unlink 锁文件)
// 如果进程崩溃 → 锁文件残留 → QLockFile 通过 pid 检测"僵尸锁"
// → staleLockTime 后自动清理
锁文件格式(/var/run/myapp.lock):
12345
myhostname
myapp
跨平台行为:
- Linux: 使用
flock()的排他锁 - Windows: 使用
LockFileEx() - 检测僵尸锁:检查 pid 进程是否存在
# 3.3 数据流详解
# 3.3.1 QTextStream
自动编码转换 + 格式化输出:
QFile file("report.txt");
file.open(QIODevice::WriteOnly);
QTextStream out(&file);
out.setEncoding(QStringConverter::Utf8);
out << qSetFieldWidth(10) << left << "Timestamp"
<< qSetFieldWidth(8) << "Value"
<< qSetFieldWidth(8) << "Unit" << Qt::endl;
out << qSetFieldWidth(10) << "12:30:00"
<< qSetFieldWidth(8) << qSetRealNumberPrecision(2) << 23.5
<< qSetFieldWidth(8) << "°C" << Qt::endl;
// 输出:
// Timestamp Value Unit
// 12:30:00 23.50 °C
# 3.3.2 QDataStream 序列化协议
QDataStream 按平台无关的二进制格式写入,自带版本标记和大小端控制:
// 写入——平台无关
QFile file("sensor.dat");
file.open(QIODevice::WriteOnly);
QDataStream out(&file);
out.setVersion(QDataStream::Qt_6_0); // 版本固定——保证跨平台兼容
out.setByteOrder(QDataStream::LittleEndian);
out << (qint32)42 << QString("温度传感器") << QDateTime::currentDateTime();
// 内部格式(简化):
// [4 bytes: qint32=42]
// [4 bytes: QString长度=7]
// [14 bytes: "温度传感器" (UTF-16)]
// [8 bytes: QDateTime 的 msecs]
版本兼容策略——如果部署到不同 Qt 版本的设备上:
QDataStream in(&file);
qint32 ver = in.version(); // 读出版本
qint32 id;
QString name;
QDateTime ts;
if (ver >= QDataStream::Qt_6_0) {
in >> id >> name >> ts; // Qt 6 格式
} else {
in.setVersion(ver);
quint16 len; in >> len; // Qt 5 QDateTime 多一个长度字段
in >> id >> name >> ts;
}
# 3.3.3 QDataStream 事务模式
Qt 5.7+ 引入了事务(Transaction)模式——解决不完整数据的问题:
QFile file("sensor.dat");
file.open(QIODevice::ReadOnly);
QDataStream in(&file);
while (!in.atEnd()) {
in.startTransaction(); // ← 标记回滚点
qint32 header;
QString body;
QDateTime ts;
in >> header >> body >> ts;
if (in.commitTransaction()) { // ← 全部成功 → 提交
processRecord(header, body, ts);
} else {
// 数据不完整或格式错误 → 自动回滚到 startTransaction 位置
// 等待更多数据到达(网络流场景)
break;
}
}
事务模式的内部实现:
startTransaction():
→ 保存当前 stream 位置 = markPosition
→ 设置 status = Ok
read 操作(如 in >> x):
→ 检测 status == Ok → 正常读取
→ 遇到 EOF/格式错 → status = ReadCorruptData
commitTransaction():
→ status == Ok → 丢弃回滚点 → return true
→ status != Ok → seek(markPosition) → return false
关键:所有读取操作都不修改原始缓冲——只在 commit 成功后"确认"位置
适用场景:
- 网络流:TCP 粘包 → 数据可能不完整 → 事务模式自动回滚等待
- 文件流:损坏文件的容错读取
- 不支持 seek 的设备(如串口):事务模式是唯一验证完整性的方式
# 3.3.4 QBuffer 内存流
不写磁盘——直接在内存中构建二进制包:
QByteArray buffer;
QBuffer io(&buffer);
io.open(QIODevice::WriteOnly);
QDataStream out(&io);
out << int32(0x48656C6F) << QString("Hello");
// 发送到网络
socket->write(buffer);
// 从网络包解析
QByteArray packet = socket->readAll();
QBuffer readBuf(&packet);
readBuf.open(QIODevice::ReadOnly);
QDataStream in(&readBuf);
qint32 header; QString msg;
in >> header >> msg;
# 3.4 目录与文件信息
// 遍历——按扩展名过滤
QDir dir("/var/log");
QStringList logFiles = dir.entryList({"*.log"}, QDir::Files, QDir::Time);
for (const auto& f : logFiles) {
QFileInfo info(dir.filePath(f));
qDebug() << f << info.size() / 1024 << "KB"
<< info.lastModified().toString("yyyy-MM-dd hh:mm");
}
// 文件信息——一行获取所有
QFileInfo info("/opt/app/config.xml");
info.exists(); info.isFile(); info.isDir();
info.size(); info.lastModified();
info.absolutePath(); info.fileName(); info.suffix();
# 3.5 QSettings 配置存储
嵌入式设备上保存配置——不需要自己设计文件格式:
// 写入——自动选择存储位置
QSettings settings("MyCompany", "DashboardApp");
settings.setValue("display/brightness", 85);
settings.setValue("network/host", "192.168.1.100");
settings.setValue("sensors/interval", 100); // ms
// 读取——带默认值
int brightness = settings.value("display/brightness", 50).toInt();
QString host = settings.value("network/host", "localhost").toString();
// 存储位置(Linux):
// /home/user/.config/MyCompany/DashboardApp.conf ← XDG 标准
// 或通过 QSettings::setPath 自定义到 /opt/config/
QSettings 跨平台存储策略:
| 平台 | 默认路径 |
|---|---|
| Linux | ~/.config/Organization/App.conf(INI 格式) |
| 嵌入式 Linux(root) | /etc/xdg/Organization/App.conf |
| Windows | 注册表 HKCU\Software\Organization\App |
# 3.6 文件监控与临时文件
// 文件监控——检测配置文件热更新
QFileSystemWatcher watcher;
watcher.addPath("/etc/dashboard/config.ini");
QObject::connect(&watcher, &QFileSystemWatcher::fileChanged,
[](const QString& path) {
qDebug() << "Config changed:" << path;
reloadConfig(); // 触发热重载
});
// 临时文件——自动命名 + 退出自动删除
QTemporaryFile tmp;
tmp.setFileTemplate("/tmp/sensor_XXXXXX.dat");
if (tmp.open()) {
tmp.write(processImage()); // 写入处理后的图像
QString path = tmp.fileName(); // 返回 /tmp/sensor_aB3f2X.dat
uploadToServer(path); // 上传后无需手动删除
} // tmp 析构时自动 unlink
# 3.6.1 QFileSystemWatcher 的内核实现
QFileSystemWatcher 在 Linux 上基于 inotify:
应用层: QFileSystemWatcher::fileChanged()
↓
Qt 内部: QInotifyFileSystemWatcherEngine
↓ syscall
内核层: inotify_add_watch(fd, "/etc/config.ini", IN_MODIFY | IN_DELETE_SELF)
→ 文件被修改 → 内核发送 inotify_event
→ Qt 事件循环 epoll_wait 检测到 inotify fd 可读
→ QSocketNotifier 投递事件 → emit fileChanged()
⚠️ 注意:被监控的文件如果被删除再重建(vim 保存方式),原 inotify watch 会失效——需要在 fileChanged 回调中重新 addPath。
# 3.6.2 QTemporaryFile 安全性
QTemporaryFile 使用 POSIX 的 mkstemp() 创建:
→ 文件名: template + 6位随机字符
→ 原子创建 + O_EXCL(已存在则重试)
→ 权限: 0600(仅当前用户可读写)
好处:防止临时文件竞态条件(TOCTOU)
即使攻击者猜到 /tmp/sensor_XXXXXX 模板
mkstemp 保证原子创建唯一文件
# 3.7 QIODevice 继承设计
Qt IO 的精髓在于统一抽象——下面的继承树展示了组合的力量:
QIODevice(抽象基类)
│ virtual qint64 readData(char*, qint64) = 0;
│ virtual qint64 writeData(const char*, qint64) = 0;
│
├── QFileDevice(文件系统设备)
│ ├── QFile → 普通文件
│ ├── QTemporaryFile → 临时文件
│ └── QSaveFile → 原子写入文件(Qt 5.1+)
│
├── QBuffer → 内存缓冲区
├── QProcess → 子进程 stdin/stdout
├── QAbstractSocket → 网络套接字
│ ├── QTcpSocket
│ ├── QUdpSocket
│ └── QSslSocket
└── QLocalSocket → Unix Domain Socket
继承 QIODevice 的最小实现——如果你需要自定义 IO 后端:
class RingBufferIO : public QIODevice {
Q_OBJECT
public:
explicit RingBufferIO(int capacity, QObject* parent = nullptr)
: QIODevice(parent), m_capacity(capacity), m_buffer(new char[capacity]) {}
bool isSequential() const override { return false; }
protected:
qint64 readData(char* data, qint64 maxSize) override {
qint64 available = (m_writePos - m_readPos + m_capacity) % m_capacity;
qint64 n = qMin(maxSize, available);
for (qint64 i = 0; i < n; ++i)
data[i] = m_buffer[(m_readPos + i) % m_capacity];
m_readPos = (m_readPos + n) % m_capacity;
return n;
}
qint64 writeData(const char* data, qint64 maxSize) override {
qint64 space = (m_readPos - m_writePos - 1 + m_capacity) % m_capacity;
qint64 n = qMin(maxSize, space);
for (qint64 i = 0; i < n; ++i)
m_buffer[(m_writePos + i) % m_capacity] = data[i];
m_writePos = (m_writePos + n) % m_capacity;
emit readyRead(); // QIODevice 标准信号
return n;
}
// QIODevice 自动提供:
// read(), readAll(), write(), peek(), seek(), pos(), size()...
// 你只需实现 readData + writeData!
private:
int m_capacity;
qint64 m_readPos = 0, m_writePos = 0;
QScopedArrayPointer<char> m_buffer;
};
QIODevice 自动处理的能力:
- 缓冲——
open()时分配内部缓冲区,减少 syscall 次数 - 编码——与
QTextStream组合自动处理字符集转换 - 信号——
readyRead()/bytesWritten()自动 emit - 错误处理——
errorString()统一错误信息接口
# 3.8 嵌入式 IO 实战
# 3.8.1 eMMC 磨损均衡与写入策略
嵌入式 eMMC 的擦写寿命通常 3000-10000 次/块——频繁写入配置文件很快就会损坏存储:
// ❌ 错误方式:每次传感器数据都写磁盘
void onSensorData(const QVector<float>& data) {
QFile file("/data/sensor.log");
file.open(QIODevice::WriteOnly | QIODevice::Append);
QTextStream out(&file);
out << QDateTime::currentDateTime().toString()
<< ", " << data[0] << Qt::endl; // 每秒 10 次 = 864000 次/天
file.close();
// → 3 个月内 eMMC 特定块可能损坏
}
// ✅ 正确方式 1:内存缓冲 + 批量刷盘
class SensorLogger : public QObject {
Q_OBJECT
public:
SensorLogger() {
m_flushTimer.setInterval(5000); // 5 秒刷一次盘
connect(&m_flushTimer, &QTimer::timeout, this, &SensorLogger::flush);
m_flushTimer.start();
}
void log(const QVector<float>& data) {
QTextStream stream(&m_buffer);
stream << QDateTime::currentDateTime().toString(Qt::ISODate)
<< ", " << data[0] << Qt::endl;
}
void flush() {
if (m_buffer.isEmpty()) return;
QSaveFile file("/data/sensor.log"); // 原子写入
if (file.open(QIODevice::WriteOnly | QIODevice::Append)) {
file.write(m_buffer);
file.commit();
}
m_buffer.clear();
}
private:
QByteArray m_buffer; // 内存缓冲
QTimer m_flushTimer; // 定时刷盘
};
// ✅ 正确方式 2:环形日志文件——限制写入量
class RingLogWriter {
static constexpr int MAX_FILES = 10;
static constexpr int MAX_SIZE = 1024 * 1024; // 1MB/文件
void write(const QByteArray& data) {
openCurrentFile();
m_file.write(data);
if (m_file.size() > MAX_SIZE) {
rotate(); // 轮转到下一个文件
}
}
void rotate() {
// sensor_9.log → 删除
// sensor_8.log → sensor_9.log
// ...
// sensor_0.log → sensor_1.log
// 新建 sensor_0.log
}
};
# 3.8.2 IO 性能对比(ARM Cortex-A53 + eMMC)
| 操作 | 方式 | 100KB 耗时 | 1MB 耗时 | 说明 |
|---|---|---|---|---|
| 读取 | QFile::readAll() | 0.8ms | 6.5ms | 一次 syscall |
| 读取 | QFile::read() 循环 | 1.2ms | 9.8ms | 多次 syscall |
| 读取 | QFile::map() | 0.3ms | 2.1ms | 零拷贝 |
| 写入 | QFile::write() | 1.5ms | 12ms | 写入 Page Cache |
| 写入 | QFile::write() + flush() | 15ms | 130ms | 强制刷盘(fsync) |
| 写入 | QSaveFile | 18ms | 145ms | fsync + rename |
| 写入 | QDataStream 序列化 | 1.8ms | 15ms | 序列化开销 ~0.3ms |
结论:嵌入式写入瓶颈在 fsync——尽量减少强制刷盘次数,用 QSaveFile 保证原子性。
# 3.8.3 文件路径可移植性
// ❌ 硬编码路径
// QFile file("C:\\Users\\admin\\config.ini"); // 只在 Windows 工作
// ❌ 手动拼接(分隔符不兼容)
// QString path = "/opt/app" + "/" + "config" + ".ini";
// ✅ 使用 QDir 或 QFileInfo 拼接
QString path = QDir("/opt/app").filePath("config.ini");
// 等价于: /opt/app/config.ini (Linux)
// C:\opt\app\config.ini (Windows)
// ✅ 使用 QStandardPaths 获取标准路径
QString configPath = QStandardPaths::writableLocation(
QStandardPaths::AppConfigLocation);
# 3.8.4 QStandardPaths 标准路径
嵌入式 Linux 上不要硬编码路径——使用 QStandardPaths:
// 数据目录
QString dataPath = QStandardPaths::writableLocation(
QStandardPaths::AppDataLocation); // ~/.local/share/<AppName>/
// 配置目录
QString configPath = QStandardPaths::writableLocation(
QStandardPaths::AppConfigLocation); // ~/.config/<AppName>/
// 缓存目录(可被系统清理)
QString cachePath = QStandardPaths::writableLocation(
QStandardPaths::CacheLocation); // ~/.cache/<AppName>/
// 运行时临时文件
QString tempPath = QStandardPaths::writableLocation(
QStandardPaths::TempLocation); // /tmp/
嵌入式自定义:
// 在 main() 最开始设置——覆盖默认路径
QStandardPaths::setTestModeEnabled(true);
// → 所有路径变成 /tmp/test_fake_home/...
// 适合单元测试和 CI 环境
# 3.9 综合案例
# 案例:嵌入式数据采集器的完整 IO 方案
需求:
1. 传感器数据每秒 20 条 → 缓冲 5 秒 → 批量写入
2. 配置文件热更新(监控 inotify)
3. 日志环形存储(最多 10 个文件 × 1MB)
4. 原子写入保证断电不丢配置
class DataCollector : public QObject {
Q_OBJECT
public:
DataCollector() {
// 1. 配置文件监控
m_watcher.addPath("/etc/collector/config.json");
connect(&m_watcher, &QFileSystemWatcher::fileChanged,
this, &DataCollector::reloadConfig);
// 2. 定时刷盘——5 秒批量写入
connect(&m_flushTimer, &QTimer::timeout, this, &DataCollector::flushData);
m_flushTimer.start(5000);
// 3. 退出前最后一次刷盘
connect(qApp, &QCoreApplication::aboutToQuit, this, &DataCollector::flushData);
}
void onSensorSample(const SensorSample& sample) {
// 写入内存缓冲(不触发磁盘 IO)
QDataStream stream(&m_dataBuffer, QIODevice::Append);
stream << sample.timestamp << sample.value << sample.channelId;
}
private:
void flushData() {
if (m_dataBuffer.isEmpty()) return;
// 环形日志文件
QSaveFile file(currentLogPath());
if (file.open(QIODevice::Append)) {
file.write(m_dataBuffer);
if (file.commit()) {
m_dataBuffer.clear();
checkRotation(); // 检查是否需要轮转
}
}
}
void reloadConfig(const QString& path) {
// 原子读取配置
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) return;
QByteArray data = file.readAll();
QJsonDocument doc = QJsonDocument::fromJson(data);
if (doc.isNull()) {
qWarning() << "JSON parse error:" << path;
return;
}
applyConfig(doc.object());
// 被删除后重建的文件 → 重新添加监控
if (!QFile::exists(path)) {
m_watcher.addPath(path);
}
}
void checkRotation() {
QFileInfo info(currentLogPath());
if (info.size() > 1024 * 1024) { // > 1MB
rotateLogFiles();
}
}
QString currentLogPath() {
return QDir("/var/log/collector")
.filePath(QString("data_%1.log").arg(m_currentLogIndex));
}
QByteArray m_dataBuffer;
QFileSystemWatcher m_watcher;
QTimer m_flushTimer;
int m_currentLogIndex = 0;
};
# 3.10 速查表
| 类 | 用途 | 关键方法 |
|---|---|---|
QIODevice | IO 抽象基类 | read(), write(), readyRead() |
QFile | 磁盘文件 | open(), map(), flush() |
QSaveFile | 原子写入 | commit(), cancelWriting() |
QLockFile | 进程锁 | tryLock(), getLockInfo() |
QTemporaryFile | 临时文件 | open(), fileName() |
QDataStream | 二进制序列化 | setVersion(), startTransaction() |
QTextStream | 文本流 | setEncoding(), qSetFieldWidth() |
QBuffer | 内存 IO | buffer(), setData() |
QDir | 目录操作 | entryList(), mkpath(), filePath() |
QFileInfo | 文件信息 | exists(), size(), lastModified() |
QSettings | 配置持久化 | setValue(), value() |
QFileSystemWatcher | 文件监控 | addPath(), fileChanged() |
QStandardPaths | 标准路径 | writableLocation() |
IO 铁律:
- 嵌入式 eMMC 写入后必须
flush()+ 用QSaveFile保证原子性 - 大文件顺序读用
map()(零拷贝),小文件用readAll() QDataStream固定版本号,避免 Qt 升级后反序列化失败- 配置文件用
QFileSystemWatcher实现热更新 - 频繁写入场景用内存缓冲 + 批量刷盘,保护 eMMC 寿命
- 不使用硬编码路径——底层用
QStandardPaths,上层用QDir::filePath()