JSON 是一种轻量级的数据格式,一般用于数据交互,服务器返回给客户端的数据,一般都是 JSON 格式或者 XML 格式(文件下载除外),所以学会如何解析服务器返回回来的数据,对于研究哪些 JSON -> Model 很重要。

JSON 初识

JSON 的格式很像 OC 中的字典和数组:

1
2
{"name": "jack", "age": 10} // 类似于 OC 中的字典
{"names": ["jack", "rose", "jim"]} // 类似于 OC 中的字典,key 为字符串,value 为数组

注意:标准 JSON 格式的注意点:key 必须用双引号。

要想从 JSON 中挖掘出具体数据,得对 JSON 进行解析 -> JSON 转换为 OC 数据类型。

JSON 和 OC 对象转换表:

JSON OC
大括号 {} NSDictionary 即 @{}
中括号 [] NSArray即@[]
双引号 “” NSString即@””
数字 10,11.11 NSNumber 即 @10,@11.11
布尔值 true,false NSNumber 即 @1, @0
空的 null NSNull 即 [NSNull null]

比如如下 JSON:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"name" : "angelen",
"age" : 24,
"height" : 1.72,
"school" : {
"name" : "小良",
"address" : "深圳市南山区"
},
"photos" : [
{
"icon" : "beauty.png",
"time" : "2014-01-01"
},
{
"icon" : "nice.png",
"time" : "2014-02-07"
},
{
"icon" : "good.png",
"time" : "2014-05-06"
}
]
}

可以看到,最外层是 NSDictionary,然后 school 的 value 是 NSDictionary,photos 的 value 是 NSArray(每个元素都是 NSDictionary)。因此转换成 OC 对象如下:

JSON -> OC

JSON 解析方案

知道了转换的原理之后,服务端返回的数据(NSData)要经过处理才能使用。在 iOS 中,JSON 的常见解析方案有 2 大种:

  • 第三方框架:JSONKit、SBJson、TouchJSON(性能从左到右,越差,注:这些库我都没有用过)
  • 苹果原生(自带):NSJSONSerialization(性能最好)

NSJSONSerialization 的常见方法

1
2
3
4
5
// Create a Foundation object from JSON data. JSON 数据 -> OC 对象
+ (nullable id)JSONObjectWithData:(NSData *)data options:(NSJSONReadingOptions)opt error:(NSError **)error;
// Generate JSON data from a Foundation object. OC 对象 -> JSON 数据
+ (nullable NSData *)dataWithJSONObject:(id)obj options:(NSJSONWritingOptions)opt error:(NSError **)error;

解析来自服务器的 JSON

解析来自服务器的 JSON

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// JSON -> OC
- (void)json2Objc {
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://120.25.226.186:32812/login?username=angelen&pwd=123456"]];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
// 这里只是为了演示 JSON -> OC
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil]; // kNilOptions 是最优性能的,其他值可以直接看文档
NSLog(@"dict = \n%@", dict);
}];
}
// 附:OC -> JSON,有时候做支付宝支付什么的需要传递 JSON 参数
- (void)objc2JSON {
// NSArray -> JSON
NSData *data01 = [NSJSONSerialization dataWithJSONObject:@[@{@"name": @"小良"}, @{@"name": @"小鸭"}, @{@"name": @"小鸡"}] options:NSJSONWritingPrettyPrinted error:nil];
NSString *str01 = [[NSString alloc] initWithData:data01 encoding:NSUTF8StringEncoding];
NSLog(@"str01 = \n%@", str01);
// NSDictionary -> JSON
NSData *data02 = [NSJSONSerialization dataWithJSONObject:@{@"name": @"小良", @"age": @24, @"great": [NSNumber numberWithBool:YES]} options:NSJSONWritingPrettyPrinted error:nil];
NSString *str02 = [[NSString alloc] initWithData:data02 encoding:NSUTF8StringEncoding];
NSLog(@"str02 = \n%@", str02);
}

