模型转换工具 Mantle, MJExtension, JSONModel 的使用和异同及防崩溃能力比较

来源:互联网 发布:s90pos机无法连接网络 编辑:程序博客网 时间:2024/05/20 02:28

现在大部分的项目都需要将服务器返回的JSON数据转换为Model再使用,手动转换不仅费时费力,还写了一堆重复代码,肯定是不科学的,一般都使用相应的工具来自动转换。目前接触的字典转模型工具有三种,Mantle, MJExtension, JSONModel, 虽然他们做的事情都是一样的,但是使用方法区别还是蛮大的,以及在一些细节上的处理也是不同的。

Mantle的使用

简单的例子就不来了,可以直接到Github上面查看,这里上一个比较典型全面的例子。

123456789101112131415161718
- (NSDictionary *)JSONDict {    if (_JSONDict == nil) {        _JSONDict = @{@"name" : [NSNull null],                      @"age" : @20,                      @"sex" : @0,                      @"login_date" : @"1445136827",                      @"phone" : @{                              @"name" : @"小明的iPhone",                              @"price" : @5000                      }                      @"books" : @[                              @{@"name" : @"西游记"},                          @{@"name" : @"三国演义"}                      ]                      };    }    return _JSONDict;}

对应模型的特点: 1、有NSNull对象, 2、模型里面嵌套模型, 3、模型里面有数组,数组里面有模型.

对应的模型如下:

1234567891011121314151617181920212223
.htypedef NS_ENUM(NSUInteger, Sex) {    SexMale,    SexFemale};@interface BookForMantle : MTLModel <MTLJSONSerializing>@property (nonatomic, copy, nullable) NSString *name;@end@interface PhoneForMantle : MTLModel <MTLJSONSerializing>@property (nonatomic, copy, nullable) NSString *name;@property (nonatomic, assign) double price;@end@interface UserForMantle : MTLModel <MTLJSONSerializing>@property (nonatomic, copy, nullable) NSString *name;@property (nonatomic, assign) NSInteger age;@property (nonatomic, assign)  Sex sex;@property (nonatomic, strong, nullable) NSDate *loginDate;@property (nonatomic, strong, nullable) PhoneForMantle *phone;@property (nonatomic, copy, nullable) NSArray<BookForMantle *> *books;@end
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
.m@implementation PhoneForMantle+ (NSDictionary *)JSONKeyPathsByPropertyKey {    return @{@"name" : @"name",             @"price" : @"price"};}@end@implementation BookForMantle+ (NSDictionary *)JSONKeyPathsByPropertyKey {    return @{@"name" : @"name"};}@end@implementation UserForMantle// 该map不光是JSON->Model, Model->JSON也会用到+ (NSDictionary *)JSONKeyPathsByPropertyKey {    return @{@"name" : @"name",             @"age" : @"age",             @"sex" : @"sex",             @"loginDate" : @"login_date",             @"phone" : @"phone",             @"books" : @"books"};}// 模型里面的模型+ (NSValueTransformer *)phoneTransformer {    return [MTLJSONAdapter dictionaryTransformerWithModelClass:[PhoneForMantle class]];}// 模型里面的数组+ (NSValueTransformer *)booksTransformer {    return [MTLJSONAdapter arrayTransformerWithModelClass:[BookForMantle class]];}// 时间+ (NSValueTransformer *)loginDateJSONTransformer {    return [MTLValueTransformer transformerUsingForwardBlock:^id(NSString *timeIntervalSince1970, BOOL *success, NSError *__autoreleasing *error) {        NSTimeInterval timeInterval = [timeIntervalSince1970 doubleValue];        NSDate *date = [NSDate dateWithTimeIntervalSince1970:timeInterval];        return date;    } reverseBlock:^id(NSDate *date, BOOL *success, NSError *__autoreleasing *error) {        NSTimeInterval timeInterval = date.timeIntervalSince1970;        return @(timeInterval).stringValue;    }];}@end

对应的解析方法:

12345678910
// 注意: Mantle不会自动转类型,如:String->Int, 一旦类型不匹配,直接crash// Json->Model// 该方法会调用key-key map方法。self.userForMantle = [MTLJSONAdapter modelOfClass:[UserForMantle class] fromJSONDictionary:self.JSONDict error:nil];// 这种方式只是简单的使用KVC进行赋值。不会调用key-key map方法, 要求属性和JSON字典中的key名称相同,否则就crash//    self.userForMantle = [UserForMantle modelWithDictionary:self.JSONDict error:&error];// Model -> JSON// 一旦有属性为nil, Mantle会转换成NSNull对象放到JSON字典中,这里有一个坑,使用NSUserDefault存储这样的JSON字典时,程序crash,原因是不可以包含NSNull对象。NSDictionary *jsonDict = [MTLJSONAdapter JSONDictionaryFromModel:self.userForMantle error:nil];

