YYKit appeared for a long time, has been wanting to analyze its source code in detail, are all kinds of reasons delayed.

Recently a little bit of leisure, decided to start with the simplest YYModel.

First of all, I also went to search YYModel related articles, analysis of the main API and usage of the majority, there are many people do not understand. In fact, mainly to calm down, because YY uses a lot of not common, not commonly used underlying API, you will find that one by one to check the official documents, they are not difficult.

YYModel is used to cache the converted Model properties and encapsulate ClassInfo, MethodInfo, PropertyInfo, etc. These are also for the convenience of caching and value.

As I mentioned earlier in the Runtime summary, all parses of JSON or automatically converts other data to the Model end up using runtime to dynamically retrieve model properties, sample variables, etc.

YYModel is also implemented using this, from the _update method in YYClassInfo:

    Class cls = self.cls;
    unsigned int methodCount = 0;
    Method *methods = class_copyMethodList(cls, &methodCount);
    if (methods) {
        NSMutableDictionary *methodInfos = [NSMutableDictionary new];
        _methodInfos = methodInfos;
        for (unsigned int i = 0; i < methodCount; i++) {
            YYClassMethodInfo *info = [[YYClassMethodInfo alloc] initWithMethod:methods[i]];
            if (info.name) methodInfos[info.name] = info;
        }
        free(methods);
    }
    unsigned int propertyCount = 0;
    objc_property_t *properties = class_copyPropertyList(cls, &propertyCount);
    if (properties) {
        NSMutableDictionary *propertyInfos = [NSMutableDictionary new];
        _propertyInfos = propertyInfos;
        for (unsigned int i = 0; i < propertyCount; i++) {
            YYClassPropertyInfo *info = [[YYClassPropertyInfo alloc] initWithProperty:properties[i]];
            if (info.name) propertyInfos[info.name] = info;
        }
        free(properties);
    }
    
    unsigned int ivarCount = 0;
    Ivar *ivars = class_copyIvarList(cls, &ivarCount);
    if (ivars) {
        NSMutableDictionary *ivarInfos = [NSMutableDictionary new];
        _ivarInfos = ivarInfos;
        for (unsigned int i = 0; i < ivarCount; i++) {
            YYClassIvarInfo *info = [[YYClassIvarInfo alloc] initWithIvar:ivars[i]];
            if (info.name) ivarInfos[info.name] = info;
        }
        free(ivars);
    }
Copy the code

YYModel call sequence overview

First mentioned above the essence of JSON to Model, and then the following step by step understanding, YYModel is how to achieve the JSON to Model.

The first step

For the first step, the following two methods are mentioned:

/** This method converts json to model (with low probability of use). Second, call the following method to convert dict to model */ + (nullable instancetype)yy_modelWithJSON:(id)json; // This method converts dict to model (high probability) + (nullable instanceType)yy_modelWithDictionary (NSDictionary *)dictionary;Copy the code

Of the above two methods, the second method should be the most used, while the first method feels like it has little chance to be used. Why do you say that?

Since our network interface often contains bool values, status codes, messages, and data (arrays, dictionaries, strings, etc.) for success and failure, we need to first convert the JSON structure returned by the interface to the dictionary and determine the bool value or status code to determine whether to further parse the data. So the second method should be the one with the highest probability of use.

The second step

The second step is to start parsing method two above. To take a look at the source code, I added some Chinese comments:

+ (instancetype)yy_modelWithDictionary:(NSDictionary *)dictionaryif(! dictionary || dictionary == (id)kCFNull)return nil;
    if(! [dictionary isKindOfClass:[NSDictionary class]])returnnil; // CLS = [self Class]; // This step, which is the most important, is to get all the information from the class and encapsulate it in _YYModelMeta. _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:cls];if(modelMeta->_hasCustomClassFromDictionary) { cls = [cls modelCustomClassForDictionary:dictionary] ? : cls; } NSObject *one = [cls new]; // This is also a crucial step, converting the model and assigning valuesif ([one yy_modelSetWithDictionary:dictionary]) return one;
    return nil;
}
Copy the code

Then _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass: CLS]; And what does it do? Before we get to the bottom of that, let’s look at what’s in _YYModelMeta.

