cJSON库源码分析

摘要:
CJSON是一个超轻量级、可移植、单文件、简单的,可以用作ANSI-C标准的Json格式解析库。这些特性使JSON成为理想的数据交换语言。事实上,Json是一种信息交换格式,而cJSON实际上是一个C语言函数库,它以Json格式构造和解析字符串。只需阅读README文件并学习如何使用cJSON库。通过简单的理解,我们知道cJSON库实际上由cJSON组成。c和cJSON。h、 它们是绝对轻质的。

本文采用以下协议进行授权:自由转载-非衍生-保持署名|Creative Commons BY-NC-ND 3.0 ,转载请注明作者及出处。    

        cJSON是一个超轻巧,携带方便,单文件,简单的可以作为ANSI-C标准的Json格式解析库。

         那什么是Json格式?这里照搬度娘百科的说法:

         Json(JavaScript Object Notation) 是一种轻量级的数据交换格式。它基于JavaScript(Standard ECMA-262 3rd Edition – December 1999)的一个子集。JSON采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C, C++, C#, Java, JavaScript, Perl, Python等)。这些特性使JSON成为理想的数据交换语言。易于人阅读和编写,同时也易于机器解析和生成。

         更加详细的解释和示例请查看 http://www.json.org/ 主页。

         其实简单说,Json就是一种信息交换格式,而cJSON其实就是对Json格式的字符串进行构建和解析的一个C语言函数库。

         可以在以下地址下载到cJSON的源代码:

         http://sourceforge.net/projects/cjson/

         __MACOSX目录是提供给Mac OS的源码,我的机器运行的是Fedora 18,所以选择另外一个目录即可。

         简单的阅读下README文件,先学习cJSON库的使用方法。若是连库都还不会使用,分析源码就无从谈起了。通过简单的了解,我们得知cJSON库实际上只有cJSON.c和cJSON.h两个文件组成,绝对轻量级。

         不过,代码风格貌似有点非主流,先用indent格式化一下代码吧。我个人喜欢K&R风格的代码,使用的indent命令行参数如下:

1

indent - bad - bli 0 - ce - kr - nsob -- space - after - if -- space - after - while -- space - after - for -- use - tabs - i8

         格式化之后,代码结构看起来清晰多了。

         那么,从何处下手来分析呢?打开代码文件逐行阅读么?当然不是了,有main函数的程序大都是从main函数开始分析,那么没有main函数的纯函数库呢?那就自己写main函数呗。

         cJSON作为Json格式的解析库,其主要功能无非就是构建和解析Json格式了,我们先写一个构建Json格式字符串的程序,尽可能的使其用到的类型多一点(事实上README文件里提供了不错的示例代码,我们直接借鉴一下吧)。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

#include <stdio.h>

#include <stdlib.h>

    

#include "cJSON.h"

    

int main ( int argc , char * argv [ ] )

{

     cJSON * root , * fmt ;

            

     root = cJSON_CreateObject ( ) ;

     cJSON_AddStringToObject ( root , "name" , "Jack ("Bee") Nimble" ) ;

     fmt = cJSON_CreateObject ( ) ;

     cJSON_AddItemToObject ( root , "format" , fmt ) ;

     cJSON_AddStringToObject ( fmt , "type" , "rect" ) ;

     cJSON_AddNumberToObject ( fmt , "width" , 1920 ) ;

     cJSON_AddFalseToObject ( fmt , "interlace" ) ;

    

     char * result = cJSON_Print ( root ) ;

     puts ( result ) ;

            

     free ( result ) ;

     cJSON_Delete ( root ) ;

    

     return EXIT_SUCCESS ;

}

        编译运行后 ( 编译时注意要链接数学库,参数行要加 -lm) ,运行结果如下:

1
2
3
4
5
6
7
8
{

     "name" : "Jack ("Bee") Nimble" ,

     "format" : {

         "type" : "rect" ,

         "width" : 1920 ,

         "interlace" : false

     }
}

        打开cJSON.h这个头文件,我们可以看到每一个节点,实际上都是由cJSON这个结构体来描述的:

1
2
3
4
5
6
7
8
9
10
11
12

