MJExtension overview

MJExtension is a very easy to use and powerful third-party Model and JSON mutual conversion commercial third-party library, help developers save time from JSON or Foundation Object conversion into Model, and powerful extension function. This satisfies most of the data modeling needs of developers.

In MJ’s own words, the third-party library MJExtension is

A fast, convenient and nonintrusive conversion between JSON and model. Your model class don’t need to extend another base class. You don’t need to modify any model file.

See the Demo on Github for the MJExtension project source code

Below, Fabric unveils the mystery of MJExtension.

What you need to know:

This article is for iOS mid-level developers, so here’s what you need to know before you start reading:

  • The Objective-C Runtime runtime mechanism is designed for iOS runtime runtime
  • For details about the objc_property_t structure, see the Declared property type encodings official documentation on data acquisition
  • The principle of dynamically adding attributes to class classification.
  • Basic usage of blocks and delegates.
  • Basic usage of SEL pointer.
  • C language structure variable storage structure, addressing method.

However, whether you know these or not, I believe that reading the whole article will help you to understand object-C more deeply and appreciate its unique charm.

For the sake of convenience, I call some basic Foundation data structures, such as NSDictionary, NSArray, NSSet, etc., Foundation Objects.

The basic principle of

The basic principle is very simple and Fabric is covered briefly here

Step 1: Get all the properties in NSObject

    unsigned int propertyCount = 0;
    /// Get the attributes of the current class at runtime
    objc_property_t *propertys = class_copyPropertyList([self class], &propertyCount);
    
    // Put the attributes in the array
    for (int i = 0; i < propertyCount; i ++) {
    /// fetch the first attribute
    objc_property_t property = propertys[i];
    // Get the name of the attribute
    NSString *name = @(property_getName(property));
    
    NSLog(@"name:%@", name);
    }
Copy the code

While we haven’t seen the full runtime source code, we can still guess the internal structure of some methods, such as:

const char * _Nonnull property_getName(objc_property_t _Nonnull property) {
    return property->name;
}
Copy the code

This is a very simple wrapper for getting the value of a pointer to a structure.

Step 2: In Foundation Object (array, dictionary, etc.), use name as the key, find the corresponding value, and then fill the corresponding value into the corresponding Model

- (void)setValue:(id)value forObject:(id)object
{
    if (self.type.KVCDisabled || value == nil) return;
    [object setValue:value forKey:self.name];
}
Copy the code

Ps: It’s important to note that NSObject can assign values to corresponding attributes by -[setValue:forKey:], which is necessary to understand the principle of MJExtension.

The advantage of MJExtension

As you can see from the basics of Fabric, it’s easy to convert JSON to model. However, as a commercial SDK, MJExtension’s strong advantage lies in its good compatibility and strong extensibility. The developer can replace the name of the key value, convert the dictionary in the array to the corresponding model, ignore some attributes of the transformation, define all attributes that need to be converted, and convert old values to new values, such as timestamps and time conversions.

Detailed principle of MJExtension

Internal structure of MJExtension

Fabric drew a schematic that roughly describes the internal structure of MJExtension and the relationships between classes.

  • MJExtensionConst class: This class defines string constants that represent the property type. The type string is stored under the Code property in the MJProperty property MJPropertyType. Different types of attributes are represented by different Encode strings that readers can use themselves@encode(int) @encode(float) @encode(NSString)Print out some encode values for commonly used type attributes to deepen your understanding.
  • The MJPropertyType class records the properties of the MJProperty, such as the type of the object to be converted, whether it is a Foundation Object, etc.

+ (instancetype)cachedTypeWithCode:(NSString *)code the type used to find the cache.

BOOL idType BOOL numberType BOOL boolType A set of attributes that represent the specific type of the value object to be converted.

Class typeClass indicates the type of the value object.

NSString *code, which writes the encode value of the Property.

BOOL fromFoundation indicates whether the object being converted is a basic Foundation object such as NSDictionary, NSArray, and NSSet. Basically, if the object being converted is a subclass of NSObject and it’s not an NS-managed Object class, return NO.

KVCDisabled Specifies whether the object can be listened on

  • The MJPropertyKey class writes a value to an MJProperty.