@interface _YYModelMeta : NSObject { @package YYClassInfo *_classInfo; /// dictionary, the key is the property name, the value is the object of type _YYModelPropertyMeta NSDictionary *_mapper; NSArray *_allPropertyMetas; NSArray *_allPropertyMetas; /// Array<_YYModelPropertyMeta>, property metawhich is mapped to a key path.
    NSArray *_keyPathPropertyMetas;
    /// Array<_YYModelPropertyMeta>, property meta whichis mapped to multi keys. NSArray *_multiKeysPropertyMetas; _allPropertyMetas.count NSUInteger _keyMappedCount; /// Model class type. YYEncodingNSType _nsType; /** The following bool values are based on whether we implemented the corresponding custom conversion method in the model class. For example if we implemented ` - (BOOL) modelCustomTransformFromDictionary: (dic ` NSDictionary *), So ` _hasCustomTransformFromDictionary ` value is YES. */ BOOL _hasCustomWillTransformFromDictionary; BOOL _hasCustomTransformFromDictionary; BOOL _hasCustomTransformToDictionary; BOOL _hasCustomClassFromDictionary; }Copy the code

Tip: _mapper and _allPropertyMetas store the properties to be transformed. It is affected by blacklists, whitelists, setters, and getters.

Suppose you have a Model with 20 attributes, five of which need no conversion and have been added to the blacklist method. So there are actually only 15 properties in _mapper and _allPropertyMetas.

If we implement the whitelist method and only write two properties, then _mapper and _allPropertyMetas have only these two properties.

If both properties in the whitelist are read-only, then there are no properties in _allPropertyMetas. The whitelist and blacklist methods are protocol methods, which are modelPropertyWhitelist and modelPropertyBlacklist respectively. The whitelist is an array of attribute names to be converted, and the blacklist is an array of attribute names not to be converted.

Ok, now you can look at [_YYModelMeta metaWithClass: CLS], and I’ll add some comments

+ (instancetype)metaWithClass:(Class)cls {
    if(! cls)returnnil; // declare a dictionary to store the Model class and its information. The key is Model name. static CFMutableDictionaryRef cache; static dispatch_once_t onceToken; Static dispatch_semaphore_t lock; // Don't be fooled by this method, it is just a way to create a mutable dictionary, just the underlying CF method. cache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); // Set the number of concurrent resources for the semaphore lock = dispatch_semaphore_create(1); }); dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); _YYModelMeta *meta = CFDictionaryGetValue(cache, (__bridge const void *)(CLS)); dispatch_semaphore_signal(lock); // if not, the cache has not been cached beforeif(! Meta | | meta - > _classInfo. NeedUpdate) {/ / 5, no cache, will need to parse the luo (here is a top priority) meta = [[_YYModelMeta alloc] initWithClass: CLS];if(meta) { dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); CFDictionarySetValue(cache, (__bridge const void *)(CLS), (__bridge const void *)(meta)); dispatch_semaphore_signal(lock); }}return meta;
}
Copy the code

Tips