JSOMModel的使用

仍旧使用上面的例子,对应的模型如下:

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
// BookForJsonModel@protocol BookForJsonModel@end@interface BookForJsonModel : JSONModel@property (nonatomic, copy, nullable) NSString *name;@end@implementation BookForJSONModel// 前面是服务器字段,后面是模型属性字段+ (JSONKeyMapper *)keyMapper {    return [[JSONKeyMapper alloc] initWithDictionary:@{       @"name" : @"name"    }];}@end// PhoneForJSONModel@interface PhoneForJSONModel : JSONModel@property (nonatomic, copy, nullable) NSString *name;@property (nonatomic, assign) double price;@end@implementation PhoneForJSONModel+ (JSONKeyMapper *)keyMapper {    return [[JSONKeyMapper alloc] initWithDictionary:@{       @"name" : @"name",       @"price" : @"price"    }];}@end// UserForJSONModel@interface UserForJSONModel : JSONModel@property (nonatomic, copy, nullable) NSString *name;@property (nonatomic, assign) NSInteger age;@property (nonatomic, assign)  Sex sex;@property (nonatomic, strong, nullable) NSDate *loginDate;@property (nonatomic, strong, nullable) PhoneForJSONModel *phone;// 注意协议@property (nonatomic, copy, nullable) NSArray<BookForJSONModel> *books;@end@implementation UserForJSONModel+ (JSONKeyMapper *)keyMapper {    return [[JSONKeyMapper alloc] initWithDictionary:@{       @"name" : @"name",       @"age" : @"age",       @"sex" : @"sex",       @"login_date" : @"loginDate",       @"phone" : @"phone",       @"books" : @"books"    }];}// 允许所有字段为空+ (BOOL)propertyIsOptional:(NSString *)propertyName {    return YES;}@end

对应的解析方法

1234
// JSON->ModelUserForJSONModel *user = [[UserForJSONModel alloc] initWithDictionary:self.JSONDict error:nil];// Model->JSONNSDictionary *dict = [user toDictionary];

JSONModel各方面都挺好的,唯一需要注意的地方是它归档的方式, 它不是将对象归档,而是转换成字典再归档。

12345678910111213141516
-(instancetype)initWithCoder:(NSCoder *)decoder{    NSString* json = [decoder decodeObjectForKey:@"json"];    JSONModelError *error = nil;    self = [self initWithString:json error:&error];    if (error) {        JMLog(@"%@",[error localizedDescription]);    }    return self;}-(void)encodeWithCoder:(NSCoder *)encoder{    [encoder encodeObject:self.toJSONString forKey:@"json"];}

MJExtension的使用

这个的使用方式就不介绍了,GitHub上写的非常详细。这里主要说说我在用的项目中时遇到问题及解决方式。情况大致是这样的: 最开始项目中的模型统统继承自BaseModel类,解析方式都是自己挨个手动解析。还自定义了一些譬如时间戳转自定义日期类型的方法。在换到MJExtension时,没法对我们的自定义解析方式进行兼容,全部重写肯定是不现实的,只能做兼容。最后通过阅读MJExtension的源码,找到了一个突破口。在BaseModel里面对MJExtension里面的一个方法使用Method Swizzling进行替换。大致代码如下:

123
BaseModel.h 里面添加一个接口,子类可以覆盖/// json->模型,转换完成之后调用, 以便进行自定义配置。- (void)mc_keyValuesDidFinishConvertingToObjectWithData:(id)data;
12345678910111213141516171819202122232425262728
BaseModel.m/* 替换方法: - (instancetype)setKeyValues:(id)keyValues context:(NSManagedObjectContext *)context error:(NSError **)error; */+ (void)load {    static dispatch_once_t onceToken;    dispatch_once(&onceToken, ^{        Class class = [self class];        SEL originalSelector = @selector(setKeyValues:context:error:);        SEL swizzledSelector = @selector(mc_setKeyValues:context:error:);        Method originalMethod = class_getInstanceMethod(class, originalSelector);        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);        BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));        if (success) {            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));        } else {            method_exchangeImplementations(originalMethod, swizzledMethod);        }    });}- (instancetype)mc_setKeyValues:(id)keyValues context:(NSManagedObjectContext *)context error:(NSError **)error {    MCDataModel *model = [self mc_setKeyValues:keyValues context:context error:error];    if ([self respondsToSelector:@selector(mc_keyValuesDidFinishConvertingToObjectWithData:)]) {        [self mc_keyValuesDidFinishConvertingToObjectWithData:keyValues];    }    return model;}

