cJSON源码分析

1,029 阅读5分钟

JSON简介

JSON(JavaScript Object Notation, JS 对象简谱) 是一种轻量级的数据交换格式。它基于 ECMAScript (欧洲计算机协会制定的js规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。 易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。

cJSON简介

cJSON是一个仅有一个.h文件,一个.c文件组成的JSON解析器,它是由纯C(ANSI C89)实现的,跨平台性较好。cJSON是采用链表存储的。

源码分析

由于源码细节太多,所以以下代码只展示核心部分,比如部分变量初始化、申请释放内存等操作省略。

skip函数

static const char *skip(const char *in)           //#1
{
	while (in && *in && (unsigned char)*in<=32)   //#2
		in++;
    return in;	
}
  • #1:该函数主要作用是跳过字符串前面的空格和“\n”.
  • #2:当指针为空、指针内容为空字符、指针内容的ASCII码值小于32时,指针++,32是空格的ASCII码值,“\n”的ACSII码值为10,空字符的ACSII码值为0。

parse_value函数

static const char *parse_value(cJSON *item,const char *value)
{
	if (!value)						return 0;	/* Fail on null. */
	if (!strncmp(value,"null",4))	{ item->type=cJSON_NULL;  return value+4; }
	if (!strncmp(value,"false",5))	{ item->type=cJSON_False; return value+5; }
	if (!strncmp(value,"true",4))	{ item->type=cJSON_True; item->valueint=1;	return value+4; }
	if (*value=='\"')				{ return parse_string(item,value); }
	if (*value=='-' || (*value>='0' && *value<='9'))	{ return parse_number(item,value); }
	if (*value=='[')				{ return parse_array(item,value); }
	if (*value=='{')				{ return parse_object(item,value); }
	ep=value;return 0;	/* failure. */
}
  • 该函数是解释器的核心,通过判断输入字符串的首字符,选择调用不同的解释函数进行解释。递归调用该函数就可以完成对JSON格式的解释。

parse_string函数

顾名思义,该函数是解释字符串用的

static const char *parse_string(cJSON *item,const char *str)
{
	...	
    const char *ptr=str+1;					//#1
    while (*ptr!='\"' && *ptr && ++len) 	//#2
    		if (*ptr++ == '\\')  ptr++;		//#3
    out=(char*)cJSON_malloc(len+1);         //#4
    ptr=str+1;ptr2=out;                     //#5
    while (*ptr!='\"' && *ptr)				//#6
	{
		if (*ptr!='\\') *ptr2++=*ptr++;		//#7
        else {ptr++;...ptr++;}				//#8
    }
    *ptr2=0;								//#9
    if (*ptr=='\"') ptr++;					//#10
	item->valuestring=out;					//#11
	item->type=cJSON_String;				//#12
	return ptr;
}
  • #1:定义一个指向输入字符串第二个字符的指针,因为字符串的第一个字符是'"'。
  • #2~#4:完成对字符串中字符个数的计数,然后分配内存。计数规则:ptr++,直到碰到下一个'"'字符就停止。在需要解释的字符中,可能出现以下情况"\"Jack (\\\"Bee\\\") Nimble\"",中间部分"\\\""解释成"\"", 所以当遇到字符"\\"时,ptr指针应该自加两次,"\\\""只算成一个长度,"\"Jack (\\\"Bee\\\") Nimble\""解释成Jack (\"Bee\") Nimble,当后续在此显示这个字符串的时候,将在此发生转义,字符串将显示为Jack ("Bee") Nimble
  • #5: 将ptr指向第二个字符。
  • #6:挨个遍历字符串内容,将其赋值给ptr2
  • #7~#8:特殊情况是当字符串里又出现转义字符数,将对其进行特殊处理,此处代码详细介绍,功能例如,将"\\\""解释成"'","\\n"解释成"\n"。
  • #9:由于字符串必须以'\0'结尾,所以要添加一个空字符作为结尾标志。
  • #10:返回指向当前解释完的字符串的下一个需要解释的字符的指针。
  • #11~#12:将解释完的内容保存到cJSON结构体中,解释完毕。

parse_number函数

顾名思义,将字符串中的数字部分提取出来,这有点像C库中的atoi()函数。

static const char *parse_number(cJSON *item,const char *num)
{
	if (*num=='-') sign=-1,num++;	
	if (*num=='0') num++;			
	if (*num>='1' && *num<='9')	do	n=(n*10.0)+(*num++ -'0');	while (*num>='0' && *num<='9');	
	if (*num=='.' && num[1]>='0' && num[1]<='9') {num++;		do	n=(n*10.0)+(*num++ -'0'),scale--; while (*num>='0' && *num<='9');}	
	if (*num=='e' || *num=='E')	
	{	num++;if (*num=='+') num++;	else if (*num=='-') signsubscale=-1,num++;		
		while (*num>='0' && *num<='9') subscale=(subscale*10)+(*num++ - '0');	
	}

	n=sign*n*pow(10.0,(scale+subscale*signsubscale));
	item->valuedouble=n;
	item->valueint=(int)n;
	item->type=cJSON_Number;
	return num;
}

parse_object函数

在JSON格式中,有两种数据,一类是对象object,一类是数组array object{开始,以}结尾,并由若干条目组成,每个条目格式如下title: content,title是字符串类型,content可以是字符串、数字、object类型,也就是说object可嵌套。下面是源码:

static const char *parse_object(cJSON *item,const char *value)
{
	...
	item->type=cJSON_Object;							  	//#1
    item->child=child=cJSON_New_Item();						//#2
	value=skip(parse_string(child,skip(value)));			//#3
	child->string=child->valuestring;child->valuestring=0;	//#4
    if (*value!=':') {ep=value;return 0;}					//#5
    value=skip(parse_value(child,skip(value+1)));           //#6
	while (*value==',')										//#7
	{
		child->next=new_item;new_item->prev=child;child=new_item; //#8
		...													//#9
	}  
	if (*value=='}') return value+1;						//#10
}
  • #1~#2: 该sJSON结构体的类型设置,并初始化一个child子结构体。
  • #3~#5:按照object的数据格式,第一项应该是字符串格式的title,所以首先提取字符串,提取完后需要判断下个字符是不是':'字符。
  • #6:由于content部分可以是多种格式,所以需要再次调用parse_value()函数进行递归解释。
  • #7~#8:由于一个object有多个条目,并且以','隔开,所以遍历所有条目,并将用cJION->next将所有条目连接起来。
  • #9: 重复执行#3~#6。
  • #10:判断object是否结束。

parse_array函数

array[开头,以]结束,并且在内部也可以进行array嵌套。过程与parse_object()函数类似。源码如下:

static const char *parse_array(cJSON *item,const char *value)
{
	item->type=cJSON_Array;									//#1
	item->child=child=cJSON_New_Item();						//#2
    value=skip(parse_value(child,skip(value)));				//#3
    while (*value==',')										//#4
	{
		child->next=new_item;new_item->prev=child;child=new_item; //#5
		value=skip(parse_value(child,skip(value+1)));		//#6
	}
    if (*value==']') return value+1;						//#7
}
  • #1~#2:该sJSON结构体的类型设置,并初始化一个child子结构体。
  • #3:获取第一个元素的内容。
  • #4:判断是否存在下一个元素。
  • #5~#6:由于一个array有多个元素,并且以','隔开,所以遍历所有条目,并将用cJION->next将所有条目连接起来。然后进行下个元素的获取。
  • #7: 判断是否结束。

实例

例:若要对下列数据进行解释

"{\n\"name\": \"Jack (\\\"Bee\\\") Nimble\", 
	\n\"format\": {\"type\":       \"rect\", 
		\n\"width\":      1920, 
			\n\"height\":     1080, \n\"interlace\":  false,
				\"frame rate\": 24\n}\n}"	

函数执行过程

  • 检测到第一个{,调用parse_object()函数。
  • 解释第一个条目,调用parse_string()函数解释title\"name\"
  • 调用parse_string()函数解释content\"Jack (\\\"Bee\\\") Nimble\"
  • 下一个条目的解释,调用parse_string()函数解释title\"format\"
  • 检测到第二个{,调用parse_object()函数解释其content
  • 调用parse_string()函数解释title\"width\"
  • 调用parse_nubmer()函数解释content1920
  • 重复操作直到解释完24,检测到第一个},至此\"format\"的内容解释完了。
  • 检测到第二个},整个数据解释完毕。

数据结构

该数据分为两个object对象。第一个object为: 第二个object为:

总结

  • 程序中内存分配的函数为cJSON_malloc()。其定义为
		static void *(*cJSON_malloc)(size_t sz) = malloc;

为了和malloc区分开来,定义了一个函数指针cJSON_malloc并让他指向malloc函数,这样就可以给函数进行重命名,增强代码的可读性。

  • 程序中内存分配部分的表示内存变量大小的数据类型为size_t,他表示对象大小的上限,也就是增强了移植性。
  • 程序中有一个全局静态变量eq,他会跟踪数据解析的位置,当解析出错时,可以根据eq的值找到解析出错的位置,方便调试。