  1. For semaphores, see 8. Dispatch_semaphore in GCD API Documentation (3)
  2. We can use a common API to write this method, which may be easier to understand:
+ (instancetype)metaWithClass:(Class)cls
{
    if(! cls)returnnil; static NSMutableDictionary *cache; static dispatch_once_t onceToken; static NSLock *lock; dispatch_once(&onceToken, ^{ cache = [[NSMutableDictionary alloc] init]; lock = [[NSLock alloc] init]; }); [lock lock]; _YYModelMeta *meta = [cache valueForKey:cls]; [lock unlock]; NeedUpdate is a bit of a problem because _classInfo is a sample variable and cannot be retrieved using dot syntax.if(! meta || meta._classInfo.needUpdate) { meta = [[_YYModelMeta alloc] initWithClass:cls];if (meta) {
            [lock lock];
            [cache setValue:meta forKey:cls]; [lock unlock]; }}return meta;
}
Copy the code

The third step

[[_YYModelMeta Alloc] initWithClass: CLS]; . Since the initWithClass code in _YYModelMeta is quite long (150 lines), I won’t post it here. Write pseudocode:

- (instancetype)initWithClass:(Class)cls { // 1. YYClassInfo *classInfo = [YYClassInfo classInfoWithClass: CLS];if(! classInfo)returnnil; self = [super init]; NSSet *blackList = [CLS getBlackList]; // 2. NSSet *whiteList = [CLS getWhiteList]; / / 4. Container Class attributes and corresponding Class dictionary NSDictionary * genericMapper = [CLS getContainerGenericMapper]; // 5. Obtain attributes to be parsed, including exclusion of blacklists, verification of whitelists, verification of getters and setters, etc. NSDictionary *allPropertyMetas = [cls getAllPropertyMetas]; // 6. If the name of the property is different from that of the key in json, set the property to the key in JSON, which can also be keyPath. NSDictionary *mapper = [cls handlerCustomMapper:allPropertyMetas]; // 7. Add the remaining values in allPropertyMetas to mapper. Because only some property names may be inconsistent with the key in json, it will be removed from allPropertyMetas after settingsetKeyValuesFrom:allPropertyMetas]; // 8. Assign other attributes _classInfo = classInfo; _keyMappedCount = _allPropertyMetas.count; _nsType = YYClassGetNSType(cls); _hasCustomWillTransformFromDictionary = ([cls instancesRespondToSelector:@selector(modelCustomWillTransformFromDictionary:)]); _hasCustomTransformFromDictionary = ([cls instancesRespondToSelector:@selector(modelCustomTransformFromDictionary:)]); _hasCustomTransformToDictionary = ([cls instancesRespondToSelector:@selector(modelCustomTransformToDictionary:)]); _hasCustomClassFromDictionary = ([cls respondsToSelector:@selector(modelCustomClassForDictionary:)]); }Copy the code

Once you simplify initWithClass with the pseudocode above, it’s easy to understand.

The fourth step

YYClassInfo *classInfo = [YYClassInfo classInfoWithClass: CLS]; YYModel through the Runtime to obtain all the attributes of the Model, which is done in this method. Let’s look at the implementation code for this method:

// This method is So easy! + (instancetype)classInfoWithClass:(Class)cls {if(! cls)returnnil; Static CFMutableDictionaryRef classCache; /** 2. Declare a metaclass cache dictionary. Because YYClassInfo, which has a superClassInfo attribute of type YYClassInfo, is also instantiated using this method, it is possible that CLS is a metaclass after many iterations. */ static CFMutableDictionaryRef metaCache; static dispatch_once_t onceToken; Static dispatch_semaphore_t lock; dispatch_once(&onceToken, ^{ // 4. Initialize the class cache dictionary classCache = CFDictionaryCreateMutable (CFAllocatorGetDefault (), 0, & kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); / / 5. Initialize the metaclass cache dictionary metaCache = CFDictionaryCreateMutable (CFAllocatorGetDefault (), 0, & kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); lock = dispatch_semaphore_create(1); }); dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); YYClassInfo *info = CFDictionaryGetValue(class_isMetaClass(CLS)? metaCache : classCache, (__bridge const void *)(cls)); // 7. If it is available but needs to be updated, use runtime to update itif(info && info->_needUpdate) { [info _update]; } dispatch_semaphore_signal(lock); // 8. If not, initialize a class information object according to CLSif(! info) { info = [[YYClassInfo alloc] initWithClass:cls];if(info) { dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); CFDictionarySetValue(info.ismeta? metaCache : classCache, (__bridge const void *)(cls), (__bridge const void *)(info)); dispatch_semaphore_signal(lock); }}return info;
}
Copy the code

With the code above and the comments I added, it should be easy to understand each step of the method.

This is the essence of all json transformation Models, and the code that was shown at the beginning of this article. If you don’t know much about using Runtime to get property lists, you can go to 1 in the Runtime series (2) -Runtime usage Scenarios. The runtime gets a property or function of a class

- (void)_update {// 1. Set some instance variables to nil. _methodInfos = nil; _propertyInfos = nil; Class cls = self.cls; unsigned int methodCount = 0; Method *methods = class_copyMethodList(CLS, &methodCount);if (methods) {
        NSMutableDictionary *methodInfos = [NSMutableDictionary new];
        _methodInfos = methodInfos;
        for(unsigned int i = 0; i < methodCount; I++) {// 2.1 encapsulates Method. YYClassMethodInfo YYClassMethodInfo *info = [[YYClassMethodInfo alloc] initWithMethod:methods[I]];if(info.name) methodInfos[info.name] = info; } free(methods); } unsigned int propertyCount = 0; Objc_property_t *properties = class_copyPropertyList(CLS, &propertyCount);if (properties) {
        NSMutableDictionary *propertyInfos = [NSMutableDictionary new];
        _propertyInfos = propertyInfos;
        for(unsigned int i = 0; i < propertyCount; I++) {// encapsulate the Property, Encapsulated into YYClassPropertyInfo YYClassPropertyInfo * info = [[YYClassPropertyInfo alloc] initWithProperty: properties [I]].if(info.name) propertyInfos[info.name] = info; } free(properties); } unsigned int ivarCount = 0; Ivar *ivars = class_copyIvarList(CLS, &ivarCount);if (ivars) {
        NSMutableDictionary *ivarInfos = [NSMutableDictionary new];
        _ivarInfos = ivarInfos;
        for(unsigned int i = 0; i < ivarCount; YYClassIvarInfo YYClassIvarInfo *info = [[YYClassIvarInfo alloc] initWithIvar:ivars[I]];if (info.name) ivarInfos[info.name] = info;
        }
        free(ivars);
    }
    
    if(! _ivarInfos) _ivarInfos = @{};if(! _methodInfos) _methodInfos = @{};if(! _propertyInfos) _propertyInfos = @{}; _needUpdate = NO; }Copy the code

It may also be worth mentioning this method:

- (instancetype)initWithClass:(Class) CLS {if(! cls)return nil;
    self = [super init];
    _cls = cls;
    _superCls = class_getSuperclass(cls);
    _isMeta = class_isMetaClass(cls);
    if(! _isMeta) { _metaCls = objc_getMetaClass(class_getName(cls)); } _name = NSStringFromClass(cls); [self _update]; _superClassInfo = [self.class classInfoWithClass:_superCls];return self;
}
Copy the code

Step 5

After reading these four steps, it should be clear how YYModel gets properties, methods, instance variables, and so on. Now we go back to the method in step 2. This method there is a need to focus on understanding the method of [one yy_modelSetWithDictionary: dictionary], all attributes in the model of assignment, are all in this method. To understand this method:

- (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic { // 1. Check dicif(! dic || dic == (id)kCFNull)return NO;
    if(! [dic isKindOfClass:[NSDictionary class]])returnNO; /** get the class information from the cache, because the metaWithClass method was called earlier. If it is a class with a transformation set up, metaWithClass may be re-executed in its entirety. */ _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:object_getClass(self)];if (modelMeta->_keyMappedCount == 0) return NO;
    
    if (modelMeta->_hasCustomWillTransformFromDictionary) {
        dic = [((id<YYModel>)self) modelCustomWillTransformFromDictionary:dic];
        if(! [dic isKindOfClass:[NSDictionary class]])returnNO; ModelSetContext = {0};} // set the ModelSetContext = {0}; context.modelMeta = (__bridge void *)(modelMeta); context.model = (__bridge void *)(self); context.dictionary = (__bridge void *)(dic);if (modelMeta->_keyMappedCount >= CFDictionaryGetCount((CFDictionaryRef)dic)) {
        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{// If the number of transformation attributes is less than the number of key-value pairs in the dictionary,  CFArrayApplyFunction((CFArrayRef)modelMeta->_allPropertyMetas, CFRangeMake(0, modelMeta->_keyMappedCount), ModelSetWithPropertyMetaArrayFunction, &context); } // Finally, if there are some special attributes that need to be converted and assigned, do something about itif (modelMeta->_hasCustomTransformFromDictionary) {
        return [((id<YYModel>)self) modelCustomTransformFromDictionary:dic];
    }
    return YES;
}
Copy the code

What might be confusing about the above code are the CFArrayApplyFunction and CFArrayApplyFunction functions.

It’s explained in the official documentation:

// Calls a function once for each key-value pair in// For each key-value pair in the dictionary, Method will be called a applier void CFDictionaryApplyFunction (CFDictionaryRef theDict, CFDictionaryApplierFunction applier. void *context); //Calls afunction once for each element in range inAn array. // Call applier void CFArrayApplyFunction(CFArrayRef theArray, CFRange range, CFArrayApplierFunction applier, void *context);Copy the code

After reading the comments, take a look at the implementation in YYModel:

static void ModelSetWithDictionaryFunction(const void *_key, const void *_value, void *_context) { ModelSetContext *context = _context; __unsafe_unretained _YYModelMeta *meta = (__bridge _YYModelMeta *)(context->modelMeta); __unsafe_unretained _YYModelPropertyMeta *propertyMeta = [meta->_mapper objectForKey:(__bridge) id)(_key)]; __unsafe_unretained id model = (__bridge id)(context->model); // 3. To prevent multiple different values of the same keywhile (propertyMeta) {
        if(propertyMeta->_setter) {// Assign this property to model. ModelSetValueForProperty(model, (__bridge __unsafe_unretained id)_value, propertyMeta); } propertyMeta = propertyMeta->_next; }; }Copy the code

In the ModelSetValueForProperty method, objc_msgSend is called to assign the value of the property type. For example, string assignment:

if (meta->_nsType == YYEncodingTypeNSString) {
    ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, value);
 } 
Copy the code

ModelSetWithPropertyMetaArrayFunction and dictionary approach similar, only the parameters in the applier is directly attribute object.

static void ModelSetWithPropertyMetaArrayFunction(const void *_propertyMeta, void *_context) { ModelSetContext *context = _context; __unsafe_unretained NSDictionary *dictionary = (__bridge NSDictionary *)(context->dictionary); __unsafe_unretained _YYModelPropertyMeta *propertyMeta = (__bridge _YYModelPropertyMeta) *)(_propertyMeta);if(! propertyMeta->_setter)return; id value = nil; // The object can have keyPath or a direct key because of the value of value. So let's take value in a different wayif (propertyMeta->_mappedToKeyArray) {
        value = YYValueForMultiKeys(dictionary, propertyMeta->_mappedToKeyArray);
    } else if (propertyMeta->_mappedToKeyPath) {
        value = YYValueForKeyPath(dictionary, propertyMeta->_mappedToKeyPath);
    } else {
        value = [dictionary objectForKey:propertyMeta->_mappedToKey];
    }
    
    if__unsafe_unretained id model = (__bridge id)(context->model); ModelSetValueForProperty(Model, Value, propertyMeta); }}Copy the code

Here YYModel analysis is finished.

Have Fun!