cJSON源碼分析

cJSON是C語言中的一個JSON編解碼器,非常輕量級,C文件只有不到一千行,代碼的可讀性也很好,很適合作為C語言項目進行學習。項目主頁:
https://sourceforge.net/projects/cjson/

對于json格式編碼與解碼,其實就是類似于一個解釋器,主要原理還是運用遞歸。個人認為,如果能用一些支持面向?qū)ο蟮恼Z言來做這個項目,代碼實現(xiàn)起來應該會更加優(yōu)雅。

先來看一下cJSON的數(shù)據(jù)結(jié)構(gòu):

/* The cJSON structure: */
typedef struct cJSON {
    struct cJSON *next,*prev;   /* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
    struct cJSON *child;        /* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */

    int type;                   /* The type of the item, as above. */

    char *valuestring;          /* The item's string, if type==cJSON_String */
    int valueint;               /* The item's number, if type==cJSON_Number */
    double valuedouble;         /* The item's number, if type==cJSON_Number */

    char *string;               /* The item's name string, if this item is the child of, or is in the list of subitems of an object. */
} cJSON;

不管是數(shù)值類型、字符串類型或者對象類型等都使用該結(jié)構(gòu)體,類型信息通過標識符 type來進行判斷,cJSON總共定義了7種類型:

/* cJSON Types: */
#define cJSON_False 0
#define cJSON_True 1
#define cJSON_NULL 2
#define cJSON_Number 3
#define cJSON_String 4
#define cJSON_Array 5
#define cJSON_Object 6

另外,如果是對象或者數(shù)組,采用的是雙向鏈表來實現(xiàn),鏈表中的每一個節(jié)點表示數(shù)組中的一個元素或者對象中的一個字段。其中child表示頭結(jié)點,next、prev分別表示下一個節(jié)點和前一個節(jié)點。valuestring、valueint、valuedouble分別表示字符串、整數(shù)、浮點數(shù)的字面量。string表示對象中某一字段的名稱,比如有這樣的一個json字符串:

{'age': 20}

'age'則用結(jié)構(gòu)體中的string來表示。

cJSON的api使用起來非常簡單:

char *out;cJSON *json;
    
json=cJSON_Parse(text);
if (!json) {
    printf("Error before: [%s]\n",cJSON_GetErrorPtr());
} else {
    out=cJSON_Print(json);
    cJSON_Delete(json);
    printf("%s\n",out);
    free(out);
}

代碼都是自解釋的就不啰嗦,唯一需要注意的是使用完成后,必須釋放內(nèi)存,以免內(nèi)存泄露。

JSON的解析

下面我們先來看一下json字符串的解析,json的字符串的解析主要是通過cJSON_Parse函數(shù)來完成,打開cJSON_Parse函數(shù)后,我們發(fā)現(xiàn)該函數(shù)使用了另外一個輔助函數(shù):

/* Default options for cJSON_Parse */
cJSON *cJSON_Parse(const char *value) {
    return cJSON_ParseWithOpts(value,0,0);
}

cJSON_ParseWithOpts提供了一些額外的參數(shù)選項:

/* Parse an object - create a new root, and populate. */
cJSON *cJSON_ParseWithOpts(const char *value, const char **return_parse_end, int require_null_terminated) {
    const char *end = 0;
    cJSON *c = cJSON_New_Item();
    ep = 0;
    if (!c) return 0;       /* memory fail */

    end = parse_value(c, skip(value));
    if (!end) {
        cJSON_Delete(c);
        return 0;
    }    /* parse failure. ep is set. */

    /* if we require null-terminated JSON without appended garbage, skip and then check for a null terminator */
    if (require_null_terminated) {
        end = skip(end);
        if (*end) {
            cJSON_Delete(c);
            ep = end;
            return 0;
        }
    }
    if (return_parse_end) *return_parse_end = end;
    return c;
}

第一步:先調(diào)用cJSON_New_Item創(chuàng)建一個節(jié)點,該函數(shù)實現(xiàn)非常簡單,就是使用malloc分配一塊內(nèi)存,再將分配的內(nèi)存使用0來進行初始化。

/* Internal constructor. */
static cJSON *cJSON_New_Item(void)
{
    cJSON* node = (cJSON*)cJSON_malloc(sizeof(cJSON));
    if (node) memset(node,0,sizeof(cJSON));
    return node;
}

第二步:調(diào)用parse_value函數(shù)進行真正的解析,該函數(shù)是json解析的核心部分,后面我們會重點分析。而在解析前,先對json字符串調(diào)用了一次skip,其實就是將字符串前面的的一些空字符去除,代碼如下:

