【转帖】iOS中的数据解析

转自:http://devlxx.com/ioskai-fa-zhong-de-shu-ju-chu-li/
在一般的iOS中,有很大部分是把从服务端获取到的数据加载到UI上。但是在这个过程中如果处理,才能达到良好的可读性、可扩展、低耦合等效果类?

现在iOS中很大部分都是对JSON数据的处理,所以本文主要拿JSON进行说明。

1、NSNull造成的Crash
记得在刚开始学习iOS的时候,有一次程序莫名Crash掉了。检查后发现原来是本来返回的数据是NSString的,结果解析后得到的是一个NSNull对象,然后将其赋值给一个UILabel对象的text,然后就造成NSInvalidArgumentException异常导致crash。相信不少开发者,都被NSNull坑过,最常见的是服务器返回的json里面,说好的字典、数组、数字,结果返回的是空值。 我们一般做一次类型判断:

#pragma mark - 如果为NSNull,则转换为nil

  • (id)objectOrNilForKey:(id)aKey fromDictionary:(NSDictionary *)dict
    {
    id object = [dict objectForKey:aKey];
    return [object isEqual:[NSNull null]] ? nil : object;
    }

//解析数据
self.title = [self objectOrNilForKey:@”title” fromDictionary:dict];
这样就不会出现把NSNull当做NSString、NSNumber、NSArray等操作而造成的Crash。 后来感觉这个不够优雅,就把它改成了Category(也可改成宏):

@interface NSDictionary (TypeChecking)

  • (id)objectOrNilForKey:(id)aKey;

@end

@implementation NSDictionary (TypeChecking)

  • (id)objectOrNilForKey:(id)aKey
    {
    id object = [self objectForKey:aKey];
    return [object isEqual:[NSNull null]] ? nil : object;
    }

@end

//使用
self.title = [dict objectOrNilForKey:@”title”];
但是后来,了解了OC强大的Runtime。发现原来利用Rumtime我们可以更优雅的处理这个问题:

#define NSNullObjects @[@””,@0,@{},@[]]

@interface NSNull (InternalNullExtention)
@end

@implementation NSNull (InternalNullExtention)

  • (NSMethodSignature)methodSignatureForSelector:(SEL)selector
    {
    NSMethodSignature
    signature = [super methodSignatureForSelector:selector];
    if (!signature) {

    for (NSObject *object in NSNullObjects) { 
        signature = [object methodSignatureForSelector:selector]; 
        if (signature) { 
            break; 
        } 
    } 
    

    }
    return signature;
    }

  • (void)forwardInvocation:(NSInvocation )anInvocation
    {
    SEL aSelector = [anInvocation selector];
    for (NSObject
    object in NSNullObjects) {

    if ([object respondsToSelector:aSelector]) { 
        [anInvocation invokeWithTarget:object];
        return; 
    } 
    

    }
    [self doesNotRecognizeSelector:aSelector];
    }
    @end
    很高端的做法,通过消息转发机制来解决这个问题。如果无法理解这段代码的含义,我们可以看这篇博文Objective-C Runtime。

常规数据解析的不足
假如我们有这样一段JSON数据:

{
“lng” : 121.480263,
“dataSource” : “SHANGHAI”,
“lat” : 31.236295,
“menus” : “测试”,
“name” : “上海”
}
我们一般是创建一个对应的实体类来解析:

@interface City : NSObject

@property (nonatomic, assign) double lng;
@property (nonatomic, strong) NSString dataSource;
@property (nonatomic, assign) double lat;
@property (nonatomic, strong) NSString
menus;
@property (nonatomic, strong) NSString *name;

  • (instancetype)modelObjectWithDictionary:(NSDictionary *)dict;
  • (instancetype)initWithDictionary:(NSDictionary *)dict;
  • (NSDictionary *)dictionaryRepresentation;

@end

#import “City.h”

NSString const kCityLng = @”lng”;
NSString
const kCityDataSource = @”dataSource”;
NSString const kCityLat = @”lat”;
NSString
const kCityMenus = @”menus”;
NSString *const kCityName = @”name”;

@interface City ()

  • (id)objectOrNilForKey:(id)aKey fromDictionary:(NSDictionary *)dict;

@end

@implementation City

@synthesize lng = _lng;
@synthesize dataSource = _dataSource;
@synthesize lat = _lat;
@synthesize menus = _menus;
@synthesize name = _name;

  • (instancetype)modelObjectWithDictionary:(NSDictionary *)dict
    {
    return [[self alloc] initWithDictionary:dict];
    }
  • (instancetype)initWithDictionary:(NSDictionary *)dict
    {
    self = [super init];

    if(self && [dict isKindOfClass:[NSDictionary class]]) {

    self.lng = [[self objectOrNilForKey:kCityLng fromDictionary:dict] doubleValue];
    self.dataSource = [self objectOrNilForKey:kCityDataSource fromDictionary:dict];
    self.lat = [[self objectOrNilForKey:kCityLat fromDictionary:dict] doubleValue];
    self.menus = [self objectOrNilForKey:kCityMenus fromDictionary:dict];
    self.name = [self objectOrNilForKey:kCityName fromDictionary:dict];
    

    }

    return self;

}

  • (NSDictionary )dictionaryRepresentation
    {
    NSMutableDictionary
    mutableDict = [NSMutableDictionary dictionary];
    [mutableDict setValue:[NSNumber numberWithDouble:self.lng] forKey:kCityLng];
    [mutableDict setValue:self.dataSource forKey:kCityDataSource];
    [mutableDict setValue:[NSNumber numberWithDouble:self.lat] forKey:kCityLat];
    [mutableDict setValue:self.menus forKey:kCityMenus];
    [mutableDict setValue:self.name forKey:kCityName];

    return [NSDictionary dictionaryWithDictionary:mutableDict];
    }

  • (NSString *)description
    {
    return [NSString stringWithFormat:@”%@”, [self dictionaryRepresentation]];
    }