typedef struct cJSON {

     struct cJSON * next , * prev ;

     struct cJSON * child ;

    
     int type ;
    

     char * valuestring ;

     int valueint ;

     double valuedouble ;

    

     char * string ;

} cJSON ;

        结合这个结构体和上面相关API的调用,其实我们大概可以猜测出cJSON对于Json格式的描述和处理的方法了:

         每一个cJSON结构都描述了一项”键-值”对的数据,其中next和prev指针显然是指向同级前后的cJSON结构,而child指针自然是指向孩子节点的cJSON结构。type类型显然是为了区分值的类型而设置的,在cJSON.h文件一开始就定义了这些类型的值:

1
2
3
4
5
6
7
8

/* 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

        很显然通过检测这里的type字段,就很容易知道该节点的类型以及其实际存储数据的字段了。其它的字段是什么意思呢?cJSON.h文件里的注释说的很明白了,valueint,valuedouble以及valuestring保存的是相应的值,string存放的是本字段的名字。

         接下来分析程序的执行过程,编译参数加上-g,使用gdb调试程序,画出整个构造过程的函数调用图。具体的调试过程就不细说了,我捡一些关键点说说:

        调试过程中,我们发现 cJSON_AddStringToObject() 等其实是宏定义,本质上调用的都是 cJSON_AddItemToObject() 函数,在cJSON.h文件中可以看到如下定义:

1
2
3
4
5
6

#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))

        另外 cJSON_CreateNull() 等函数都是调用 cJSON_New_Item() 函数申请到初始化为0的空间构造相关的节点信息。构造过程中的函数调用图如下:

cJSON库源码分析第1张

         构造的Json字符串最终在内存中形成的结构如下图所示:

cJSON库源码分析第2张

        构造过程相对来说比较简单,数组类型这里没有涉及到,但是分析起来也很简单。

         我们最后调用 cJSON_Print() 函数生成这个结构所对应的字符串。生成说起来容易,遍历起整个结构并进行字符串格式控制却比较繁琐。这里相关的代码还有递归清理这个内存结构的函数不再赘述,有兴趣的同学请自行研究。

         构造的过程我们就说到这里,明天我们研究下解析的过程。


         昨天简单的分析了一下cJSON对Json格式的构造过程,今天仔细读了读README文件,发现README其实说的已经很详细了。重复造轮子就重复造轮子吧,今天我们再一起分析解析的过程。

         继续用之前构造的Json格式来进行解析,之前分析构造函数的时候,我们只是简单的分析了几个cJSON结构的构造过程,并没有涉及到各种类型的数组等构造。因为我觉得理解了一般的构造过程,更复杂的类型自己再简单看看源码,画画图就很容易理解。

          学习一个事物一定要先抓住主线,先掌握一个事物最常用的那50%,其他的边边角角完全可以留给实践去零敲碎打(孟岩语)。

闲话打住,先上一段解析使用的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

#include <stdio.h>

#include <stdlib.h>

#include "cJSON.h"

int main ( int argc , char * argv [ ] )

{

char * text = "{"name": "Jack (\"Bee\") Nimble", "

                         ""format": {"type": "rect", "

                         ""width": 1920, "interlace": false}}" ;

cJSON * root = cJSON_Parse ( text ) ;

if ( ! root ) {

printf ( "Error before: [%s] " , cJSON_GetErrorPtr ( ) ) ;

return EXIT_FAILURE ;

}

char * out = cJSON_Print ( root ) ;

printf ( "text: %s " , out ) ;

free ( out ) ;

char * name = cJSON_GetObjectItem ( root , "name" ) -> valuestring ;

printf ( "name : %s " , name ) ;

cJSON * format = cJSON_GetObjectItem ( root , "format" ) ;

int width = cJSON_GetObjectItem ( format , "width" ) -> valueint ;

printf ( "width : %d " , width ) ;

cJSON_Delete ( root ) ;

return EXIT_SUCCESS ;

}

         程序运行输出:

1
2
3
4
5
6
7
8
9
10
11
12
text :
{

"name" : "Jack ("Bee") Nimble" ,

"format" : {
"type" : "rect" ,
"width" : 1920 ,

"interlace" : false

}
}

name : Jack ( "Bee" ) Nimble

width : 1920

         从这段代码中可以看到,解析过程就 cJSON_Parse() 一个接口,调用成功返回cJSON结构体的指针,错误返回NULL,此时调用 cJSON_GetErrorPtr() 可以得要错误原因的描述字符串。

         查看 cJSON_GetErrorPtr() 的源码可以得知,其实错误信息就保存在全局字符串指针ep里。

         关键就是对 cJSON_Parse() 过程的分析了,我们带参数-g重新编译代码并下断点开始调试跟踪。

         首先 cJSON_Parse() 调用 cJSON_New_Item() 申请一个新的cJSON节点,然后使用函数对输入字符串进行解析(中间使用了 skip() 函数来跳过空格和换行符等字符)。

         parse_value() 函数对输入字符串进行匹配和解析,检测输入数据的类型并调用 parse_string()parse_number()parse_array()parse_object() 等函数进行解析,然后返回结束的位置。

         函数调用的关系如下图:

cJSON库源码分析第3张

         这些函数之间相互调用,传递待解析的字符串直到结束或者遇见错误便返回,最后会构建出一个和之前结构一样的Json内存结构来,解析的过程就完成了。检索过程很简单 cJSON_GetObjectItem() 函数负责进行某个对象的自成员的名字比对和指针的返回。不过要注意这里采用了 cJSON_strcasecmp() 这个无视大小写的字符串比较函数,因为Json格式的键值对的名称不区分大小写。

        这样cJSON库的整个构建和解析过程的主干内容就总结出来了,剩下的边边角角可以在这个主线分析结束之后再继续下去,比如Json格式化,解析出来的内存结构复制,从这个内存结构解析出字符串以及这个内存结构的递归删除等等留给大家自己进行吧。

         P.S. cJSON_InitHooks() 这个函数不过是cJSON允许用户使用其它的内存申请和释放函数罢了(默认是malloc和free),另外啰嗦一下,这个接口也可以用来检测内存泄露。只要实现malloc和free的包装函数,在其中统计和打印内存申请释放操作就可以了。

若无特别声明,本站文章皆为原创,转载时烦请注明: 转载自 浅墨的博客

免责声明:文章转载自《cJSON库源码分析》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Win10离线安装.NET Framework 3.5的方法技巧(附离线安装包下载)UCOSIII任务创建下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

修改apk里面的源码

1.解压apk文件,获取classes.dex并拷贝到资源根目录(使用zip或其他解压工具即可) 2.使用baksmali工具将classes.dex转为smali文件,在命令行定位到资源根目录并执行: java -jar baksmali-2.0.3.jar -x classes.dex 执行完后会在当前目录下生成out目录,目录结构跟源码相同,在对应...

【嵌入式开发】 Linux Kernel 下载 配置 编译 安装 及 驱动简介

作者 : 韩曙亮 转载请出名出处 : http://blog.csdn.net/shulianghan/article/details/38636827 一. Linux 内核简介  1. 内核功能简介 (1) 操作系统 和 内核 简介 操作系统 :  -- 功能 : 完成基本功能 和 系统管理; -- 组成 : 内核(kernel), 设备...

C# 抓取网页Html源码 (网络爬虫)

http://www.cnblogs.com/wxxian001/archive/2011/09/07/2169519.html 刚刚完成一个简单的网络爬虫,因为在做的时候在网上像无头苍蝇一样找资料。发现了很多的资料,不过真正能达到我需要,有用的资料--代码很难找。所以我想发这篇文章让一些要做这个功能的朋友少走一些弯路。 首先是抓取Html源码,并选择&l...

【MyBatis源码分析】Configuration加载(下篇)

元素设置 继续MyBatis的Configuration加载源码分析: 1 private void parseConfiguration(XNode root) { 2 try { 3 Properties settings = settingsAsPropertiess(root.evalNode("settings"))...

[转]开发者必备的6款源码搜索引擎

From : http://news.cnblogs.com/n/184662/   英文原文:Open Source Matters: 6 Source Code Search Engines You Can Use For Programming Projects   在推动技术变革上,开源运动发挥了非常显著的作用。而 Linux 成功地将开源转换成...

eclipse导入redis的源码

import--c/c++ Executable  选择编译后的so文件。这样就导入工程了。 可以在eclipse直接修改c代码,重新编译后就能看到效果了。 重新编译:   1:make clean   2:make CFLAGS='-g -O0' 表示代码编译的时候不进行优化。...