/* Utility to jump whitespace and cr/lf */
static const char *skip(const char *in) {
    while (in && *in && (unsigned char) *in <= 32) in++;
    return in;
}

最后一步:函數(shù)中參數(shù)中提供了require_null_terminated是為了確保json字符串必須以'\0'字符作為結(jié)尾。若參數(shù)提供了return_parse_end,將返回json字符串解析完成后剩余的部分。

下面來看json解析算法的核心部分:

static const char *parse_value(cJSON *item, const char *value) {
    if (!value) return 0;    /* Fail on null. */
    //若字符串等于null,直接將type標記為cJSON_NULL
    if (!strncmp(value, "null", 4)) {
        item->type = cJSON_NULL;
        return value + 4;
    }
    //若字符串等于false,直接將type標記為cJSON_False
    if (!strncmp(value, "false", 5)) {
        item->type = cJSON_False;
        return value + 5;
    }
    //若字符串等于true,直接將type標記為cJSON_True
    if (!strncmp(value, "true", 4)) {
        item->type = cJSON_True;
        item->valueint = 1;
        return value + 4;
    }
    //若字符串以\"開頭,說明是一個字符串
    if (*value == '\"') { return parse_string(item, value); }
    //若字符串以0~9或者-開頭,則是一個數(shù)值
    if (*value == '-' || (*value >= '0' && *value <= '9')) { return parse_number(item, value); }
    //若字符串以[開頭,則是一個數(shù)組
    if (*value == '[') { return parse_array(item, value); }
    //若字符串以{開頭,則是一個對象
    if (*value == '{') { return parse_object(item, value); }

    ep = value;
    return 0;    /* failure. */
}

上述 代碼看上去應該清晰易懂,且都已加入注釋。對于json為null、true或者false的情況,直接將type置為對應的類型即可。對于其他情況,需要分別再做處理,先來看json為字符串的情況:

static const char *parse_string(cJSON *item, const char *str) {
    const char *ptr = str + 1;
    char *ptr2;
    char *out;
    int len = 0;
    unsigned uc, uc2;
    if (*str != '\"') {
        ep = str;
        return 0;
    }    /* not a string! */

    /* 這一步主要是為了確定字符串的長度,以便下一步進行內(nèi)存分配
     * 問題在于字符串中可能存在一定的轉(zhuǎn)義字符,由于json字符串本身
     * 是一個字符串,碰到像雙引號必須進行轉(zhuǎn)義,另外像\t這樣的轉(zhuǎn)義
     * 字符,需要再次轉(zhuǎn)義,用\\t來表示。而在解析的時候,我們不需要
     * 這些多余的轉(zhuǎn)義字符。(說得有點繞,不知道能不能看明白)
     */
    while (*ptr != '\"' && *ptr && ++len) 
        if (*ptr++ == '\\') ptr++;   

    //分配內(nèi)存,保存解析后的結(jié)果
    out = (char *) cJSON_malloc(len + 1);
    if (!out) return 0;

    ptr = str + 1; //跳過第一個\"
    ptr2 = out;
    //當遇到\"時,說明到達字符串的末尾
    while (*ptr != '\"' && *ptr) {
        //如果不是轉(zhuǎn)義字符,直接原樣復制
        if (*ptr != '\\') *ptr2++ = *ptr++;
        else { //碰到像\\b的再轉(zhuǎn)義字符,改成\b
            ptr++;
            switch (*ptr) {
                case 'b':
                    *ptr2++ = '\b';
                    break;
                case 'f':
                    *ptr2++ = '\f';
                    break;
                case 'n':
                    *ptr2++ = '\n';
                    break;
                case 'r':
                    *ptr2++ = '\r';
                    break;
                case 't':
                    *ptr2++ = '\t';
                    break;
                case 'u':     /*這里將 utf16 轉(zhuǎn)為 utf8,算法比較復雜,不做分析 */
                    uc = parse_hex4(ptr + 1);
                    ptr += 4;    /* get the unicode char. */

                    if ((uc >= 0xDC00 && uc <= 0xDFFF) || uc == 0) break;    /* check for invalid.  */

                    if (uc >= 0xD800 && uc <= 0xDBFF)    /* UTF16 surrogate pairs.  */
                    {
                        if (ptr[1] != '\\' || ptr[2] != 'u') break;    /* missing second-half of surrogate. */
                        uc2 = parse_hex4(ptr + 3);
                        ptr += 6;
                        if (uc2 < 0xDC00 || uc2 > 0xDFFF) break;    /* invalid second-half of surrogate.    */
                        uc = 0x10000 + (((uc & 0x3FF) << 10) | (uc2 & 0x3FF));
                    }

                    len = 4;
                    if (uc < 0x80) len = 1; else if (uc < 0x800) len = 2; else if (uc < 0x10000) len = 3;
                    ptr2 += len;

                    switch (len) {
                        case 4:
                            *--ptr2 = ((uc | 0x80) & 0xBF);
                            uc >>= 6;
                        case 3:
                            *--ptr2 = ((uc | 0x80) & 0xBF);
                            uc >>= 6;
                        case 2:
                            *--ptr2 = ((uc | 0x80) & 0xBF);
                            uc >>= 6;
                        case 1:
                            *--ptr2 = (uc | firstByteMark[len]);
                    }
                    ptr2 += len;
                    break;
                default:
                    *ptr2++ = *ptr;
                    break;
            }
            ptr++;
        }
    }
    *ptr2 = 0;
    if (*ptr == '\"') ptr++;
    item->valuestring = out;
    item->type = cJSON_String;
    return ptr;
}

