3. cJSON库入门:移植、核心API与第一个JSON对象

好,咱们进入正题。JSON在嵌入式世界里,说白了就是数据交换的“通用语言”。ESP32作为物联网节点,免不了要和服务器、手机App打交道。那数据怎么组织?怎么解析?cJSON库就是干这个的。

我个人习惯,在项目初期就把cJSON库移植好。别等到后面数据收发出问题了,再回头找原因。那时候你想想看,排查起来多麻烦。

3.1 cJSON库的移植

cJSON库是纯C写的,就两个文件:cJSON.ccJSON.h。移植到ESP32上,其实就三步。

第一步:获取源码

去GitHub上搜一下cJSON,把最新版下载下来。我建议用v1.7.15之后的版本,API更稳定。我之前用过老版本,有个内存泄漏的坑,折腾了我一整天。

第二步:添加到工程

在ESP-IDF工程里,把这两个文件放到 components 目录下。或者直接丢到 main 文件夹里,简单粗暴。我个人喜欢建一个 components/cjson 文件夹,结构更清晰。

第三步:配置CMakeLists.txt

CMakeLists.txt 里加上:

set(COMPONENT_SRCS "cJSON.c")
set(COMPONENT_ADD_INCLUDEDIRS ".")
register_component()

嗯,这里要注意:ESP-IDF默认开启了 newlib 的浮点支持,cJSON的浮点数打印功能才能正常工作。如果你用的是其他RTOS,记得检查一下。

小技巧: 移植完成后,先编译一次。如果报错,99%是路径问题。检查一下头文件引用路径,别问我怎么知道的。

3.2 cJSON核心API介绍

cJSON的API不多,但每个都很关键。我按使用频率给你排个序。

API函数 功能说明 使用频率
cJSON_CreateObject() 创建一个JSON对象(根节点) ★★★★★
cJSON_CreateString() 创建字符串类型节点 ★★★★★
cJSON_CreateNumber() 创建数字类型节点 ★★★★★
cJSON_AddItemToObject() 向对象中添加键值对 ★★★★★
cJSON_Print() 将JSON对象转为字符串 ★★★★☆
cJSON_Parse() 将字符串解析为JSON对象 ★★★★☆
cJSON_Delete() 释放JSON对象内存 ★★★★★

为什么 cJSON_Delete() 使用频率也是五星?因为你不释放内存,ESP32那点RAM很快就爆了。我曾经在项目里漏了一次 cJSON_Delete(),结果设备运行48小时后,内存泄漏导致系统崩溃。从那以后,我写cJSON代码第一件事就是先写释放逻辑。

3.3 创建第一个JSON对象

光说不练假把式。咱们直接上手,创建一个传感器数据JSON。

假设我们要封装这样的数据:

{
  "device": "ESP32_01",
  "temperature": 25.6,
  "humidity": 68.2,
  "online": true
}

代码怎么写?看这里:

#include "cJSON.h"

void create_sensor_json(void)
{
    // 1. 创建根对象
    cJSON *root = cJSON_CreateObject();
    if (root == NULL) {
        // 内存分配失败,直接返回
        return;
    }

    // 2. 添加键值对
    cJSON_AddStringToObject(root, "device", "ESP32_01");
    cJSON_AddNumberToObject(root, "temperature", 25.6);
    cJSON_AddNumberToObject(root, "humidity", 68.2);
    cJSON_AddBoolToObject(root, "online", 1);  // true

    // 3. 转为字符串
    char *json_str = cJSON_Print(root);
    if (json_str) {
        printf("生成的JSON: %s\n", json_str);
        free(json_str);  // 注意:cJSON_Print分配的内存需要手动释放
    }

    // 4. 释放cJSON对象
    cJSON_Delete(root);
}
重点提醒: cJSON_Print() 返回的字符串是动态分配的,用完一定要 free()。而 cJSON_Delete() 会递归释放所有子节点,你只需要释放根节点就行。

你可能会问:为什么不用 cJSON_AddItemToObject() ?其实上面的 cJSON_AddStringToObject() 就是它的封装。我个人习惯,简单类型用封装函数,复杂嵌套才用 cJSON_AddItemToObject()

3.4 避坑指南

我刚开始用cJSON时,踩过不少坑。分享几个典型的:

  • 内存泄漏:每次 cJSON_Parse() 后,记得 cJSON_Delete()。我曾经在循环里解析JSON,忘了释放,结果RAM被吃光。
  • 空指针检查:cJSON的API在内存不足时会返回NULL。别偷懒,每次创建都检查一下。
  • 浮点数精度:ESP32的浮点运算没问题,但打印时默认保留6位小数。如果你要控制精度,可以用 cJSON_SetNumberValue() 配合 cJSON_PrintPreallocated()
特别注意: 在多任务环境下,cJSON不是线程安全的。如果你在多个任务中同时操作同一个JSON对象,记得加互斥锁。我有个项目就因为这个,数据偶尔会乱掉,排查了好久才发现是竞态问题。

好了,cJSON的入门就到这里。你把这个例子跑通,就算入门了。下一节咱们聊聊怎么解析JSON,那才是真正考验功底的地方。