cJSON 源碼分析

 

一、cJSON概述


cJSON  是一個非常輕量的C語言庫,構建在ANSI C標準之上,用來解析JSON格式的數據。說其輕量是因爲該庫只包含了一個頭文件和一個源文件,總的代碼量不到一千行,源碼可以在這裏下載。本篇博客對cJSON庫源碼進行分析,旨在弄懂其實現原理和相關技術。我認爲可以從以下三個問題入手:

        1. 如何表示JSON數據

        2. 如何生成JSON數據

        3. 如何解析JSON數據

 

二、如何表示JSON數據


JSON主要的值類型null,false,true,number(數字),string(字符串),array(數組)以及object(對象),其中字符串用雙引號" "表示,數組用[]表示,對象用{}表示,樣例如下所示,值得注意的是數組或者對象是可以嵌套使用的。

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

 對於JSON數據,cJSON庫用了雙向鏈表這兩種數據結構來構建,裏面的基本元素(鍵值對)稱作一個結點,將上面的樣例數據用樹形結構表示,如圖1所示。

                                                      圖1 JSON樣例數據的樹形結構

樹中的每一個結點用結構體表示,對應cJSON庫的源碼如下,其中

    字段nextprev用於構建雙向鏈表查找同一層的結點(如linklist(name、format)或者linklist(type、width、height、interlace、frame rate)),即同一層的結點通常是雙向鏈表的元素;
    字段child是構建樹層級結構的關鍵,用於查找下一層的結點,如Root的child是linklist(name、format),format結點的child是linklist(type、width、height、interlace、frame rate),注意只有對象和數組有child字段,該字段指向linklist的表頭
    字段type記錄這個結點的值類型(null,false,true,number,string,array,object中的一種);
    字段valuestringvalueintvaluedouble分別記錄所在結點的字符串值、整型值和雙精度浮點型值;
    字段string是該結點的名字。

typedef struct cJSON {
	struct cJSON *next,*prev;
	struct cJSON *child;		
 
	int type;					
 
	char *valuestring;			
	int valueint;				
	double valuedouble;
 
	char *string;				
} cJSON;

 

三、如何生成JSON數據


好了,到這裏我們已經知道cJSON庫是如何表示JSON格式的數據了,通過以下代碼就可以生成先前的JSON樣例數據,因此,我們只要弄懂了下列函數的原理,生成JSON數據的整個過程自然就懂了。

cJSON *root,*fmt;
root=cJSON_CreateObject();	
cJSON_AddItemToObject(root, "name", cJSON_CreateString("Jack (\"Bee\") Nimble"));
cJSON_AddItemToObject(root, "format", fmt=cJSON_CreateObject());
cJSON_AddStringToObject(fmt,"type",		"rect");
cJSON_AddNumberToObject(fmt,"width",		1920);
cJSON_AddNumberToObject(fmt,"height",		1080);
cJSON_AddFalseToObject (fmt,"interlace");
cJSON_AddNumberToObject(fmt,"frame rate",	24);

首先看cJSON_CreateObject()函數內部的執行過程,該函數通過調用cJSON_New_Item ()分配內存,創建空白結點,如果創建成功則標識該結點的類型爲對象類型,也就是說該函數的作用是創建一個對象結點。

cJSON * cJSON_CreateObject (void)
{
	cJSON * item = cJSON_New_Item ();
 
	if (item)
		item->type = cJSON_Object;
 
	return item;
}
 
/* Internal constructor. */
static cJSON * cJSON_New_Item (void)
{
	cJSON * node = (cJSON *)cJSON_malloc (sizeof (cJSON));
 
	if (node)
		memset (node, 0, sizeof (cJSON));
 
	return node;
}

接着看cJSON_AddItemToObject函數,這裏涉及的代碼有點多,不過大家不要怕,我們一起來解讀這段代碼:該函數有3個形參,分別是對象父節點、子結點名字以及子結點,該函數要求子結點已經分配內存,如果子結點的名字已被賦值,則會被釋放再重新賦予指定的名字,在這一過程中,由於名字是通過指針傳入的,爲了不影響原先這塊內存的值,所以子結點的名字值實際指向的是傳入值在內存中的一份副本;到這裏也僅僅是修改了子結點的名字,最關鍵的一步是將對象父節點和子結點建立聯繫,該過程封裝在cJSON_AddItemToArray (object,item)函數中,其通過父節點的child字段完成綁定,並將子結點插入鏈表的表尾。