这样在使用MJExtension将模型解析完成之后再调用mc_keyValuesDidFinishConvertingToObjectWithData: 将原始数据传递过去进行自定义配置。就可以很好的与老工程兼容了。


Mantle,JSONModel,MJExtension防崩溃比较:
对于这样一条数据:

{"firstName": "张", "lastName": "三", "age": 23,"height": 172.3,  "weight": 51.2, "sex": true}

客户端属性声明为:
@property (nonatomic, copy) NSString *age;

Mantle的模型赋值:

if (![obj validateValue:&validatedValue forKey:key error:error]) return NO;if (forceUpdate || value != validatedValue) {[obj setValue:validatedValue forKey:key];}return YES;

它是使用了KVC的- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;方法来验证要赋的值的类型是否和key的类型是否匹配.该方法默认会调用a validator method whose name matches the pattern -validate<Key>:error:.因此我们还需要在模型里面写一个这样的方法:

-(BOOL)validateAge:(id *)ioValue error:(NSError * __autoreleasing *)outError{    if ([*ioValue isKindOfClass:[NSNumber class]])    {        return YES;    }    return NO;}

如果没写默认返回YES.Mantle貌似并没有帮我们做这么一步,所以如果你自己没写的话,那么上述验证的方法会返回YES.好的程序不应该总是期望服务器端永远都返回正确的东西,然而我们又无法知道服务器哪些字段会返回和我们不一致的类型.难道每一个属性都要写一个-validate<Key>:error:来判断?个人认为在这一点上Mantle做的很鸡肋.如果你没写-validate<Key>:error:而碰巧服务器端返回一个数字类型,那么你的程序很有可能会崩溃.

对于JSONModel的模型赋值:

//check for custom transformerBOOL foundCustomTransformer = NO;if ([valueTransformer respondsToSelector:selector]) {    foundCustomTransformer = YES;} else {    //try for hidden custom transformer    selectorName = [NSString stringWithFormat:@"__%@",selectorName];    selector = NSSelectorFromString(selectorName);    if ([valueTransformer respondsToSelector:selector]) {        foundCustomTransformer = YES;    }}//check if there's a transformer with that nameif (foundCustomTransformer) {    //it's OK, believe me...#pragma clang diagnostic push#pragma clang diagnostic ignored "-Warc-performSelector-leaks"    //transform the value    jsonValue = [valueTransformer performSelector:selector withObject:jsonValue];#pragma clang diagnostic pop    if (![jsonValue isEqual:[self valueForKey:property.name]]) {        [self setValue:jsonValue forKey: property.name];    }} else {    // it's not a JSON data type, and there's no transformer for it    // if property type is not supported - that's a programmer mistake -> exception    @throw [NSException exceptionWithName:@"Type not allowed"                                   reason:[NSString stringWithFormat:@"%@ type not supported for %@.%@", property.type, [self class], property.name]                                 userInfo:nil];    return NO;}

可以看到它有做一个转换,对于一般的服务端数字,客户端NSString,或服务器端字符,客户端NSNumber,这样比较简单的转换,JSONModel已经帮我们实现好了,但是如果服务器端返回一个数组,但是客户端是NSString,那么这需要我们自己按照它的格式去写一个转换的方法,如果没写的话,JSONModel会抛出一个异常.然而通常我们需要服务器端传错了,我们客户端应该不崩溃,而是将对应的字段赋值为nil.

对于MJExtension的模型赋值:
由于MJ的考虑的情况比较全面,代码较多,有兴趣的可以自己去看,这里只截取最后一部分:

// value和property类型不匹配if (propertyClass && ![value isKindOfClass:propertyClass]){value = nil;}

可以看到如果类型不匹配那么对应的属性将被赋值为nil.而这些不需要我们写任何代码,可以的.最为厉害的就是当服务器传字符,客户端为NSUInteger类型时,Mantle,JSONModel都会崩溃,而MJ不会崩溃,且正确转换.
综上防崩溃最强的当属MJExtension,如果服务器端开发人员很菜的话强烈推荐使用MJExtension.