比如解析一个稍微复杂一点的 JSON:

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
32
33
34
35
{
"videos": [
{
"id": 1,
"image": "resources/images/minion_01.png",
"length": 10,
"name": "小黄人 第01部",
"url": "resources/videos/minion_01.mp4"
},
{
"id": 2,
"image": "resources/images/minion_02.png",
"length": 12,
"name": "小黄人 第02部",
"url": "resources/videos/minion_02.mp4"
},
... ...
{
"id": 15,
"image": "resources/images/minion_15.png",
"length": 38,
"name": "小黄人 第15部",
"url": "resources/videos/minion_15.mp4"
},
{
"id": 16,
"image": "resources/images/minion_16.png",
"length": 40,
"name": "小黄人 第16部",
"url": "resources/videos/minion_16.mp4"
}
]
}
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
32
33
34
35
36
37
38
39
40
41
42
@interface ViewController ()
// 所有视频数据(每个元素为字典)
@property (strong, nonatomic) NSArray *videos;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://120.25.226.186:32812/video"]];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
// 这里只是为了演示 JSON -> OC,不要在意太多细节
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
NSLog(@"dict = \n%@", dict);
self.videos = dict[@"videos"];
// 可以直接刷新 UI,因为我们这里用的队列是主队列
[self.tableView reloadData];
}];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.videos.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *kReusableCellIdentifier = @"VideoCell"; // 已经在 StoryBoard 注册好
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kReusableCellIdentifier forIndexPath:indexPath];
NSDictionary *video = self.videos[indexPath.row];
cell.textLabel.text = video[@"name"];
// 注意 id 和 time 都是 NSNumber 对象
cell.detailTextLabel.text = [NSString stringWithFormat:@"id = %@, time = %@", video[@"id"], video[@"length"]];
// -[__NSCFNumber isEqualToString:]: unrecognized selector sent to instance 0xb0000000000000a3 因为拿到 length 不是字符串
// cell.detailTextLabel.text = video[@"length"];
return cell;
}

解析来自服务器的 JSON 2

小 tips:

  • 为了看 JSON 方便,可以直接在电脑弄一个 plist 文件(当前现在已经有很多工具可以直接用)
1
[dict writeToFile:@"/Users/angelen/Desktop/dict.plist" atomically:YES];
  • 使用 Apple 原生播放器原来只需一两行代码:
1
2
3
4
5
6
7
8
9
10
11
12
NSURL *url = [NSURL URLWithString:@"http://wvideo.spriteapp.cn/video/2016/0328/56f8ec01d9bfe_wpc.mp4"];
#import <MediaPlayer/MediaPlayer.h>
// 已过时
// MPMoviePlayerViewController *vc = [[MPMoviePlayerViewController alloc] initWithContentURL:url];
// [self presentViewController:vc animated:YES completion:nil];
#import <AVKit/AVKit.h>
#import <AVFoundation/AVFoundation.h>
AVPlayerViewController *vc = [[AVPlayerViewController alloc] init];
vc.player = [AVPlayer playerWithURL:url];
[self presentViewController:vc animated:YES completion:nil];
  • 这个方法
1
+ (nullable id)JSONObjectWithData:(NSData *)data options:(NSJSONReadingOptions)opt error:(NSError **)error;

的 NSJSONReadingOptions 有以下几个值:

1
2
3
4
5
6
7
8
9
10
typedef enum NSJSONReadingOptions : NSUInteger {
NSJSONReadingMutableContainers = (1UL << 0), // 创建出来的数组或者字典是可变的(mutable)
NSJSONReadingMutableLeaves = (1UL << 1), // 数组或者字典里面的字符串是可变的
NSJSONReadingAllowFragments = (1UL << 2) // 允许解析出来的对象不是数组或者字典,比如直接是字符串或者 NSNumber
} NSJSONReadingOptions;
// MacType.h 定义,就是 0
enum {
kNilOptions = 0
};

JSON -> Model

