这篇文章是对 RFID 仓库管理系统中 db_manager 模块(SQLite 数据库集成)的全流程总结。涵盖了从 SQL 执行、回调机制、内存管理、JSON 解析到 TCP 发送的每一个关键点。
一、SQLite 两种执行方式
1.1 sqlite3_exec --- 回调方式(适合 SELECT 查询)
int rc = sqlite3_exec(g_db, sql, callback, user_data, &err_msg);
流程:
- 编译 SQL
- 执行 SQL
- 每查到一行数据,调用一次你的回调函数
- 所有行处理完毕,返回
1.2 sqlite3_prepare_v2 + step --- 预处理方式(适合有参数的查询)
sqlite3_stmt *stmt = NULL;
sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM t WHERE batch_no = ?", -1, &stmt, NULL);
sqlite3_bind_text(stmt, 1, batch_no, -1, SQLITE_STATIC);
sqlite3_step(stmt);
int count = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt);
流程:
prepare--- 编译 SQL,带?占位符bind--- 把实际值填进?step--- 执行,SQLITE_ROW表示有结果column--- 取出结果finalize--- 释放资源
为什么用两步?
?占位符需要先"挖坑",再"填值"。没有?的话可以直接exec。
二、回调函数参数全解析
static int collect_batch_callback(
void *data, // 你自己的数据(如 BatchListData 的指针)
int col_count, // SELECT 后面有几个字段
char **col_values, // 当前行的数据(字符串数组)
char **col_names // 各列的列名(字符串数组)
)
参数来源
| 参数 | 谁给的 | 例子 |
|---|---|---|
data |
你 (sqlite3_exec 第4个参数) |
&bd |
col_count |
SQLite 编译时算的 | SELECT a,b → 2 |
col_values |
SQLite 执行时从数据库读的 | ["RK20260610-003"] |
col_names |
SQLite 从 CREATE TABLE 读的 | ["batch_no"] |
col_count 是一个数字,决定两个数组的长度
sql
SELECT batch_no FROM inbound_records; -- col_count = 1
SELECT batch_no, warehouse FROM ...; -- col_count = 2
SELECT batch_no, warehouse, goods_name FROM ...; -- col_count = 3
col_count 跟数据无关(即使值是 NULL,列数不变),只跟 SELECT 写了几个字段有关。
回调的调用次数 = 表中的行数
sql
-- inbound_records 表有 3 行数据
SELECT batch_no FROM inbound_records;
-- 回调被调用 3 次,每次一行:
-- 第1次:col_values = ["RK20260610-003"]
-- 第2次:col_values = ["RK20260610-002"]
-- 第3次:col_values = ["RK20260610-001"]
col_names 是列名
sql
CREATE TABLE inbound_records (
batch_no TEXT, ← 这个 "batch_no" 就是 col_names 的值的来源
warehouse TEXT, ← "warehouse"
goods_name TEXT ← "goods_name"
);
SELECT batch_no FROM inbound_records;
-- col_names = ["batch_no"] ← 来自表结构定义
三、为什么必须 strdup?
static int collect_batch_callback(void *data, ..., char **col_values, ...)
{
// col_values[0] 指向 SQLite 内部的临时内存
// 回调返回后,这个内存会被回收
// ✅ 正确做法:strdup 复制到堆
bd->list[bd->count] = strdup(col_values[0]);
// ❌ 错误做法:只存指针
bd->list[bd->count] = col_values[0]; // 回调返回后变野指针!
}
strdup 干了什么
// strdup = malloc + memcpy
char *strdup(const char *s) {
char *copy = malloc(strlen(s) + 1); // 申请新内存
strcpy(copy, s); // 复制字符串过去
return copy; // 返回新内存的地址
}
类比
SQLite 临时缓冲区(借你用的纸条)
┌──────────────────┐
│ RK20260610-003 │ ← col_values[0] 指向这里
└──────────────────┘
回调结束后,这张纸条就被回收了
strdup 复制一份到堆上(你抄到自己笔记本上)
┌──────────────────┐
│ RK20260610-003 │ ← 永远有效,直到你 free
└──────────────────┘
四、realloc 和 strdup 的关系
两个不同的概念
| 函数 | 作用 | 比喻 |
|---|---|---|
realloc |
扩大 指针数组(增加"格子"数量) | 书架不够用了,买个更大的书架 |
strdup |
复制 字符串数据 到新内存 | 买了本新书,放在仓库里 |
内存结构
char **list = realloc(NULL, 16 * sizeof(char*));
// list 是指针数组,每个元素存一个地址(4或8字节)
bd->list(指针数组,realloc 管理的)
┌──────┬──────┬──────┬──────┐
│ ● │ ● │ ● │ │ ← 每个格子 4/8 字节,只存地址
└──│───┴──│───┴──│───┴──────┘
│ │ │
│ │ └────→ strdup("RK20260610-001") → 15字节,在另一块内存
│ │
│ └──────────→ strdup("RK20260610-002")
│
└────────────────→ strdup("RK20260610-003")
为什么不能合并
因为 指针数组的格子只能放指针,放不下 15 个字符的字符串。
list[0] = "RK20260610-001"; // list[0] 存的是地址,不是15个字符
realloc 扩再大,每个格子的大小仍然是 sizeof(char*) = 4 或 8 字节。需要 strdup 在别处申请内存存真正的字符串。
五、先 realloc 再 strdup 的顺序
正确顺序
// 1. 先确保指针数组有格子
if(bd->count >= bd->capacity) {
bd->capacity *= 2;
bd->list = realloc(bd->list, bd->capacity * sizeof(char*));
}
// 2. 再把数据放进去
bd->list[bd->count] = strdup(col_values[0]);
bd->count++;
为什么不能反过来
如果先 strdup 再 realloc,一旦 realloc 失败返回 NULL,之前 strdup 的内存就没人管了(内存泄漏)。
六、TCP 发送时的 malloc
为什么不能用固定数组
// tcp_send_tag 可以用固定数组,因为格式固定:
void tcp_send_tag(const char *epc, const char *tid) {
char buf[256]; // 固定大小,够用
snprintf(buf, sizeof(buf),
"{\"cmd\":\"tag_found\",\"epc\":\"%s\",\"tid\":\"%s\"}",
epc, tid);
tcp_send(buf);
}
// tcp_send_batch_list 必须用 malloc,因为长度不确定:
void tcp_send_batch_list(char **list, int count) {
int buf_size = 128; // 固定部分
for(int i = 0; i < count; i++)
buf_size += strlen(list[i]) + 16; // 每个批次号 + 格式字符
char *buf = malloc(buf_size); // 按需申请
snprintf(buf, ...);
tcp_send(buf);
free(buf); // 用完即释
}
+16 是什么
每个批次号在 JSON 里要包这些额外字符:
"RK20260610-001",
↑ ↑↑
左引号 右引号+逗号
所以额外需要 3 字节(" + " + ,),+16 是保守估算。
完整内存生命周期
SQLite 回调(临时数据)
│
│ strdup → 复制到堆
▼
bd->list(永久保存,直到 TCP 发完)
│
│ snprintf → 拼到 JSON 缓冲区
▼
buf(malloc 临时缓冲区,发完即 free)
│
│ tcp_send → 发送到网络
▼
free(buf) ← JSON 容器释放
db_free_str_list(list) ← 数据释放
七、字符常量 '"' 的语法
C 语言的规则
'A' // 字符 A(ASCII 65)
'1' // 字符 1(ASCII 49)
',' // 字符 逗号(ASCII 44)
'"' // 字符 双引号(ASCII 34) ← 外层单引号是语法标记
外层两个 ' 是 C 语法标记 ,表示"这是一个字符常量"。内层的 " 才是真正的值。
在 JSON 解析中的应用
for(i = 0; p[i] && p[i] != '"'; i++)
out[i] = p[i];
// ↑
// 遇到双引号字符就停
在 JSON 里匹配的是值的结束引号:
...batch_no":"RK20260610-001","warehouse"...
↑
p[i] == '"' → 循环结束
八、JSON 解析方式
get_json_str 解析流程
static void get_json_str(const char *json, const char *key, char *out, int out_size)
{
char search[64];
snprintf(search, sizeof(search), "\"%s\":\"", key);
// search = "batch_no":"
const char *p = strstr(json, search);
// 在 JSON 里找 "batch_no":"
p += strlen(search);
// 跳过 "batch_no":",p 指向值开头
int i;
for(i = 0; i < out_size - 1 && p[i] && p[i] != '"'; i++)
out[i] = p[i];
// 逐字复制直到遇到 "
out[i] = '\0';
}
为什么不直接用 cJSON
嵌入式设备上引入一个 JSON 库会增加代码体积和内存开销。用 strstr 做简单解析足够满足当前需求(只解析字符串值,不涉及嵌套对象)。
局限
- 只能解析字符串值(键值对中的
"value") - JSON 格式不能有额外空格
- 遇到
\"转义会出错 - 但对我们当前够用
九、SQL 整数 vs 字符串
sql
CREATE TABLE inbound_records (
total_count INTEGER -- 整数类型
);
-- 插入时:
INSERT INTO inbound_records (..., total_count) VALUES ('...', 98);
-- ↑
-- 整数,不带引号
-- 错误的写法:
INSERT INTO inbound_records (..., total_count) VALUES ('...', '98');
-- ↑
-- 带引号变成了字符串
在 C 代码中对应的格式化:
sqlite3_mprintf(
"VALUES ('%q', '%q', '%q', '%q', '%q', %d);",
batch_no, warehouse, shelf, goods_name, product, count);
// 字符串用 '%q' ↑
// 整数用 %d(没有引号)
十、哈希表清空
为什么每次扫描前要 destroy
int scan_Until_stable(...) {
destroy_hash(head); // 清空上次扫描的数据
// 开始新扫描
BeginInv(hDev);
// ...
}
不清空的后果
第1次扫描 → 哈希表有 98 个标签
用户忘了点"确认入库"
第2次扫描 → hash 不清 → 98 + 50 = 148 个
用户点"确认入库" → 保存了 148 个,实际只扫了 50 个 → 数据错乱
每次扫描都是独立的,从零开始。
十一、栈数组大小
// handle_inbound_confirm 中:
char epc_list[1024][64]; // 1024 × 64 = 65536 字节 = 64KB
char tid_list[1024][64]; // 另一个 64KB
// 合计 128KB,在栈上
嵌入式设备的栈大小
i.MX6ULL 默认栈 8MB,128KB 占 1.5%,安全。
如果不够怎么办
用 malloc 按需分配:
int total = HASH_COUNT(scan_tag_hash_list);
char (*epc_list)[64] = malloc(total * sizeof(*epc_list));
// 用完 free(epc_list);
| 方式 | 内存位置 | 大小 |
|---|---|---|
| 栈数组 1024 | 栈 | 128KB 固定 |
| malloc 按需 | 堆 | 实际标签数 × 128 字节 |
十二、为什么不在回调里直接发 TCP
三个原因
| 问题 | 说明 |
|---|---|
| 回调每行调一次 | 如果发 TCP,QT 会收到 N 条小消息,QT 端要自己拼成完整列表 |
| TCP 阻塞回调 | send() 可能阻塞,导致 SQLite 查询也被阻塞 |
| 回调后数据失效 | 回调结束后 col_values 就没了 |
正确做法
// 回调里只收集
static int collect_batch_callback(void *data, ...) {
BatchListData *bd = data;
bd->list[bd->count] = strdup(col_values[0]);
bd->count++;
return 0;
}
// 回调结束后统一发送
char **list = db_get_batch_list(&count);
tcp_send_batch_list(list, count); // 拼成一条完整 JSON 发送
db_free_str_list(list, count); // 释放
总结
SQLite 操作
│
├─ exec(回调) → collect → strdup + realloc → 收集完整
│
└─ prepare + bind + step → finalize → 安全释放
│
▼
TCP 发送(malloc JSON → send → free)
核心原则:回调只收集,不发送。需要持久化的数据必须 strdup。指针数组 realloc 扩大,字符串 strdup 存数据。JSON 发送时临时 malloc 缓冲区用完即释。