As an iOS/OSX model conversion framework, YYModel provides a high-performance solution for the conversion between JSON and data models.

In my daily development, I mainly use the following methods:

/ / JSON | dictionary model + (nullable instancetype) yy_modelWithJSON (id) JSON; + (nullable instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary; / / by JSON | dictionary - (BOOL) for model assignment yy_modelSetWithJSON (id) JSON; - (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic; // Model to JSON - (NSString *)yy_modelToJSONString; + (nullable NSArray *)yy_modelArrayWithClass:(Class) CLS :(id) JSON;Copy the code

Because multiple functions end up calling the same method, only the code parsing of the main method is listed here.

function

JSON transfer model

+ yy_modelWithDictionary:

Since the + yy_modelWithJSON: method is called, the JSON is first serialized into an available dictionary internally, and then the + yy_modelWithDictionary: method is called. So let’s go straight to + yy_modelWithDictionary: for analysis.

Code:

/** Create and return a new instance through a set of key-value pairs (NSDictionary) this method is thread-safe. @parameter: dictionary A set of key-value pairs that map instance attributes. Invalid key-value pairs are ignored. @ return: a new instance created by a key-value pair (dictionary) that returns nil in case of an error. @ Description: Keys and values in dictionaries are mapped to model property names and property values, respectively. If the value type does not match the attribute, this method attempts to convert according to the following rules: 'NSString' or 'NSNumber' -> c number, such as BOOL, int, long, float, NSUInteger... `NSString` -> NSDate, parsed with format "yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd HH:mm:ss" or "yyyy-MM-dd". `NSString` -> NSURL. `NSValue` -> struct or union, such as CGRect, CGSize, ... `NSString` -> SEL, Class. */ + (instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary { if (! dictionary || dictionary == (id)kCFNull) return nil; if (! [dictionary isKindOfClass:[NSDictionary class]]) return nil; Class CLS = [self Class]; _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass: CLS]; / / determine whether the user custom class type (subclass) if (modelMeta - > _hasCustomClassFromDictionary) {CLS = [CLS modelCustomClassForDictionary:dictionary] ? : cls; } // create instance instance NSObject *one = [CLS new]; / / if for attribute assignment ([one yy_modelSetWithDictionary: dictionary]) return one; return nil; }Copy the code

In the + yy_modelWithDictionary: method, three main things are done: 1. Determine the type; 2. Create an instance. 3. Assign to the instance.

1. Determine the type

Use [self class] in the class method to easily obtain the class object of the current class. Here the author creates the class element _YYModelMeta *model through the class object. The class element contains rich information about the class.

_YYModelMetaClass element definition:

@interface _YYModelMeta: NSObject {@package YYClassInfo *_classInfo; /// Key:mapped key and key path, Value:_YYModelPropertyMeta. Data structure: {" PIC ": [_YYModelPropertyMeta new]} NSDictionary *_mapper; // Array<_YYModelPropertyMeta>, Array of all valid property elements NSArray *_allPropertyMetas; // Array<_YYModelPropertyMeta>, NSArray *_keyPathPropertyMetas; NSArray *_multiKeysPropertyMetas; // Array<_YYModelPropertyMeta>, NSArray *_multiKeysPropertyMetas; /// The number of valid key/value pairs, including _getter, _setter, and member variables. NSUInteger _keyMappedCount; /// Data type YYEncodingNSType _nsType; BOOL _hasCustomWillTransformFromDictionary; BOOL _hasCustomTransformFromDictionary; BOOL _hasCustomTransformToDictionary; BOOL _hasCustomClassFromDictionary; } @endCopy the code

Before determine the type, need to determine whether the user custom according to the different situation of returning to class (subclass) types, namely whether realized + modelCustomClassForDictionary (NSDictionary *) a dictionary; Method returns a custom type.

Official example:

@class YYCircle, YYRectangle, YYLine; @implementation YYShape + (Class)modelCustomClassForDictionary:(NSDictionary*)dictionary { if (dictionary[@"radius"] ! = nil) { return [YYCircle class]; } else if (dictionary[@"width"] ! = nil) { return [YYRectangle class]; } else if (dictionary[@"y2"] ! = nil) { return [YYLine class]; } else { return [self class]; } } @endCopy the code

2. Create an instance

Once the data type is determined, instances are quickly created from class objects.

NSObject *one = [cls new];
Copy the code

3. Assign to the instance

Call the -yy_modelsetwithDictionary: method to assign a value to the instance.

Code:

- (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic { if (! dic || dic == (id)kCFNull) return NO; if (! [dic isKindOfClass:[NSDictionary class]]) return NO; _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:object_getClass(self)]; If (modelMeta->_keyMappedCount == 0) return NO; / / to determine whether a user custom conversion mapping the if (modelMeta - > _hasCustomWillTransformFromDictionary) = {dic [((id < YYModel >) self) modelCustomWillTransformFromDictionary:dic]; if (! [dic isKindOfClass:[NSDictionary class]]) return NO; } // create the model setting context ModelSetContext Context = {0}; context.modelMeta = (__bridge void *)(modelMeta); context.model = (__bridge void *)(self); context.dictionary = (__bridge void *)(dic); If (modelMeta->_keyMappedCount >= CFDictionaryGetCount((CFDictionaryRef)dic)) {if (modelMeta->_keyMappedCount >= CFDictionaryGetCount((CFDictionaryRef)dic)) / * * @ function CFDictionaryApplyFunction each key values in the dictionary to call a function at a time. @param theDict A dictionary to look up. The callback that @param applier calls once for each value in the dictionary. @param context A user-defined value of the size of a pointer passed to the applier function as a third argument, but not used by the function. */ CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context); if (modelMeta->_keyPathPropertyMetas) { CFArrayApplyFunction((CFArrayRef)modelMeta->_keyPathPropertyMetas, CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_keyPathPropertyMetas)), ModelSetWithPropertyMetaArrayFunction, &context); } if (modelMeta->_multiKeysPropertyMetas) { CFArrayApplyFunction((CFArrayRef)modelMeta->_multiKeysPropertyMetas, CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_multiKeysPropertyMetas)), ModelSetWithPropertyMetaArrayFunction, &context); } else {/** @function CFArrayApplyFunction calls the function once for each element in the array. @param theArray theArray to operate on. @param range The range of values in the array to apply the function to. The callback function that @param applier calls once for each value in a given range in the array. If this parameter is not a pointer to a function that points to the correct stereotype, the behavior is undefined. The behavior is undefined if a value exists or cannot be applied correctly within the expected range of an application function. @param context A user-defined value for the size of a pointer that is passed to the applier function as a second argument, but is not used by the function. If the context is not what the applier function expects, the behavior is undefined. */ CFArrayApplyFunction((CFArrayRef)modelMeta->_allPropertyMetas, CFRangeMake(0, modelMeta->_keyMappedCount), ModelSetWithPropertyMetaArrayFunction, &context); } if (modelMeta->_hasCustomTransformFromDictionary) { return [((id<YYModel>)self) modelCustomTransformFromDictionary:dic]; } return YES; }Copy the code

Here will determine whether the user do the additional processing of data dictionary, namely whether implemented – modelCustomWillTransformFromDictionary: method. If so, return and use the custom dictionary.

So everything is ready, create the model setting context ModelSetContext context, ready to assign.

typedef struct { void *modelMeta; //< _YYModelMeta class element void *model; ///< id (self) instance itself void *dictionary; //< NSDictionary (json) data dictionary (json)} ModelSetContext;Copy the code

Compare the number of valid key values of class elements to the number of key values passed into the dictionary, and perform traversal assignments of attributes at a low cost (reducing unnecessary loops). Here, respectively CFDictionaryApplyFunction () and CFArrayApplyFunction () ModelSetWithDictionaryFunction ModelSetWithPropertyMetaArrayFunction and () () to traverse the call. Both are ultimately assigned via the ModelSetValueForProperty() function.

Static void ModelSetValueForProperty(__unsafe_unretained ID model,// The instance object __unsafe_unretained ID value,// value __unsafe_unretained _YYModelPropertyMeta *meta // attributes)Copy the code

The ModelSetValueForProperty() function makes a detailed type judgment on the data of the attribute, which is mainly divided into three categories (basic data type of C, NS data type of Foundation, and custom data type). Except for C’s basic data types, the latter are assigned by calling the meta->_setter method of the property by sending an objc_msgSend message.

Because the implementation code is longer, it is not shown here, interested can view the source code: “YYModel/NSObject+YYModel.m” line 784~1098.

At this point, the JSON to model work is complete.

Model turn JSON

+ yy_modelToJSONString:

- (id)yy_modelToJSONObject { /* Apple said: The top level object is an NSArray or NSDictionary. All objects are instances of NSString, NSNumber, NSArray, NSDictionary, or NSNull. All dictionary keys are instances of NSString. Numbers are not NaN or infinity. */ id jsonObject = ModelToJSONObjectRecursive(self); if ([jsonObject isKindOfClass:[NSArray class]]) return jsonObject; if ([jsonObject isKindOfClass:[NSDictionary class]]) return jsonObject; return nil; } - (NSData *)yy_modelToJSONData { id jsonObject = [self yy_modelToJSONObject]; if (! jsonObject) return nil; return [NSJSONSerialization dataWithJSONObject:jsonObject options:0 error:NULL]; } - (NSString *)yy_modelToJSONString { NSData *jsonData = [self yy_modelToJSONData]; if (jsonData.length == 0) return nil; return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; }Copy the code

It is not hard to see from the method, model turn JSON ModelToJSONObjectRecursive mainly depends on the recursive function, This function will return a valid JSON object (NSArray/NSDictionary/nsstrings/NSNumber/NSNull).

ModelToJSONObjectRecursiveInternal implementation code disassembly:

if (! model || model == (id)kCFNull) return model; if ([model isKindOfClass:[NSString class]]) return model; if ([model isKindOfClass:[NSNumber class]]) return model; if ([model isKindOfClass:[NSURL class]]) return ((NSURL *)model).absoluteString; if ([model isKindOfClass:[NSAttributedString class]]) return ((NSAttributedString *)model).string; if ([model isKindOfClass:[NSDate class]]) return [YYISODateFormatter() stringFromDate:(id)model]; if ([model isKindOfClass:[NSData class]]) return nil;Copy the code

When the model value matches or approaches the target type, it can be simply converted or returned.

// dictionary if ([model isKindOfClass:[NSDictionary class]]) {if ([NSJSONSerialization isValidJSONObject:model]) return model;  NSMutableDictionary *newDic = [NSMutableDictionary new]; [((NSDictionary *)model) enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { NSString *stringKey = [key isKindOfClass:[NSString class]] ? key : key.description; if (!stringKey) return; id jsonObj = ModelToJSONObjectRecursive(obj); if (! jsonObj) jsonObj = (id)kCFNull; newDic[stringKey] = jsonObj; }]; return newDic; } if ([model isKindOfClass:[NSSet class]]) {NSArray *array = ((NSSet *)model).allobjects; if ([NSJSONSerialization isValidJSONObject:array]) return array; NSMutableArray *newArray = [NSMutableArray new]; for (id obj in array) { if ([obj isKindOfClass:[NSString class]] || [obj isKindOfClass:[NSNumber class]]) { [newArray addObject:obj]; } else { id jsonObj = ModelToJSONObjectRecursive(obj); if (jsonObj && jsonObj ! = (id)kCFNull) [newArray addObject:jsonObj]; } } return newArray; } if ([model isKindOfClass:[NSArray class]]) {if ([NSJSONSerialization isValidJSONObject:model]) return model; NSMutableArray *newArray = [NSMutableArray new]; for (id obj in (NSArray *)model) { if ([obj isKindOfClass:[NSString class]] || [obj isKindOfClass:[NSNumber class]]) { [newArray addObject:obj]; } else { id jsonObj = ModelToJSONObjectRecursive(obj); if (jsonObj && jsonObj ! = (id)kCFNull) [newArray addObject:jsonObj]; } } return newArray; }Copy the code

When a model value is a dictionary, collection array type, its internal elements need to be iterated and recursed until they are converted into valid JSON objects one by one.

_YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:[model class]]; if (! modelMeta || modelMeta->_keyMappedCount == 0) return nil; NSMutableDictionary *result = [[NSMutableDictionary alloc] initWithCapacity:64]; __unsafe_unretained NSMutableDictionary *dic = result; // avoid retain and release in block [modelMeta->_mapper enumerateKeysAndObjectsUsingBlock:^(NSString *propertyMappedKey, _YYModelPropertyMeta *propertyMeta, BOOL *stop) { if (! propertyMeta->_getter) return; id value = nil;  if (propertyMeta->_isCNumber) { value = ModelCreateNumberFromProperty(model, propertyMeta);  } else if (propertyMeta->_nsType) { id v = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);  value = ModelToJSONObjectRecursive(v);  } else { switch (propertyMeta->_type & YYEncodingTypeMask) { case YYEncodingTypeObject: { id v = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);  value = ModelToJSONObjectRecursive(v); if (value == (id)kCFNull) value = nil; } break; case YYEncodingTypeClass: { Class v = ((Class (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);  value = v ? NSStringFromClass(v) : nil; } break; case YYEncodingTypeSEL: { SEL v = ((SEL (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);  value = v ? NSStringFromSelector(v) : nil; } break; default: break; } } if (! value) return; if (propertyMeta->_mappedToKeyPath) { NSMutableDictionary *superDic = dic;  NSMutableDictionary *subDic = nil; for (NSUInteger i = 0, max = propertyMeta->_mappedToKeyPath.count; i < max;  i++) { NSString *key = propertyMeta->_mappedToKeyPath[i]; if (i + 1 == max) { // end if (! superDic[key]) superDic[key] = value; break; } subDic = superDic[key];  if (subDic) { if ([subDic isKindOfClass:[NSDictionary class]]) { subDic = subDic.mutableCopy; superDic[key] = subDic;  } else { break; } } else { subDic = [NSMutableDictionary new]; superDic[key] = subDic; } superDic = subDic;  subDic = nil; } } else { if (!dic[propertyMeta->_mappedToKey]) { dic[propertyMeta->_mappedToKey] = value; } } }];Copy the code

When the model value is of a user-defined type, the mapping table _mapper ({attribute name: attribute element}) needs to be traversed and recursed, and the meta->_getter method of the attribute is called to value the model value by sending objc_msgSend message, until it is converted into valid JSON objects one by one.

If (modelMeta - > _hasCustomTransformToDictionary) {/ / calibration data BOOL suc = [((id < YYModel >) model) modelCustomTransformToDictionary:dic]; if (! suc) return nil; } return result;Copy the code

Finally, determine whether the user has additional conversion processing and verify the validity of the data.

Note: result and DIC refer to the same instance, so if DIC is modified in an external function, it is equivalent to changing result.

conclusion

  • The use of YYModel is non-invasive, and the function is realized by Category, which is relatively flexible.
  • In terms of fault tolerance, YYModel has made a detailed classification and judgment on data types. Even if conversion fails, it will automatically leave nil.
  • Performance, useCoreFoundation, inline functions, Runtime, caching mechanism and other ways to reduce unnecessary overhead.