– (id)valueInObject:(id)object writes a value to an MJProperty object. MJPropertyKeyType type is used to indicate whether the value to be converted in the current MJProperty is a dictionary value or an array value. NSString *name is used to represent the key of the value in the current NSDictionary or the index of the value in NSArray.

  • MJFoundation class: Determine if the current object is a Foundation object(NSDictionary, NSArray, etc.)
  • MJProperty class: MJExtension wraps the basic unit of property values. Each objC_property_t value is wrapped with an MJProperty class. This class is the core of the MJExtension code, and Fabric describes its role in this class below.
  • The NSObject+MJProperty class reserves rewritable methods and blocks for developers to replace keys in dictionaries with properties in Models and specify models for dictionaries in NSArray.
  • NSObject+MJClass: set the whitelist for JSON and Model exchange, set the archived whitelist.
  • NSObject+MJKeyValue: implements JSON and Model interchange.
  • NSString+MJExtension class: some special string handling methods, including case to case conversion, camel name and underline name string conversion, etc.
  • NSObject+MJCoding class: rewritten-[encodeObject:forKey:]and-[decoderObject:forkey:]Two methods that allow objects to be archived directly.

MJExtension core code analysis

The design of MJExtension is very clever and involves many methods, which is difficult to be detailed in limited space, so Fabric decided to take you to explore the core method of MJExtension to implement Model transformation: – (instancetype)mj_setKeyValues:(id)keyValues context:(ns-managedobjectcontext *)context. We follow the code inside the method, from top to bottom, from inside out:

