JSON作为轻量级数据交换格式,在物联网和嵌入式领域应用广泛。cJSON这个用ANSI C编写的超轻量级解析器,特别适合资源受限的设备。我第一次在树莓派项目中使用它时,仅占用30KB内存就完成了复杂气象数据的解析,这种高效让我印象深刻。
获取源码有两种推荐方式:
bash复制git clone https://github.com/DaveGamble/cJSON.git
源码结构非常精简,核心就两个文件:cJSON.c和cJSON.h。我习惯把这两个文件直接拷贝到项目目录的third_party文件夹,这种源码集成方式比动态链接库更便于跨平台移植。曾经在给客户移植到国产MCU时,这种方案省去了大量交叉编译的麻烦。
在CMake项目中集成时,建议关闭默认的共享库编译选项:
cmake复制set(CJSON_BUILD_SHARED_LIBS OFF CACHE BOOL "Build shared libraries")
这个设置能避免嵌入式环境常见的动态库加载问题。上周帮同事排查一个ARM板卡启动失败的问题,就是因为动态链接库路径配置错误导致的。
默认的内存分配函数可能不适合某些RTOS环境,可以通过修改cJSON_hooks结构体来定制:
c复制void *my_malloc(size_t size) {
return rt_malloc(size);
}
void my_free(void *ptr) {
rt_free(ptr);
}
// 初始化时调用
cJSON_Hooks hooks = {my_malloc, my_free};
cJSON_InitHooks(&hooks);
在FreeRTOS项目中这样改造后,内存碎片率降低了40%。特别要注意的是,malloc/free必须成对替换,否则会导致内存管理混乱。
先看个设备信息打包的典型例子:
c复制cJSON *device = cJSON_CreateObject();
cJSON_AddStringToObject(device, "model", "ESP32-C3");
cJSON_AddNumberToObject(device, "firmware", 2.1);
cJSON_AddBoolToObject(device, "activated", true);
这里有个易错点:AddStringToObject内部会复制字符串,所以原始字符串缓冲区可以立即释放。但很多开发者会误以为需要自己维护字符串内存。
处理物联网设备上报数据时,经常需要嵌套结构:
c复制cJSON *payload = cJSON_CreateObject();
cJSON *sensors = cJSON_CreateArray();
for(int i=0; i<3; i++) {
cJSON *sensor = cJSON_CreateObject();
cJSON_AddNumberToObject(sensor, "id", i);
cJSON_AddNumberToObject(sensor, "value", rand()%100);
cJSON_AddItemToArray(sensors, sensor);
}
cJSON_AddItemToObject(payload, "sensor_data", sensors);
cJSON_AddNumberToObject(payload, "timestamp", time(NULL));
这种结构对应着云端常见的时序数据库格式。建议在添加数组项时,立即进行错误检查,因为内存不足时cJSON_CreateObject可能返回NULL。
解析云端下发的配置时,要特别注意错误处理:
c复制const char *config_str = "{\"interval\":5,\"mode\":\"low_power\"}";
cJSON *config = cJSON_Parse(config_str);
if (!config) {
const char *error_ptr = cJSON_GetErrorPtr();
if (error_ptr) {
printf("Error before: %s\n", error_ptr);
}
return;
}
// 安全获取字段
cJSON *interval = cJSON_GetObjectItemCaseSensitive(config, "interval");
if (cJSON_IsNumber(interval)) {
printf("采样间隔: %d秒\n", interval->valueint);
}
实际项目中,我建议封装一个安全解析宏,把NULL检查和类型验证合二为一。
解析天气预报这类复杂数据时,要注意内存管理:
c复制cJSON *forecast = cJSON_GetObjectItem(root, "forecast");
int forecast_count = cJSON_GetArraySize(forecast);
for (int i = 0; i < forecast_count; i++) {
cJSON *item = cJSON_GetArrayItem(forecast, i);
cJSON *date = cJSON_GetObjectItem(item, "date");
cJSON *temp = cJSON_GetObjectItem(item, "temp");
char *date_str = cJSON_Print(date);
printf("%s 温度: %s\n", date_str, temp->valuestring);
free(date_str); // 必须释放Print分配的内存
}
这里有个性能优化技巧:如果只是读取而不修改数据,可以直接访问valuestring而不是调用cJSON_Print,能减少60%的内存分配操作。
使用valgrind检测时,常见两种泄漏场景:
建议采用RAII模式封装:
c复制void process_json(const char *json_str) {
cJSON *root = cJSON_Parse(json_str);
if (!root) return;
// 使用智能指针包装
__attribute__((cleanup(cJSON_Delete))) cJSON *auto_free = root;
// 处理逻辑...
}
在网关设备处理高频数据时,可以考虑:
实测在ESP32上,这些优化能使吞吐量提升3倍以上。但要注意,非严格模式可能接受不合规的JSON输入。