29、JSON解析:cJSON库的使用,JSON的创建与解析
JSON,全称 JavaScript Object Notation,说白了就是一种轻量级的数据交换格式。你想想看,在嵌入式世界里,设备跟服务器通信、配置文件读写、甚至两个模块之间传数据,总得有个大家都认的格式吧?JSON 就是那个「大家都认」的格式。
我最早接触 JSON 是在做一个物联网网关项目的时候。那时候设备上报的数据五花八门,有温度、湿度、GPS坐标,还有设备状态。如果用自定义协议,解析起来累死人,而且不好扩展。后来我换成了 cJSON 库,嗯,世界清净了。
29.1 cJSON 是什么?为什么选它?
cJSON 是一个用 C 语言写的超轻量级 JSON 解析库。整个库就一个 .c 文件和一个 .h 文件,加起来不到 2000 行代码。对于嵌入式系统来说,这简直是福音——你想想看,那些动不动就几万行的库,在资源受限的 MCU 上根本跑不动。
我个人习惯在项目里直接拷贝 cJSON 的源码,而不是用包管理器。为什么?因为嵌入式环境千差万别,有些编译器对标准 C 的支持并不完整,直接源码集成,出了问题我能马上定位。
核心特点:
- 纯 C 实现,无外部依赖
- 支持 JSON 的创建、解析、修改、删除
- 内存占用极小,适合资源受限环境
- API 简洁,学习成本低
29.2 cJSON 的核心数据结构
要理解 cJSON 怎么用,先得搞懂它的数据结构。说白了,cJSON 把 JSON 里的每一种数据类型都映射成一个结构体节点,然后这些节点通过链表串起来。
typedef struct cJSON {
struct cJSON *next; // 指向下一个兄弟节点
struct cJSON *prev; // 指向前一个兄弟节点
struct cJSON *child; // 指向第一个子节点
int type; // 节点类型
char *valuestring; // 字符串值
int valueint; // 整数值
double valuedouble; // 浮点数值
char *string; // 节点的键名
} cJSON;
你看这个结构体,它其实是一个双向链表加树的混合体。每个节点通过 next 和 prev 跟兄弟节点连起来,通过 child 指向自己的子节点。这种设计让 JSON 的嵌套结构在内存里变得非常直观。
我记得有一次,一个同事问我:「为什么 cJSON 要同时支持 int 和 double?直接用 double 不就行了?」嗯,这个问题问得好。在嵌入式系统里,很多传感器数据是整数,用 double 存储会浪费内存,而且浮点运算比整数运算慢。cJSON 这么设计,说白了就是为了性能。
29.3 JSON 的创建:从零构建一个数据包
假设我们要创建一个这样的 JSON 对象:
{
"device_id": "sensor_01",
"temperature": 25.6,
"humidity": 68,
"status": "online",
"tags": ["indoor", "living_room"]
}
用 cJSON 来创建,代码是这样的:
#include "cJSON.h"
cJSON *root = cJSON_CreateObject();
if (root == NULL) {
// 内存分配失败,处理错误
return -1;
}
cJSON_AddStringToObject(root, "device_id", "sensor_01");
cJSON_AddNumberToObject(root, "temperature", 25.6);
cJSON_AddNumberToObject(root, "humidity", 68);
cJSON_AddStringToObject(root, "status", "online");
// 创建数组
cJSON *tags = cJSON_CreateArray();
cJSON_AddItemToArray(tags, cJSON_CreateString("indoor"));
cJSON_AddItemToArray(tags, cJSON_CreateString("living_room"));
cJSON_AddItemToObject(root, "tags", tags);
// 转换成字符串
char *json_str = cJSON_Print(root);
if (json_str) {
printf("JSON: %s\n", json_str);
free(json_str);
}
// 清理
cJSON_Delete(root);
小技巧: 我建议你每次创建完 JSON 对象后,都检查一下返回值是否为 NULL。嵌入式系统的内存很宝贵,malloc 失败是常有的事。我曾经在一个项目中因为没检查返回值,导致程序在内存不足时直接崩溃,排查了好久才发现是这里的问题。
29.4 JSON 的解析:从字符串到结构化数据
解析 JSON 是更常见的场景。比如从服务器收到一段 JSON 字符串,我们需要把它解析成 C 语言能处理的数据。
const char *json_string = "{"
"\"device_id\": \"sensor_01\","
"\"temperature\": 25.6,"
"\"humidity\": 68,"
"\"status\": \"online\""
"}";
cJSON *root = cJSON_Parse(json_string);
if (root == NULL) {
const char *error_ptr = cJSON_GetErrorPtr();
if (error_ptr != NULL) {
printf("解析错误,位置: %s\n", error_ptr);
}
return -1;
}
// 提取数据
cJSON *device_id = cJSON_GetObjectItem(root, "device_id");
cJSON *temperature = cJSON_GetObjectItem(root, "temperature");
cJSON *humidity = cJSON_GetObjectItem(root, "humidity");
cJSON *status = cJSON_GetObjectItem(root, "status");
if (cJSON_IsString(device_id) && (device_id->valuestring != NULL)) {
printf("设备ID: %s\n", device_id->valuestring);
}
if (cJSON_IsNumber(temperature)) {
printf("温度: %.1f\n", temperature->valuedouble);
}
if (cJSON_IsNumber(humidity)) {
printf("湿度: %d\n", humidity->valueint);
}
if (cJSON_IsString(status) && (status->valuestring != NULL)) {
printf("状态: %s\n", status->valuestring);
}
cJSON_Delete(root);
注意: 解析 JSON 时,一定要用 cJSON_IsString()、cJSON_IsNumber() 这些类型检查函数。不要直接访问 valuestring 或 valuedouble,因为如果节点类型不匹配,这些字段可能是未初始化的垃圾值。我曾经见过有人直接访问 valuestring,结果拿到的是乱码,程序直接段错误。
29.5 嵌套对象的解析
实际项目中的 JSON 往往不是扁平的,而是多层嵌套。比如:
{
"device": {
"id": "sensor_01",
"location": {
"floor": 3,
"room": "A201"
}
},
"readings": [
{"time": "2024-01-15", "value": 25.6},
{"time": "2024-01-16", "value": 26.1}
]
}
解析这种嵌套结构,关键是要一层一层往下走:
cJSON *device = cJSON_GetObjectItem(root, "device");
if (cJSON_IsObject(device)) {
cJSON *id = cJSON_GetObjectItem(device, "id");
cJSON *location = cJSON_GetObjectItem(device, "location");
if (cJSON_IsObject(location)) {
cJSON *floor = cJSON_GetObjectItem(location, "floor");
cJSON *room = cJSON_GetObjectItem(location, "room");
// 处理数据...
}
}
// 解析数组
cJSON *readings = cJSON_GetObjectItem(root, "readings");
if (cJSON_IsArray(readings)) {
int array_size = cJSON_GetArraySize(readings);
for (int i = 0; i < array_size; i++) {
cJSON *item = cJSON_GetArrayItem(readings, i);
if (cJSON_IsObject(item)) {
cJSON *time = cJSON_GetObjectItem(item, "time");
cJSON *value = cJSON_GetObjectItem(item, "value");
// 处理每个读数...
}
}
}
29.6 内存管理:别让内存泄漏找上你
cJSON 使用动态内存分配,所以内存管理是重中之重。我总结了几条铁律:
- 谁创建,谁释放。 用
cJSON_Create*创建的对象,必须用cJSON_Delete释放。 - cJSON_Print 返回的字符串需要 free。 很多人会忘记这一步。
- 不要手动 free cJSON 节点。 用
cJSON_Delete会自动处理整个树。 - cJSON_AddItemToArray 和 cJSON_AddItemToObject 会接管节点的所有权。 添加之后不要再手动释放那个节点。
避坑指南: 我曾经在一个长期运行的设备上发现内存不断增长,排查了两天才找到原因——我在循环里反复调用 cJSON_Parse 和 cJSON_Print,但忘记释放中间生成的字符串。嗯,从那以后,我每次写完 JSON 操作代码,都会用 Valgrind 跑一遍内存检查。
29.7 性能优化:嵌入式环境下的注意事项
在嵌入式系统里,资源是有限的。我分享几个实战经验:
- 用 cJSON_PrintUnformatted 代替 cJSON_Print。 格式化输出会加换行和缩进,浪费空间。非调试场景下,用无格式输出能省 30% 左右的字符串长度。
- 预分配内存池。 如果项目对实时性要求高,可以修改 cJSON 的
cJSON_hooks,用自己实现的内存池代替 malloc/free。 - 避免频繁创建和销毁。 如果同一个 JSON 结构要反复使用,考虑复用节点,而不是每次都重新创建。
29.8 知识体系总览
下面这张图总结了 cJSON 的核心操作流程,从创建/解析到最终的内存释放,每一步都环环相扣:
29.9 实战建议
最后,我想说几句掏心窝子的话。cJSON 虽然好用,但它不是万能的。如果你的项目对性能要求极高,或者 JSON 数据量特别大(比如几百 KB 以上),你可能需要考虑更轻量级的方案,比如用 CBOR 或者 MessagePack。但对于绝大多数嵌入式场景,cJSON 完全够用。
我个人习惯在项目里封装一层 JSON 工具函数,把 cJSON 的细节藏起来。比如写一个 json_get_string()、json_get_int(),这样上层代码根本不需要知道 cJSON 的存在。万一以后要换库,改动范围也小。
嗯,JSON 解析这个话题就聊到这里。记住一句话:用好工具,但别被工具束缚。cJSON 是你的武器,不是你的枷锁。