void cJSON_AddItemToObject (cJSON * object, const char * string, cJSON * item)
{
	if (!item)
		return;
 
	if (item->string)
		cJSON_free (item->string);
 
	item->string = cJSON_strdup (string);
	cJSON_AddItemToArray (object, item);
}
 
static char * cJSON_strdup (const char * str)
{
	size_t	len;
	char *	copy;
 
	len = strlen (str) + 1;
 
	if (! (copy = (char *) cJSON_malloc (len)))
		return 0;
 
	memcpy (copy, str, len);
	return copy;
}
 
/* Add item to array/object. */
void cJSON_AddItemToArray (cJSON * array, cJSON * item)
{
	cJSON * c = array->child;
 
	if (!item)
		return;
 
	if (!c)
	{
		array->child = item;
	}
	else 
	{
		while (c && c->next)
			c = c->next;
 
		suffix_object (c, item);
	}
}
 
/* Utility for array list handling. */
static void suffix_object (cJSON * prev, cJSON * item)
{
	prev->next = item;
	item->prev = prev;
}

 通過下面的源碼得知,其餘各類型數據實際都是調用函數cJSON_AddItemToObject添加的,因此只要根據樣例數據的內容,分別調用對應的函數將子結點添加到對象父節點即可。

/* Macros for creating things quickly. */
#define cJSON_AddNullToObject(object,name)		cJSON_AddItemToObject(object, name, cJSON_CreateNull())
#define cJSON_AddTrueToObject(object,name)		cJSON_AddItemToObject(object, name, cJSON_CreateTrue())
#define cJSON_AddFalseToObject(object,name)		cJSON_AddItemToObject(object, name, cJSON_CreateFalse())
#define cJSON_AddBoolToObject(object,name,b)	cJSON_AddItemToObject(object, name, cJSON_CreateBool(b))
#define cJSON_AddNumberToObject(object,name,n)	cJSON_AddItemToObject(object, name, cJSON_CreateNumber(n))
#define cJSON_AddStringToObject(object,name,s)	cJSON_AddItemToObject(object, name, cJSON_CreateString(s))

 

四、如何解析JSON數據


經過前兩步,我們已經知道了如何表示JSON數據以及生成JSON數據,接下來看看cJSON庫是如何解析JSON數據的。代碼如下所示,通過調用cJSON_parse函數返回JSON樹的根節點,其執行流程如圖2所示,原始的輸入JSON_text是存在字符數組(char *)中的,在解析的時候利用指針逐字符判斷當前數據的類型,根據該類型進入相應的分支解析數據;接着只要調用cJSON_GetObjectItem就可以獲取指定名字的結點。

cJSON *root_parse = cJSON_Parse(JSON_data);
cJSON *format = cJSON_GetObjectItem(root_parse,"format");
char *name = cJSON_GetObjectItem(root_parse, "name")->valuestring;
char *type = cJSON_GetObjectItem(format, "type")->valuestring;
int width = cJSON_GetObjectItem(format, "width")->valueint;
int height = cJSON_GetObjectItem(format, "height")->valueint;
int interlace = cJSON_GetObjectItem(format, "interlace")->valueint;
int framerate = cJSON_GetObjectItem(format,"frame rate")->valueint;

                                                          圖2 cJSON_parse 函數執行流程

由於null,false,true類型的數據解析簡單,這裏主要介紹string、number、array、object的解析,源碼就不貼了,可以在源文件中找到對應函數來看。

parse_string:初始化兩個指針,一個指向源字符數組,另一個指向存放解析後字符的目標數組,基本原理是遍歷源字符數組,如果是合法字符則拷貝到目標數組,否則做相應處理。其複雜性在於要處理各種轉義字符,特別是編碼的轉換;
       parse_number:基本原理是將ASCII值轉十進制,同時考慮浮點數和科學計數法的情況;
       parse_array:將同一層結點解析成雙向鏈表,該函數主要是遞歸調用了parse_value這個函數;
       parse_object:該函數也是主要遞歸調用了parse_value這個函數,以此解析對象數據。

 

至此,cJSON庫的核心功能差不多這些了,其他的細節以及值得學習借鑑的技術有待日後琢磨總結,最後看一下cJSON庫生成和解析JSON數據的效果。

generate JSON data: 
{
	"name":	"Jack (\"Bee\") Nimble",
	"format":	{
		"type":	"rect",
		"width":	1920,
		"height":	1080,
		"interlace":	false,
		"frame rate":	24
	}
}
 
JSON parse result: 
name: Jack ("Bee") Nimble
type: rect
width: 1920
height: 1080
interlace: 0
framerate: 24

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章