现在大部分的项目都需要将服务器返回的JSON数据转换为Model再使用,手动转换不仅费时费力,还写了一堆重复代码,肯定是不科学的,一般都使用相应的工具来自动转换。目前接触的字典转模型工具有三种,Mantle,MJExtension,JSONModel,虽然他们做的事情都是一样的,但是使用方法区别还是蛮大的,以及在一些细节上的处理也是不同的。
Mantle的使用
简单的例子就不来了,可以直接到Github上面查看,这里上一个比较典型全面的例子。
对应模型的特点: 1、有NSNull对象,2、模型里面嵌套模型,3、模型里面有数组,数组里面有模型.
对应的模型如下:
.@H_301_70@h typedef @H_301_70@NS_ENUM(@H_301_70@NSUInteger, @H_301_70@Sex) { @H_301_70@SexMale, @H_301_70@SexFemale }; @@H_301_70@interface BookForMantle : @H_301_70@MTLModel <@H_301_70@MTLJSONSerializing> property (@H_301_70@nonatomic,210)!important">copy,210)!important">nullable) @H_301_70@NSString *@H_301_70@name; end PhoneForMantle : @H_301_70@assign) double @H_301_70@price; UserForMantle : @H_301_70@assign) @H_301_70@NSInteger @H_301_70@age; Sex @H_301_70@sex; strong,210)!important">NSDate *@H_301_70@loginDate; PhoneForMantle *@H_301_70@phone; NSArray<@H_301_70@BookForMantle *> *@H_301_70@books; end
m implementation @H_301_70@PhoneForMantle + (@H_301_70@JSONKeyPathsByPropertyKey { return "name",152)!important">"price"}; } BookForMantle + ("name"}; } UserForMantle // 该map不光是JSON->Model,Model->JSON也会用到 + ("age",152)!important">"sex",152)!important">"loginDate" : "login_date",152)!important">"phone",152)!important">"books"}; } // 模型里面的模型 + (@H_301_70@NSValueTransformer *)@H_301_70@phoneTransformer { return [@H_301_70@MTLJSONAdapter dictionaryTransformerWithModelClass:[@H_301_70@PhoneForMantle @H_301_70@class]]; } // 模型里面的数组 + (@H_301_70@booksTransformer { arrayTransformerWithModelClass:[@H_301_70@BookForMantle // 时间 + (@H_301_70@loginDateJSONTransformer { MTLValueTransformer transformerUsingForwardBlock:^@H_301_70@id(@H_301_70@timeIntervalSince1970,210)!important">BOOL *@H_301_70@success,210)!important">NSError *@H_301_70@__autoreleasing *@H_301_70@error) { @H_301_70@NSTimeInterval @H_301_70@timeInterval = [@H_301_70@timeIntervalSince1970 @H_301_70@doubleValue]; @H_301_70@date = [@H_301_70@NSDate dateWithTimeIntervalSince1970:@H_301_70@timeInterval]; date; } reverseBlock:^@H_301_70@date,210)!important">timeInterval = @H_301_70@date.@H_301_70@timeIntervalSince1970; @(@H_301_70@timeInterval).@H_301_70@stringValue; }]; } end
对应的解析方法:
// 注意: Mantle不会自动转类型,如:String->Int,一旦类型不匹配,直接crash // Json->Model // 该方法会调用key-key map方法。 @H_301_70@self.@H_301_70@userForMantle = [modelOfClass:[@H_301_70@UserForMantle @H_301_70@class] fromJSONDictionary:@H_301_70@JSONDict error:@H_301_70@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对象。 @H_301_70@NSDictionary *@H_301_70@jsonDict = [JSONDictionaryFromModel:@H_301_70@userForMantle nil];
JSOMModel的使用
仍旧使用上面的例子,对应的模型如下:
// BookForJsonModel protocol @H_301_70@BookForJsonModel BookForJsonModel : @H_301_70@JSONModel BookForJSONModel // 前面是服务器字段,后面是模型属性字段 + (@H_301_70@JSONKeyMapper *)@H_301_70@keyMapper { return [[@H_301_70@JSONKeyMapper @H_301_70@alloc] initWithDictionary:"name" }]; } end // PhoneForJSONModel PhoneForJSONModel : @H_301_70@PhoneForJSONModel + ("price" }]; } // UserForJSONModel UserForJSONModel : @H_301_70@PhoneForJSONModel *@H_301_70@phone; // 注意协议 BookForJSONModel> *@H_301_70@UserForJSONModel + ("loginDate",152)!important">"books" }]; } // 允许所有字段为空 + (@H_301_70@BOOL)propertyIsOptional:(@H_301_70@NSString *)@H_301_70@propertyName { YES; } 对应的解析方法4
// JSON->Model @H_301_70@UserForJSONModel *@H_301_70@user = [[@H_301_70@UserForJSONModel initWithDictionary:// Model->JSON @H_301_70@dict = [@H_301_70@user @H_301_70@toDictionary];
JSONModel各方面都挺好的,唯一需要注意的地方是它归档的方式,它不是将对象归档,而是转换成字典再归档。
-(@H_301_70@instancetype)initWithCoder:(@H_301_70@NSCoder *)@H_301_70@decoder { @H_301_70@NSString* @H_301_70@json = [@H_301_70@decoder decodeObjectForKey:"json"]; @H_301_70@JSONModelError *@H_301_70@error = @H_301_70@nil; @H_301_70@self = [@H_301_70@self initWithString:@H_301_70@json error:&@H_301_70@error]; JMLog("%@",[@H_301_70@error @H_301_70@localizedDescription]); } self; } -(void)encodeWithCoder:(@H_301_70@encoder { [@H_301_70@encoder encodeObject:@H_301_70@toJSONString forKey:"json"]; }
MJExtension的使用
这个的使用方式就不介绍了,GitHub上写的非常详细。这里主要说说我在用的项目中时遇到问题及解决方式。情况大致是这样的: 最开始项目中的模型统统继承自BaseModel类,解析方式都是自己挨个手动解析。还自定义了一些譬如时间戳转自定义日期类型的方法。在换到MJExtension时,没法对我们的自定义解析方式进行兼容,全部重写肯定是不现实的,只能做兼容。最后通过阅读MJExtension的源码,找到了一个突破口。在BaseModel里面对MJExtension里面的一个方法使用Method Swizzling进行替换。大致代码如下:
@H_301_70@BaseModel.@H_301_70@h 里面添加一个接口,子类可以覆盖 /// json->模型,转换完成之后调用,以便进行自定义配置。 - (mc_keyValuesDidFinishConvertingToObjectWithData:(@H_301_70@id)@H_301_70@data;
m /* 替换方法: - (instancetype)setKeyValues:(id)keyValues context:(NSManagedObjectContext *)context error:(NSError **)error; */ + (void)@H_301_70@load { static dispatch_once_t @H_301_70@onceToken; @H_301_70@dispatch_once(&@H_301_70@onceToken, ^{ @H_301_70@Class @H_301_70@class = [@H_301_70@self @H_301_70@class]; @H_301_70@SEL @H_301_70@originalSelector = selector(setKeyValues:context:error:); @H_301_70@swizzledSelector = mc_setKeyValues:Method @H_301_70@originalMethod = @H_301_70@class_getInstanceMethod(@H_301_70@class,210)!important">originalSelector); @H_301_70@swizzledMethod = @H_301_70@swizzledSelector); @H_301_70@BOOL @H_301_70@success = @H_301_70@class_addMethod(@H_301_70@originalSelector,210)!important">method_getImplementation(@H_301_70@swizzledMethod),210)!important">method_getTypeEncoding(@H_301_70@swizzledMethod)); success) { @H_301_70@class_replaceMethod(@H_301_70@swizzledSelector,210)!important">originalMethod),210)!important">originalMethod)); } else { @H_301_70@method_exchangeImplementations(@H_301_70@originalMethod,210)!important">swizzledMethod); } }); } - (mc_setKeyValues:(@H_301_70@keyValues context:(@H_301_70@NSManagedObjectContext *)@H_301_70@context error:(@H_301_70@NSError **)@H_301_70@error { @H_301_70@MCDataModel *@H_301_70@model = [mc_setKeyValues:context:if ([respondsToSelector:mc_keyValuesDidFinishConvertingToObjectWithData:)]) { [mc_keyValuesDidFinishConvertingToObjectWithData:@H_301_70@keyValues]; } model; }
这样在使用MJExtension将模型解析完成之后再调用mc_keyValuesDidFinishConvertingToObjectWithData: 将原始数据传递过去进行自定义配置。就可以很好的与老工程兼容了。
Mantle,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 transformer
BOOL 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 name
if (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:[@"%@ type not supported for %@.%@",property.type,[self class],property.name]
userInfo:nil];
可以看到它有做一个转换,对于一般的服务端数字,客户端NSString,或服务器端字符,客户端NSNumber,这样比较简单的转换,JSONModel已经帮我们实现好了,但是如果服务器端返回一个数组,但是客户端是NSString,那么这需要我们自己按照它的格式去写一个转换的方法,如果没写的话,JSONModel会抛出一个异常.然而通常我们需要服务器端传错了,我们客户端应该不崩溃,而是将对应的字段赋值为nil.
对于MJExtension的模型赋值:
由于MJ的考虑的情况比较全面,代码较多,有兴趣的可以自己去看,这里只截取最后一部分:
// value和property类型不匹配
if (propertyClass && ![value isKindOfClass:propertyClass])
{
value = nil;
}
可以看到如果类型不匹配那么对应的属性将被赋值为nil.而这些不需要我们写任何代码,可以的.最为厉害的就是当服务器传字符,客户端为NSUInteger类型时,Mantle,JSONModel都会崩溃,而MJ不会崩溃,且正确转换. 综上防崩溃最强的当属MJExtension,如果服务器端开发人员很菜的话强烈推荐使用MJExtension.