#pragma mark - Helper Method

  • (id)objectOrNilForKey:(id)aKey fromDictionary:(NSDictionary *)dict
    {
    id object = [dict objectForKey:aKey];
    return [object isEqual:[NSNull null]] ? nil : object;
    }

#pragma mark - NSCoding Methods

  • (id)initWithCoder:(NSCoder *)aDecoder
    {
    self = [super init];
    self.lng = [aDecoder decodeDoubleForKey:kCityLng];
    self.dataSource = [aDecoder decodeObjectForKey:kCityDataSource];
    self.lat = [aDecoder decodeDoubleForKey:kCityLat];
    self.menus = [aDecoder decodeObjectForKey:kCityMenus];
    self.name = [aDecoder decodeObjectForKey:kCityName];
    return self;
    }

  • (void)encodeWithCoder:(NSCoder *)aCoder
    {
    [aCoder encodeDouble:_lng forKey:kCityLng];
    [aCoder encodeObject:_dataSource forKey:kCityDataSource];
    [aCoder encodeDouble:_lat forKey:kCityLat];
    [aCoder encodeObject:_menus forKey:kCityMenus];
    [aCoder encodeObject:_name forKey:kCityName];
    }

  • (id)copyWithZone:(NSZone )zone
    {
    City
    copy = [[City alloc] init];
    if (copy) {

    copy.lng = self.lng;
    copy.dataSource = [self.dataSource copyWithZone:zone];
    copy.lat = self.lat;
    copy.menus = [self.menus copyWithZone:zone];
    copy.name = [self.name copyWithZone:zone];
    

    }
    return copy;
    }
    @end
    虽然说, 有这样的工具(JSON Accelerator)直接根据JSON来生成对应的类,但是我现在能想到的就有这些问题存在:

如果突然服务端说要加/减几个字段,改比较麻烦。
生成大量的同类型代码,不好维护
类嵌类的时候容易导致死循环
数据转换处理麻烦。如NSNumber直接转NSString,会出现精度问题。
其他问题。
那么我们如何解决这个问题类?

解决方案
在上面的City类中,我们可以发现,这其实是一个模板。如果我们根据这个模板去生成各种类,那么就会存在大量类似的代码。 那么我们如何把他们优雅的聚合在一起,做到高内聚,低耦合类? 答案就是Runtime。 而且轮子别人已经帮我们做好了。 现在在Github上有三个关于这方面的库是比较好的。分别是MJExtension、JSONModel、Mantle。他们的作用都是用于JSON解析。

Mantle 是老牌的,使用人数最多,久经考验。
JSONModel 使用人数居中,带有网络请求部分。
MJExtention 开源不久,但是使用人数也不少,性能最好,使用最简单(不需要你的模型类继承任何特殊基类,毫无污染,毫无侵入性),而且开源者是中国的,交流也比较方便。
在这里JSONModel及Mantle就不做介绍了,就对MJExtention做一个简单的说明,有兴趣的话可以点击链接直接在Github上看官方的文档,是中文的,而且写的很详细。 下面依然拿City类做例子:

简单使用
//只需要引入头文件,就可以使用这个库的各个方法了。

#import “MJExtension.h”

@interface City : NSObject

@property (nonatomic, assign) double lng;
@property (nonatomic, strong) NSString dataSource;
@property (nonatomic, assign) double lat;
@property (nonatomic, strong) NSString
menus;
@property (nonatomic, strong) NSString *name;

@end

#import “City.h”

@implementation City

@end
只需要这样就可以实现上面那个类的全部功能了。

字段转模型
// 将字典转City模型
City city = [City objectWithKeyValues:dict];
将模型转为字典
// 新建模型
City
city = [[City alloc] init];
city.dataSource = @”SHANGHAI”;
city.menus = @”测试”;
city.name = @”上海”;
city.lng = 121.480263;
city.lat = 31.236295;

// 将模型转为字典
NSDictionary cityDict = city.keyValues;
NSLog(@”%@”, cityDict);
/

{
“lng” : 121.480263,
“dataSource” : “SHANGHAI”,
“lat” : 31.236295,
“menus” : “测试”,
“name” : “上海”
}
*/
当然这些都只是简单用法,这个库还支持这些功能:

字典数组(JSON Array) –> 模型数组(Model Array)
模型数组(Model Array) –> 字典数组(JSON Array)
模型中嵌套模型
模型中有个数组属性,数组里面又要装着其他模型
模型中的属性名和字典中的key不相同(或者需要多级映射)
更多用法请参考对应的文档及自己摸索咯。