解析字符串的困難之處在于可能會碰到轉(zhuǎn)義字符,需要將類似于\\t這樣的再轉(zhuǎn)義字符轉(zhuǎn)化為\t,對于如何將utf16轉(zhuǎn)為utf8的算法比較復雜,我就不做分析了。其他情況原樣復制即可。

下面是解析數(shù)值類型的代碼:

static const char *parse_number(cJSON *item, const char *num) {
    double n = 0, sign = 1, scale = 0;
    int subscale = 0, signsubscale = 1;

    if (*num == '-') sign = -1, num++;    /*如果是負數(shù),將sign置位-1 */
    if (*num == '0') num++;            /* 如果是0,直接越過 */
    if (*num >= '1' && *num <= '9') /* 碰到數(shù)字1~9,用循環(huán)計算整數(shù)部分的數(shù)值 */
        do n = (n * 10.0) + (*num++ - '0'); while (*num >= '0' && *num <= '9');    
    if (*num == '.' && num[1] >= '0' && num[1] <= '9') { /* 遇到小數(shù)點,計算小數(shù)位的數(shù)值,用scale來標記小數(shù)有多少位 */
        num++;
        do n = (n * 10.0) + (*num++ - '0'), scale--; while (*num >= '0' && *num <= '9');
    }    
    if (*num == 'e' || *num == 'E')     /* 遇到指數(shù),計算指數(shù)位的數(shù)值 */
    {
        num++;
        if (*num == '+') num++; else if (*num == '-') signsubscale = -1, num++;       
        while (*num >= '0' && *num <= '9') subscale = (subscale * 10) + (*num++ - '0');   
    }

   /* 最后根據(jù)前面的得到的數(shù)值,計算最終的結(jié)果 */
    n = sign * n * pow(10.0, (scale + subscale * signsubscale));    /* number = +/- number.fraction * 10^+/- exponent */

    item->valuedouble = n;
    item->valueint = (int) n;
    item->type = cJSON_Number;
    return num;
}

這部分代碼也不難,花點時間應該就能看懂。主要是考慮了小數(shù)和指數(shù)的情況,會帶來一定的復雜性。 吐槽一下作者把if語句塊中的代碼都寫在一行上,導致看起來很費勁。

接下來是如何解析數(shù)組:

static const char *parse_array(cJSON *item, const char *value) {
    cJSON *child;
    if (*value != '[') { //不是一個數(shù)組,直接返回
        ep = value;
        return 0;
    }   

    item->type = cJSON_Array;
    value = skip(value + 1); //跳過前面的空白字符
    if (*value == ']') return value + 1;    /* 空數(shù)組 */

    //創(chuàng)建數(shù)組的頭結(jié)點,表示數(shù)組的第一個元素
    item->child = child = cJSON_New_Item();
    if (!item->child) return 0;      
    //對數(shù)組中的第一個元素遞歸調(diào)用parse_value 
    value = skip(parse_value(child, skip(value))); 
    if (!value) return 0;

    //如果數(shù)組第一個元素后面存在逗號,說明還有其他元素
    //對剩下的元素同樣遞歸調(diào)用parse_value,并將后面的元素
    //放在child節(jié)點的尾部,形成一個鏈表
    while (*value == ',') {
        cJSON *new_item;
        if (!(new_item = cJSON_New_Item())) return 0;   
        //移動指針
        child->next = new_item;
        new_item->prev = child;
        child = new_item;
        
        value = skip(parse_value(child, skip(value + 1)));
        if (!value) return 0;
    }

    if (*value == ']') return value + 1;    /* 達到數(shù)組的尾部 */
    ep = value;
    return 0;
}

解析數(shù)組時,其基本思想是對數(shù)組中每一個元素遞歸調(diào)用parse_value,再將這些元素連接形成一個鏈表。