// Convert JSON to Foundation Object (NSDictionary, NSArray, etc.)
keyValues = [keyValues mj_JSONObject];
Copy the code
// Set the blacklist and whitelist
NSArray *allowedPropertyNames = [clazzmj_totalAllowedPropertyNames];
NSArray *ignoredPropertyNames = [clazz mj_totalIgnoredPropertyNames];
Copy the code
// claims all MJProperty attributes and iterates through the output
 [clazz mj_enumerateProperties:^(MJProperty *property, BOOL *stop) {
 // Iterate over all MJProperty objects and set them to the corresponding properties of Model
 }
Copy the code

Now let’s explore how each MJProperty object is generated, which is easier to understand with the internal structure diagram of MJExtension I have shown above. In the NSObject+MJProperty class, the + (NSMutableArray *)properties method is responsible for generating all MJProperty objects.

// First read the stored MJProperty array from the cache
NSMutableArray *cachedProperties = [self dictForKey:&MJCachedPropertiesKey][NSStringFromClass(self)];
Copy the code

Two points to note here:

1. Mastering caching skills can improve project performance and reduce code repetition.

Fabric does not need a dictionary to store an array of MJproperty objects because each Model corresponds to a class, so there is no case for two classes sharing a cachedProperty. So we can just write an array of MJProperty properties.

Continue exploring the + (NSMutableArray *)properties method. If there is no cache, iterate over all non-Foundation Object primitypes, take the objC_property_T array and wrap it as an MJProperty array.

        unsigned int outCount = 0;
        objc_property_t *properties = class_copyPropertyList(c, &outCount);
        for (unsigned int i = 0; i<outCount; i++) {
                / / packaging properties
                MJProperty *property = [MJProperty cachedPropertyWithProperty:properties[i]];
                if ([MJFoundation isClassFromFoundation:property.srcClass]) continue;
                property.srcClass = c;
                [property setOriginKey:[self propertyKey:property.name] forClass:self];
                [property setObjectClassInArray:[self propertyObjectClassInArray:property.name] forClass:self];
                [cachedProperties addObject:property];
        }
Copy the code

The Fabric that

        if ([MJFoundation isClassFromFoundation:property.srcClass]) continue;
        property.srcClass = c;
Copy the code

These two lines of code are a bit tricky because there are only two possible srcclasses :Model or nil, so it’s perfectly possible to use a BOOL instead of a Class *srcClass attribute to determine if srcClass is a Foundation Object.

These lines of code are important to understand here

[property setOriginKey:[self propertyKey:property.name] forClass:self];
[property setObjectClassInArray:[self propertyObjectClassInArray:property.name]
Copy the code

The first method is to wrap all the keys to be replaced into an array and store it in an NSMutableDictionary *propertyKeysDict object.

The second method is to save the Class type of the array corresponding to the Model you want to convert to in the NSMutableDictionary *objectClassInArrayDict dictionary.

Understanding of these two methods also understand + mj_replacedKeyFromPropertyName and + (NSDictionary *) (void)mj_setupObjectClassInArray:(MJObjectClassInArray)objectClassInArray describes the implementation principles of the two functions.

To continue to see how to wrap the objc_property_t property, first read the MJProperty from the cache.

// It is important to note that the property pointer to the memory address is the same every time, so it can be dynamically associated
MJProperty *propertyObj = objc_getAssociatedObject(self, property);
Copy the code

If there is no cache then wrap the property as an MJProperty,

- (void)setProperty:(objc_property_t)property
{
    _property = property;
    
    MJExtensionAssertParamNotNil(property);
    
    // 1. Attribute name
    _name = @(property_getName(property));
    
    // 2. Member type
    NSString *attrs = @(property_getAttributes(property));
    NSUInteger dotLoc = [attrs rangeOfString:@ ","].location;
    NSString *code = nil;
    NSUInteger loc = 1;
    if (dotLoc == NSNotFound) { / / no,
        code = [attrs substringFromIndex:loc];
    } else {
        code = [attrs substringWithRange:NSMakeRange(loc, dotLoc - loc)];
    }
    _type = [MJPropertyType cachedTypeWithCode:code];
}
Copy the code

Here the code is clear: Get the name of the property and the encode property in Attrs to get the type of the property by intercepting the string. Let’s look at how the type property of the MJProperty is set,

#pragmaMark - Public method
- (void)setCode:(NSString *)code
{
    _code = code;
    
    MJExtensionAssertParamNotNil(code);
    
    if ([code isEqualToString:MJPropertyTypeId]) {
        _idType = YES;
    } else if (code.length == 0) {
        _KVCDisabled = YES;
    } else if (code.length > 3 && [code hasPrefix:@ "@ \" "]) {
        // Remove the @" and "and intercept the middle type name
        _code = [code substringWithRange:NSMakeRange(2, code.length - 3)];
        _typeClass = NSClassFromString(_code);
        _fromFoundation = [MJFoundation isClassFromFoundation:_typeClass];
        _numberType = [_typeClass isSubclassOfClass:[NSNumber class]];
        
    } else if ([code isEqualToString:MJPropertyTypeSEL] ||
               [code isEqualToString:MJPropertyTypeIvar] ||
               [code isEqualToString:MJPropertyTypeMethod]) {
        _KVCDisabled = YES;
    }
    
    // Whether it is a number
    NSString *lowerCode = _code.lowercaseString;
    NSArray *numberTypes = @[MJPropertyTypeInt, MJPropertyTypeShort, MJPropertyTypeBOOL1, MJPropertyTypeBOOL2, MJPropertyTypeFloat, MJPropertyTypeDouble, MJPropertyTypeLong, MJPropertyTypeLongLong, MJPropertyTypeChar];
    if ([numberTypes containsObject:lowerCode]) {
        _numberType = YES;
        
        if ([lowerCode isEqualToString:MJPropertyTypeBOOL1]
            || [lowerCode isEqualToString:MJPropertyTypeBOOL2]) {
            _boolType = YES; }}}Copy the code

The above code determines the specific type of property property, and you should be able to understand it by referring to the @encode() function and the MJExtensionConst method.

Here, the packaging of MJProperty is basically said.

Next, fetch the corresponding value from keyValues,

// 1. Fetch the attribute value
    id value;
    NSArray *propertyKeyses = [property propertyKeysForClass:clazz];
    for (NSArray *propertyKeys in propertyKeyses) {
        value = keyValues;
        for (MJPropertyKey *propertyKey in propertyKeys) {
            value = [propertyKey valueInObject:value];
        }
        if (value) break;
    }
Copy the code

Handle value,

// Filter the value
            id newValue = [clazz mj_getNewValueFromObject:self oldValue:value property:property];
            if(newValue ! = value) {// There are new filtered values
                [property setValue:newValue forObject:self];
                return;
            }
            
            // If there is no value, return it directly
            if(! value || value == [NSNull null]) return;
Copy the code

Finally, the unextensible collection is converted to a mutable collection according to the typeClass in the TYPE of the MJProperty for subsequent operations. For a particular value, and if it’s a model property, continue the recursive transformation,

if(! type.isFromFoundation && propertyClass) {// Model attributes
                value = [propertyClass mj_objectWithKeyValues:value context:context];
            }
Copy the code

If an array, it is through the array, if he continue to recursion in an array or array, if not call + [mj_objectWithKeyValues: context] into the corresponding Model elements in the array,

for (NSDictionary *keyValues in keyValuesArray) {
        if ([keyValues isKindOfClass:[NSArray class]]){
            [modelArray addObject:[self mj_objectArrayWithKeyValuesArray:keyValues context:context]];
        } else {
            id model = [self mj_objectWithKeyValues:keyValues context:context];
            if(model) [modelArray addObject:model]; }}Copy the code

For other propertyClass types, Fabric will not be described here, you can do your own research, not complicated.

In the final step, the processed value is substituted into the Model,

/ / 3. Assignment
[property setValue:value forObject:self];
Copy the code

After the completion of a model transformation, we can also rewrite the – (void) mj_keyValuesDidFinishConvertingToObject this function for subsequent operations.

At this point, Fabric basically describes the principle of converting MJExtension’s Foundation Object (or JSON) into a Model in detail. Due to the limited space and my limited ability, MANY things are not clear, interested students and friends can add my wechat :justlikeitRobert for detailed discussion.

Thank you for your patience and Fabric wishes you all a prosperous year of the Dog!