背景介绍
现在的移动App基本上免不了和网络传输打交道,我们经常需要和服务器进行数据的传输,常用的数据格式无外乎XML或者JSON。这也就引出了一个新的话题,如何将获得的数据实际应用到我们的App中?答案很简单,建立Model。我们拿到的网络传输数据,无非就是一种以JSON格式标准划分的字符串罢了,我们需要将字符串解析到相应的字段中来,否则如果每次我们都需要直接和网络字符串打交道,那也太复杂了。在刚学习iOS开发的时候,那个时候看了Stanford老爷爷讲的开发教程,里面在涉及Core Data的章节曾经写过这样的一段代码:
+ (XXXModel *)insertModelWithDictionary:(NSDictionary *)dict inContext:(NSManagedObjectContext *)context
{
if (dict == nil || ![dict isKindOf[NSDictionary class]]) return nil;
XXXModel *model = [XXXModel getByIdentifier:[dict objectForKey:@"id"] inContext:context];
if (!model) {
model = [NSEntityDescription insertNewObjectForEntityForName:@"XXXModel"
inManagedObjectContext:context];
}
model.identifier = [dict objectForKey:@"id"];
model.name = [dict objectForKey:@"name"];
model.age = [dict objectForKey:@"age"];
// 许多许多属性
......
......
......
return model;
}
+ (XXXModel *)getByIdentifier:(NSString *)identifier inContext:(NSManagedObjectContext *)context
{
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"XXXModel"];
[request setPredicate:[NSPredicate predicateWithFormat:@"identifier == %@", identifier]];
XXXModel *model = [[context executeFetchRequest:request error:nil] lastObject];
return model;
}
当时一直以为这个是圣经式的写法,一直坚持着,直到后来我接触到了Mantle。当然,今天这篇文章说的不是Mantle,而是另外一个类似的开源库:JSONModel
JSONModel
既然要解析它,首先先放上他的代码地址以示尊重:点我点我!
从Github上的介绍来看,利用JSONModel,我们可以节省大量的时间,它会利用类似于反射(Introspect)的机制,帮助我们自动将JSON的数据解析成Model的字段。
它的用法也很简单,比如有如下这样的JSON数据:
{"id":"10", "country":"Germany", "dialCode": 49, "isInEurope":true}
你只需要建立如下这样的Model:
#import "JSONModel.h"
@interface CountryModel : JSONModel
@property (assign, nonatomic) int id;
@property (strong, nonatomic) NSString* country;
@property (strong, nonatomic) NSString* dialCode;
@property (assign, nonatomic) BOOL isInEurope;
@end
然后像这样:
NSString* json = (fetch here JSON from Internet) ...
NSError* err = nil;
CountryModel* country = [[CountryModel alloc] initWithString:json error:&err];
嘿嘿,JSON的数据就成功得被解析到country这个变量的对应字段上了!
是不是很神奇?让我们赶快来一探背后的原理吧!!!
源码分析
下载源代码,我们可以看到如下格式的包内容:
-- JSONModel -- JSONModel.h/.m -- JSONModelArray.h/.m -- JSONModelClassProperty.h/.m -- JSONModelError.h/.m -- JSONModelCategories -- NSArray+JSONModel.h/.m -- JSONModelNetworking -- 略 -- JSONModelTransformations -- JSONKeyMapper.h/.m -- JSONValueTransformer.h/.m
我们把JSONModelNetworking中的内容略过,因为基本就是网络传输的东西,不是我们了解的重点。
JSONModel文件
JSONModel是我们要讲解的重点,我们首先从它的初始化方法谈起。
-(instancetype)initWithString:(NSString*)string error:(JSONModelError**)err;
-(instancetype)initWithString:(NSString *)string usingEncoding:(NSStringEncoding)encoding error:(JSONModelError**)err;
-(instancetype)initWithDictionary:(NSDictionary*)dict error:(NSError **)err;
-(instancetype)initWithData:(NSData *)data error:(NSError **)error;
粗略一看,四个初始化方法,太可怕了。但是我们知道在iOS的设计理念里,有一种designated initializer的说法,因此,我们挑一个initWithDictionary看起。
initWithDictionary
-(id)initWithDictionary:(NSDictionary*)dict error:(NSError**)err
{
1. //check for nil input
if (!dict) {
if (err) *err = [JSONModelError errorInputIsNil];
return nil;
}
2. //invalid input, just create empty instance
if (![dict isKindOfClass:[NSDictionary class]]) {
if (err) *err = [JSONModelError errorInvalidDataWithMessage:@"Attempt to initialize JSONModel object using initWithDictionary:error: but the dictionary parameter was not an 'NSDictionary'."];
return nil;
}
3. //create a class instance
self = [self init];
if (!self) {
//super init didn't succeed
if (err) *err = [JSONModelError errorModelIsInvalid];
return nil;
}
// Class infoClass = NSClassFromString([NSString stringWithFormat:@"%@Info", NSStringFromClass(class)]);
4. //check incoming data structure
if (![self __doesDictionary:dict matchModelWithKeyMapper:self.__keyMapper error:err]) {
return nil;
}
5. //import the data from a dictionary
if (![self __importDictionary:dict withKeyMapper:self.__keyMapper validation:YES error:err]) {
return nil;
}
6. //run any custom model validation
if (![self validate:err]) {
return nil;
}
7. //model is valid! yay!
return self;
}
我们分七步来解析这个函数:
- 检查参数dict是不是空,是空直接解析失败
- 检查参数dict是不是NSDictionary或其子类的实例,是空直接解析失败
- 关键步骤:创建JSONModel,创建失败直接滚粗,千万别小看这一步哦,具体我们下文说
- 关键步骤:这是一个通过检查是不是有没被映射到的property的过程,如果有存在疏漏的property,那么会导致解析失败,触发Crash
- 关键步骤:这是真正将解析的值对应的赋予Model的property的阶段
- 进行自定义的检查工作,比如避免直接触发Crash之类的
- 得到了正确的model,返回给程序使用。
下面我们来着重分析最重要的三个步骤
重点1: 第三步 [self init]
[self init]的代码调用了init初始化函数,实现如下,我们主要关注的是其中[self setup]
- (id)init
{
self = [super init];
if (self) {
//do initial class setup
[self __setup__];
}
return self;
}
来看看setup的实现,
- (void)__setup__
{
//if first instance of this model, generate the property list
if (!objc_getAssociatedObject(self.class, &kClassPropertiesKey)) {
[self __inspectProperties];
}
//if there's a custom key mapper, store it in the associated object
id mapper = [[self class] keyMapper];
if ( mapper && !objc_getAssociatedObject(self.class, &kMapperObjectKey) ) {
objc_setAssociatedObject(
self.class,
&kMapperObjectKey,
mapper,
OBJC_ASSOCIATION_RETAIN // This is atomic
);
}
}
WoW! What are 这个函数弄啥类?看起来很吓人?别怕,让我来一个个解释。
objc_getAssociatedObject想必大家都不会陌生,这就是一个Associate Object,我们经常用这种方法在Category给一个类添加property。
所以,第一个if的意思就是,我根据kClassPropertiesKey这个key,去当前类的Associate Object找property list,如果没找到,就说明是第一次执行解析,所以我要自己建立一个。所以我们赶紧进入
__inspectProperties一探究竟!
- (void)__inspectProperties
{
//JMLog(@"Inspect class: %@", [self class]);
NSMutableDictionary* propertyIndex = [NSMutableDictionary dictionary];
//temp variables for the loops
Class class = [self class];
NSScanner* scanner = nil;
NSString* propertyType = nil;
// inspect inherited properties up to the JSONModel class
while (class != [JSONModel class]) {
//JMLog(@"inspecting: %@", NSStringFromClass(class));
unsigned int propertyCount;
objc_property_t *properties = class_copyPropertyList(class, &propertyCount);
//loop over the class properties
for (unsigned int i = 0; i < propertyCount; i++) {
JSONModelClassProperty* p = [[JSONModelClassProperty alloc] init];
//get property name
objc_property_t property = properties[i];
const char *propertyName = property_getName(property);
p.name = @(propertyName);
//JMLog(@"property: %@", p.name);
//get property attributes
const char *attrs = property_getAttributes(property);
NSString* propertyAttributes = @(attrs);
NSArray* attributeItems = [propertyAttributes componentsSeparatedByString:@","];
//ignore read-only properties
if ([attributeItems containsObject:@"R"]) {
continue; //to next property
}
//check for 64b BOOLs
if ([propertyAttributes hasPrefix:@"Tc,"]) {
//mask BOOLs as structs so they can have custom convertors
p.structName = @"BOOL";
}
scanner = [NSScanner scannerWithString: propertyAttributes];
//JMLog(@"attr: %@", [NSString stringWithCString:attrs encoding:NSUTF8StringEncoding]);
[scanner scanUpToString:@"T" intoString: nil];
[scanner scanString:@"T" intoString:nil];
//check if the property is an instance of a class
if ([scanner scanString:@"@\"" intoString: &propertyType]) {
[scanner scanUpToCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString:@"\"<"]
intoString:&propertyType];
//JMLog(@"type: %@", propertyClassName);
p.type = NSClassFromString(propertyType);
p.isMutable = ([propertyType rangeOfString:@"Mutable"].location != NSNotFound);
p.isStandardJSONType = [allowedJSONTypes containsObject:p.type];
//read through the property protocols
while ([scanner scanString:@"<" intoString:NULL]) {
NSString* protocolName = nil;
[scanner scanUpToString:@">" intoString: &protocolName];
if ([protocolName isEqualToString:@"Optional"]) {
p.isOptional = YES;
} else if([protocolName isEqualToString:@"Index"]) {
p.isIndex = YES;
objc_setAssociatedObject(
self.class,
&kIndexPropertyNameKey,
p.name,
OBJC_ASSOCIATION_RETAIN // This is atomic
);
} else if([protocolName isEqualToString:@"ConvertOnDemand"]) {
p.convertsOnDemand = YES;
} else if([protocolName isEqualToString:@"Ignore"]) {
p = nil;
} else {
p.protocol = protocolName;
}
[scanner scanString:@">" intoString:NULL];
}
}
//check if the property is a structure
else if ([scanner scanString:@"{" intoString: &propertyType]) {
[scanner scanCharactersFromSet:[NSCharacterSet alphanumericCharacterSet]
intoString:&propertyType];
p.isStandardJSONType = NO;
p.structName = propertyType;
}
//the property must be a primitive
else {
//the property contains a primitive data type
[scanner scanUpToCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString:@","]
intoString:&propertyType];
//get the full name of the primitive type
propertyType = valueTransformer.primitivesNames[propertyType];
if (![allowedPrimitiveTypes containsObject:propertyType]) {
//type not allowed - programmer mistaked -> exception
@throw [NSException exceptionWithName:@"JSONModelProperty type not allowed"
reason:[NSString stringWithFormat:@"Property type of %@.%@ is not supported by JSONModel.", self.class, p.name]
userInfo:nil];
}
}
NSString *nsPropertyName = @(propertyName);
if([[self class] propertyIsOptional:nsPropertyName]){
p.isOptional = YES;
}
if([[self class] propertyIsIgnored:nsPropertyName]){
p = nil;
}
//few cases where JSONModel will ignore properties automatically
if ([propertyType isEqualToString:@"Block"]) {
p = nil;
}
//add the property object to the temp index
if (p && ![propertyIndex objectForKey:p.name]) {
[propertyIndex setValue:p forKey:p.name];
}
}
free(properties);
//ascend to the super of the class
//(will do that until it reaches the root class - JSONModel)
class = [class superclass];
}
//finally store the property index in the static property index
objc_setAssociatedObject(
self.class,
&kClassPropertiesKey,
[propertyIndex copy],
OBJC_ASSOCIATION_RETAIN // This is atomic
);
}
这么多代码,其实总结起来就几个步骤:
- 获取当前类的的property list,通过class_copyPropertyList runtime的方法
遍历每一个propery,解析他们的属性,这里的属性包括是否只读、类型、是否是weak类型,是否是原子性的等等,如果不了解,可以看如下的表格:
| Code | Meaning |
| :————-: |:————-
| R | The property is read-only (readonly).
| C | The property is a copy of the value last assigned (copy).
| & | The property is a reference to the value last assigned (retain).
| N | The property is non-atomic (nonatomic).
| G| The property defines a custom getter selector name. The name follows the G (for example, GcustomGetter,).
| S| The property defines a custom setter selector name. The name follows the S (for example, ScustomSetter:,).
| D | The property is dynamic (@dynamic).
| W | The property is a weak reference (__weak).
| P | The property is eligible for garbage collection.
| t| Specifies the type using old-style encoding. 根据解析结果,检测是不是合法,如果合法创建对应的JSONModelClassProperty并赋值相应的属性值。
然后重复执行,查看完当前的类就去查询其父类,直到没有为止。
最后将解析出来的property list通过Associate Object给赋值,这和我们刚刚在setup中看到的相呼应。
同样,附上关于property属性的苹果的官方文档链接
这一步基本就解释完了,我们来看看下一步。
重点2: 第四步 __doesDictionary:(NSDictionary)dict matchModelWithKeyMapper:(JSONKeyMapper)keyMapper error:(NSError**)err
老样子,我们先来看看他的实现:
-(BOOL)__doesDictionary:(NSDictionary*)dict matchModelWithKeyMapper:(JSONKeyMapper*)keyMapper error:(NSError**)err
{
//check if all required properties are present
NSArray* incomingKeysArray = [dict allKeys];
NSMutableSet* requiredProperties = [self __requiredPropertyNames].mutableCopy;
NSSet* incomingKeys = [NSSet setWithArray: incomingKeysArray];
//transform the key names, if neccessary
if (keyMapper || globalKeyMapper) {
NSMutableSet* transformedIncomingKeys = [NSMutableSet setWithCapacity: requiredProperties.count];
NSString* transformedName = nil;
//loop over the required properties list
for (JSONModelClassProperty* property in [self __properties__]) {
transformedName = (keyMapper||globalKeyMapper) ? [self __mapString:property.name withKeyMapper:keyMapper importing:YES] : property.name;
//chek if exists and if so, add to incoming keys
id value;
@try {
value = [dict valueForKeyPath:transformedName];
}
@catch (NSException *exception) {
value = dict[transformedName];
}
if (value) {
[transformedIncomingKeys addObject: property.name];
}
}
//overwrite the raw incoming list with the mapped key names
incomingKeys = transformedIncomingKeys;
}
//check for missing input keys
if (![requiredProperties isSubsetOfSet:incomingKeys]) {
//get a list of the missing properties
[requiredProperties minusSet:incomingKeys];
//not all required properties are in - invalid input
JMLog(@"Incoming data was invalid [%@ initWithDictionary:]. Keys missing: %@", self.class, requiredProperties);
if (err) *err = [JSONModelError errorInvalidDataWithMissingKeys:requiredProperties];
return NO;
}
//not needed anymore
incomingKeys= nil;
requiredProperties= nil;
return YES;
}
抛开我们暂时还不熟悉的keyMapper(用过Mantle的人估计有一定了解。),整个函数非常容易理解。我们首先获取我们传入的代表JSON数据的Dictionary,然后和我们解析出来的property list进行对比(基于NSSet),如果得到property list室Dictionary的超集,意味着JSON中的数据不能完全覆盖我们生命的属性,说明我们有属性得不到赋值,因此会判断出错。在默认的实现中,如果出现没匹配的实现,就是导致Crash
重点2: 第五步 __importDictionary:(NSDictionary)dict withKeyMapper:(JSONKeyMapper)keyMapper validation:(BOOL)validation error:(NSError**)err
继续看实现:
-(BOOL)__importDictionary:(NSDictionary*)dict withKeyMapper:(JSONKeyMapper*)keyMapper validation:(BOOL)validation error:(NSError**)err
{
//loop over the incoming keys and set self's properties
for (JSONModelClassProperty* property in [self __properties__]) {
//convert key name ot model keys, if a mapper is provided
NSString* jsonKeyPath = (keyMapper||globalKeyMapper) ? [self __mapString:property.name withKeyMapper:keyMapper importing:YES] : property.name;
//JMLog(@"keyPath: %@", jsonKeyPath);
//general check for data type compliance
id jsonValue;
@try {
jsonValue = [dict valueForKeyPath: jsonKeyPath];
}
@catch (NSException *exception) {
jsonValue = dict[jsonKeyPath];
}
//check for Optional properties
if (isNull(jsonValue)) {
//skip this property, continue with next property
if (property.isOptional || !validation) continue;
if ([property.type isSubclassOfClass:[JSONModel class]]) {
NSMutableDictionary *infoKey = [NSMutableDictionary new];
for (NSString *name in dict.allKeys) {
id value = [dict objectForKey:name];
NSMutableString *ignoredCaseName = [NSMutableString stringWithString:[name lowercaseString]];
NSString *ignoredCaseKey = [property.name lowercaseString];
NSRange range = [ignoredCaseName rangeOfString:ignoredCaseKey];
if (range.location != NSNotFound) {
[ignoredCaseName deleteCharactersInRange:range];
NSString *newPropertyName = [ignoredCaseName copy];
if (!isNull(value)) {
[infoKey setObject:value forKey:newPropertyName];
}
}
}
jsonValue = [infoKey copy];
}
if (err && isNull(jsonValue)) {
//null value for required property
NSString* msg = [NSString stringWithFormat:@"Value of required model key %@ is null", property.name];
JSONModelError* dataErr = [JSONModelError errorInvalidDataWithMessage:msg];
*err = [dataErr errorByPrependingKeyPathComponent:property.name];
return NO;
}
}
Class jsonValueClass = [jsonValue class];
BOOL isValueOfAllowedType = NO;
for (Class allowedType in allowedJSONTypes) {
if ( [jsonValueClass isSubclassOfClass: allowedType] ) {
isValueOfAllowedType = YES;
break;
}
}
if (isValueOfAllowedType==NO) {
//type not allowed
JMLog(@"Type %@ is not allowed in JSON.", NSStringFromClass(jsonValueClass));
if (err) {
NSString* msg = [NSString stringWithFormat:@"Type %@ is not allowed in JSON.", NSStringFromClass(jsonValueClass)];
JSONModelError* dataErr = [JSONModelError errorInvalidDataWithMessage:msg];
*err = [dataErr errorByPrependingKeyPathComponent:property.name];
}
return NO;
}
//check if there's matching property in the model
if (property) {
// check for custom setter, than the model doesn't need to do any guessing
// how to read the property's value from JSON
if ([self __customSetValue:jsonValue forProperty:property]) {
//skip to next JSON key
continue;
};
// 0) handle primitives
if (property.type == nil && property.structName==nil) {
//generic setter
if (jsonValue != [self valueForKey:property.name]) {
[self setValue:jsonValue forKey: property.name];
}
//skip directly to the next key
continue;
}
// 0.5) handle nils
if (isNull(jsonValue)) {
if ([self valueForKey:property.name] != nil) {
[self setValue:nil forKey: property.name];
}
continue;
}
// 1) check if property is itself a JSONModel
if ([self __isJSONModelSubClass:property.type]) {
//initialize the property's model, store it
JSONModelError* initErr = nil;
id value = [[property.type alloc] initWithDictionary: jsonValue error:&initErr];
if (!value) {
//skip this property, continue with next property
if (property.isOptional || !validation) continue;
// Propagate the error, including the property name as the key-path component
if((err != nil) && (initErr != nil))
{
*err = [initErr errorByPrependingKeyPathComponent:property.name];
}
return NO;
}
if (![value isEqual:[self valueForKey:property.name]]) {
[self setValue:value forKey: property.name];
}
//for clarity, does the same without continue
continue;
} else {
// 2) check if there's a protocol to the property
// ) might or not be the case there's a built in transofrm for it
if (property.protocol) {
//JMLog(@"proto: %@", p.protocol);
jsonValue = [self __transform:jsonValue forProperty:property error:err];
if (!jsonValue) {
if ((err != nil) && (*err == nil)) {
NSString* msg = [NSString stringWithFormat:@"Failed to transform value, but no error was set during transformation. (%@)", property];
JSONModelError* dataErr = [JSONModelError errorInvalidDataWithMessage:msg];
*err = [dataErr errorByPrependingKeyPathComponent:property.name];
}
return NO;
}
}
// 3.1) handle matching standard JSON types
if (property.isStandardJSONType && [jsonValue isKindOfClass: property.type]) {
//mutable properties
if (property.isMutable) {
jsonValue = [jsonValue mutableCopy];
}
//set the property value
if (![jsonValue isEqual:[self valueForKey:property.name]]) {
[self setValue:jsonValue forKey: property.name];
}
continue;
}
// 3.3) handle values to transform
if (
(![jsonValue isKindOfClass:property.type] && !isNull(jsonValue))
||
//the property is mutable
property.isMutable
||
//custom struct property
property.structName
) {
// searched around the web how to do this better
// but did not find any solution, maybe that's the best idea? (hardly)
Class sourceClass = [JSONValueTransformer classByResolvingClusterClasses:[jsonValue class]];
//JMLog(@"to type: [%@] from type: [%@] transformer: [%@]", p.type, sourceClass, selectorName);
//build a method selector for the property and json object classes
NSString* selectorName = [NSString stringWithFormat:@"%@From%@:",
(property.structName? property.structName : property.type), //target name
sourceClass]; //source name
SEL selector = NSSelectorFromString(selectorName);
//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 mistaked -> exception
@throw [NSException exceptionWithName:@"Type not allowed"
reason:[NSString stringWithFormat:@"%@ type not supported for %@.%@", property.type, [self class], property.name]
userInfo:nil];
return NO;
}
} else {
// 3.4) handle "all other" cases (if any)
if (![jsonValue isEqual:[self valueForKey:property.name]]) {
[self setValue:jsonValue forKey: property.name];
}
}
}
}
}
return YES;
}
这一个函数看着吓人,其实非常容易理解。根据我们刚刚得到的property list,我们一个个取出来,用property name作为key,来查询在对应的JSON字典中的value。然后分为如下几个情况:
- 检查是不是空值。如果该属性是optional,那么无所谓。如果不能为空,那么抛出错误。
- 检查这个值是不是合法的JSON类型,如果不是,抛出错误。
- 如果property是非自定义JSONModel子类的字段,基于Key-Value赋值,当然,你可以自己override setter。
- 如果是自定义的JSONModel子类是,创建一个对应的新类,解析对应的value。
- 如果再不行,进行一系列的判断和利用JSONValueTransformer进行类型转换进行解析。
为什么要进行JSONValueTransformer的转换呢,是因为在iOS的视线中,由于抽象工厂的存在,构建了大量的簇类,比如NSArray, NSNumber, NSDictionary等等,他们只是对外暴露的一层皮,实质上底层对于真正的类。比如NSArrayI <=> NSArray等等。因此,我们需要通过JSONValueTransformer得到真正的Class Type,同时通过class Type找到最合适的转换方法,在JSONValueTransformer.m的文件中,我们能找到一大堆xxxFromYYY的函数。
好了,到此为止,JSONModel的源码解读就差不多了,下周带来SDWebImageCache的解读。