如果能看懂數(shù)組的解析過程,對象的解析對你來說應該也不難,直接來看代碼:

static const char *parse_object(cJSON *item, const char *value) {
    cJSON *child;
    if (*value != '{') {
        ep = value;
        return 0;
    }    //不是一個對象,直接返回

    item->type = cJSON_Object;
    value = skip(value + 1);
    if (*value == '}') return value + 1;    /* 空對象 */

    //創(chuàng)建節(jié)點,用于保存對象中的第一個字段
    item->child = child = cJSON_New_Item();
    if (!item->child) return 0;
    
    //調(diào)用parse_string,解析第一個字段的名稱
    value = skip(parse_string(child, skip(value)));
    if (!value) return 0;
    child->string = child->valuestring;
    child->valuestring = 0;

    //如果字段名稱后面不是冒號,則不是一個合法的json字符串,直接返回
    if (*value != ':') {
        ep = value;
        return 0;
    }    

    //遞歸調(diào)用parse_value解析第一個字段的值
    value = skip(parse_value(child, skip(value + 1)));    
    if (!value) return 0;

   //若第一個字段后面存在逗號,則說明json對象存在其他的字段
   //后面的步驟基本跟解析數(shù)組時相同,也就是將各個字段放在
   //child節(jié)點后面,形成一個鏈表,唯一不同的是需要解析每個
   //字段的名稱
    while (*value == ',') {
        cJSON *new_item;
        if (!(new_item = cJSON_New_Item())) return 0; 
        child->next = new_item;
        new_item->prev = child;
        child = new_item;
        value = skip(parse_string(child, skip(value + 1)));
        if (!value) return 0;
        child->string = child->valuestring;
        child->valuestring = 0;
        if (*value != ':') {
            ep = value;
            return 0;
        }  
        value = skip(parse_value(child, skip(value + 1)));   
        if (!value) return 0;
    }

    if (*value == '}') return value + 1;    /* 達到對象的末尾 */
    ep = value;
    return 0; 
}

最后,客戶端代碼在使用完成后,需要將分配的內(nèi)存釋放掉:

void cJSON_Delete(cJSON *c) {
    cJSON *next;
    while (c) { //循環(huán)刪除鏈表中所有節(jié)點
        next = c->next;
        if (c->child) cJSON_Delete(c->child); //存在子節(jié)點,遞歸刪除
        if (c->valuestring) cJSON_free(c->valuestring); 
        if (c->string) cJSON_free(c->string);
        cJSON_free(c); //刪除結(jié)構(gòu)體本身
        c = next;
    }
}

cJSON對于json字符串的解析基本就結(jié)束了。后面還有關(guān)于一個生成好的json對象如何打印,其實就是解析的逆向過程,我就不贅述了。老實說,代碼的基本原理很簡單,無非就是使用遞歸,如果讓我寫個demo,花一個小時就能寫出來。但是這樣的代碼之所以值得我去閱讀,不光是對于代碼的風格、設(shè)計還有可讀性上。更重要的是,作者能考慮到很多我不會去考慮的東西,比如說對于字符串的解析,如果是我自己寫,可能不會考慮存在轉(zhuǎn)義字符或者utf16的情況。在解析數(shù)值時,也不會考慮存在指數(shù)的情況。如果給定的字符串不是一個合法的json字符串,又該去如何處理?這也是一個代碼新手做出來的東西與一款成熟產(chǎn)品的區(qū)別所在。

如果有時間,我想根據(jù)cJSON的思路,用java面向?qū)ο蟮姆绞絹磉M行實現(xiàn),并且在這基礎(chǔ)上加入類型信息,而不是通過一個type標識符來判斷類型。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 第5章 引用類型(返回首頁) 本章內(nèi)容 使用對象 創(chuàng)建并操作數(shù)組 理解基本的JavaScript類型 使用基本類型...
    大學一百閱讀 3,663評論 0 4
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,502評論 19 139
  • 前段時間,聽了阿何的網(wǎng)絡(luò)課程《引爆你的學習力》,里面提到了一種學習方法叫“三步式快速學習法”,已經(jīng)嘗試著用了一段時...
    w王大銘閱讀 1,320評論 0 2
  • 他站在校園門口 用他最好的時光 沒有一絲不耐煩 只是聽著音樂 站著 很多人走了以后的以后 一個女孩捧著一摞書走出來...
    鄙人十九閱讀 249評論 0 0
  • 作者:Dave Calhoun(Time Out London電影編輯) 編譯:haru 刊于:Time Out ...
    haru閱讀 747評論 0 3

友情鏈接更多精彩內(nèi)容