一般在开发中,我们都是将返回回来的 JSON 数据转换成 Model 使用比较方便,也好管理。我们一般会定义一个 Model 文件(属性和字段对应,这里暂时和字段一模一样,返回多少个字段也要对应):

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
32
33
34
35
36
37
38
39
40
41
42
43
ZLVideo.h
#import <Foundation/Foundation.h>
@interface ZLVideo : NSObject
/** id */
@property (assign, nonatomic) NSUInteger id;
/** image */
@property (strong, nonatomic) NSString *image;
/** length */
@property (assign, nonatomic) NSUInteger length;
/** name */
@property (strong, nonatomic) NSString *name;
/** url */
@property (strong, nonatomic) NSString *url;
// dict -> model, use Key-Value Coding(KVC)
+ (instancetype)videoWithDict:(NSDictionary *)dict;
@end
//"id": 1,
//"image": "resources/images/minion_01.png",
//"length": 10,
//"name": "小黄人 第01部",
//"url": "resources/videos/minion_01.mp4"
ZLVideo.m
#import "ZLVideo.h"
@implementation ZLVideo
+ (instancetype)videoWithDict:(NSDictionary *)dict {
ZLVideo *video = [[self alloc] init];
[video setValuesForKeysWithDictionary:dict];
return video;
}
@end

然后可以直接使用:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#import "ViewController.h"
#import "ZLVideo.h"
@interface ViewController ()
// 所有视频数据(每个元素为 ZLVideo)
@property (strong, nonatomic) NSArray<ZLVideo *> *videos;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://120.25.226.186:32812/video"]];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
// 这里只是为了演示 JSON -> OC,不要在意太多细节
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
// 这个得到的是字典数组
NSArray *dictArray = dict[@"videos"];
// 临时用来装模型数组的
NSMutableArray *videoArray = [NSMutableArray array];
for (NSDictionary *dict in dictArray) {
[videoArray addObject:[ZLVideo videoWithDict:dict]];
}
self.videos = videoArray;
[self.tableView reloadData];
}];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.videos.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *kReusableCellIdentifier = @"VideoCell"; // 已经在 StoryBoard 注册好
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kReusableCellIdentifier forIndexPath:indexPath];
ZLVideo *video = self.videos[indexPath.row];
cell.textLabel.text = video.name;
// 注意 id 和 time 都是 NSNumber 对象
cell.detailTextLabel.text = [NSString stringWithFormat:@"id = %zd, time = %zd", video.id, video.length];
return cell;
}
@end

但是,这种做法有很大的弊端就是:

属性要和返回回来的字段一模一样(像 id、description 等应该定义成其他的)、属性要和返回回来的字段数量相等,否则还得在 Model 中这样写:

1
2
3
4
5
6
7
8
9
10
+ (instancetype)videoWithDict:(NSDictionary *)dict {
ZLVideo *video = [[self alloc] init];
// [video setValuesForKeysWithDictionary:dict];
video.id = [dict[@"id"] integerValue];
video.image = dict[@"image"];
video.length = [dict[@"length"] integerValue];
video.name = dict[@"name"];
video.url = dict[@"url"];
return video;
}

这样子就可以让属性和返回回来的字段不一定要一模一样,而且数量也可以不等,但是很麻烦的说。

因此一般来说,在 JSON -> Model 的时候使用框架来转换,可以看这篇文章:http://blog.ibireme.com/2015/10/23/ios_model_framework_benchmark/

  • Manually

    手动进行 JSON/Model 转换,不用任何开源库,可以进行高效、自由的转换,但手写代码非常繁琐,而且容易出错。

  • YYModel

    ibireme 造的一个新轮子,比较轻量(算上 .h 只有 5 个文件),支持自动的 JSON/Model 转换,支持定义映射过程。API 简洁,功能也比较简单。

  • FastEasyMapping

    Yalantis 开发的一个 JSON 模型转换库,可以自定义详细的 Model 映射过程,支持 CoreData。使用者较少。

  • JSONModel

    一个 JSON 模型转换库,有着比较简洁的接口。Model 需要继承自 JSONModel。

  • Mantle

    Github 官方团队开发的 JSON 模型转换库,Model 需要继承自 MTLModel。功能丰富,文档完善,使用广泛。

  • MJExtension

    国内开发者『小码哥』开发的 JSON 模型库,号称性能超过 JSONModel 和 Mantle,使用简单无侵入。国内有大量使用者。

设计框架需要考虑的一些问题:

  • 侵入性

    侵入性大就意味这一旦用上这个框架就很难离开这个框架

  • 易用性

    可以用少量代码实现比较多的功能

  • 扩展性

    以后加入新功能会比较简单