iOS模型框架- Mantle 解读

Mantle 是由 Github 开发一款模型框架,Mantle 的主要作用在于让开发者更简单的构建应用的 model 层。本文主要介绍 Mantle 中最常使用的 JSON 功能--字典转模型。

Mantle的使用

先以知乎的一个 API 为例,讲解如何使用 Mantle 为这个 API 构建 model 。

API的数据结构
  1. 按照 Mantle 要求构建对应的 model
    ( Mantle 要求所有的 Model 都要继承于 MTLModel 并实现 MTLJSONSerializing 协议)

构建 ZhihuLatestNews model 对应返回的 JSONKeyPath


ZhihuLatestNews.h

ZhihuLatestNews.m

构建 ZhihuStory model 对应 sotries 的JSONkeyPath


ZhihuStory.h

ZhihuStory.m

构建 ZhihuStory model 对应 top_sotries 的JSONkeyPath


ZhihuTopStory.h

ZhihuTopStory.m

2.将网络请求的结果使用 Manlte 转化成 model

Manlte使用

Mantle的接口

Mantle 通过 MTLJSONAdapter 实现 字典和 model 之间的转化

ZhihuLatestNews *lateNews = [MTLJSONAdapter modelOfClass:ZhihuLatestNews.class fromJSONDictionary:dict error:&merror];

Mantle的核心操作步骤

1.获取 model 的属性--> JSONKeyPath 映射字典
2.获取 model 的属性列表
3.根据 model 的方法给网络请求中返回的 JSON 字典中的 value 做值类型转化操作
4.使用 KVC 把值赋给 model 的属性,完成操作

Mantle 的 JSON字典--> model 方法调用层级,Mantle 源码解读过程中主要是参考这个调用过程


| +[MTLJSONAdapter modelOfClass:fromJSONDictionary:error:]// Mantle 调用入口
|   [<MTLJSONAdapter 0x7fe68bd64340> initWithModelClass:]// 创建 MTLJSONAdapter 
|     +[ZhihuLatestNews JSONKeyPathsByPropertyKey]// 获取 属性-> JSONKeyPath 映射字典
|     +[ZhihuLatestNews propertyKeys] // 获取 model 的所有 Mantle 可用属性
|       +[ZhihuLatestNews enumeratePropertiesUsingBlock:]
|         +[ZhihuLatestNews storageBehaviorForPropertyWithKey:] // 判断 model 的属性是否 Mantle 要求

| From: -[MTLJSONAdapter initWithModelClass:]
| +[MTLJSONAdapter valueTransformersForModelClass:] //值转化
|   +[ZhihuLatestNews propertyKeys]
|   +[MTLJSONAdapter transformerForModelPropertiesOfClass:]

| From: +[MTLJSONAdapter modelOfClass:fromJSONDictionary:error:]
| [<MTLJSONAdapter 0x7fe68bd64340> modelFromJSONDictionary:error:] 
|   +[ZhihuLatestNews propertyKeys] // 获取 model 的所有 Mantle 可用属性
|   +[ZhihuLatestNews modelWithDictionary:error:] //生成 model 对象
|     [<ZhihuLatestNews 0x7fe68bf2dea0> initWithDictionary:error:]
|       [<ZhihuLatestNews 0x7fe68bf2dea0> init]
|       [<ZhihuLatestNews 0x7fe68bf2dea0> setDate:]
|   [<ZhihuLatestNews 0x7fe68bf2dea0> validate:]
|     +[ZhihuLatestNews propertyKeys]

Mantle的源码解读

1.初始化 MTLJSONAdapter

MTLJSONAdapter 是 字典和 model 之间的适配器,将 JSON 字典 转成应用的 model 对象


方法定义
+ (id)modelOfClass:(Class)modelClass fromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error {
     //初始化MTLJSONAdapter
    MTLJSONAdapter *adapter = [[self alloc] initWithModelClass:modelClass];

    return [adapter modelFromJSONDictionary:JSONDictionary error:error];
}

2.initWithModelClass:(Class)modelClass 的作用是使用给定的 modelClass 初始化 MTLJSONAdapter

方法定义
- (id)initWithModelClass:(Class)modelClass {
  
    //对modelClass进行判空操作
    NSParameterAssert(modelClass != nil);
    //是否实现MTLJSONSerializing协议的判断,对是否继承了MTLModel进行确认
    NSParameterAssert([modelClass conformsToProtocol:@protocol(MTLJSONSerializing)]);

    self = [super init];
    if (self == nil) return nil;

    //使用变量保存modelClass,以便后续使用
     _modelClass = modelClass;

    //获取属性值和JSONKeyPaths的映射字典,并使用_JSONKeyPathsByPropertyKey保存
    //JSONKeyPathsByPropertyKey 是 MTLJSONSerializing 协议中定义的一个方法,在MTLModel的子类中实现
    _JSONKeyPathsByPropertyKey = [modelClass JSONKeyPathsByPropertyKey];

   //获取 model 的属性,保存在propertyKeys变量中
   //  **该方法后文有详细的实现解读**
    NSSet *propertyKeys = [self.modelClass propertyKeys];

     //判断 _JSONKeyPathsByPropertyKey 是否包含在 propertyKeys 里面,
     //用来确认 _JSONKeyPathsByPropertyKey 里面的 key 都是 model 的属性
    for (NSString *mappedPropertyKey in _JSONKeyPathsByPropertyKey) {
        if (![propertyKeys containsObject:mappedPropertyKey]) {
            NSAssert(NO, @"%@ is not a property of %@.", mappedPropertyKey, modelClass);
            return nil;
        }
        //根据 model 的属性 key 取出 JSONKeyPath
        id value = _JSONKeyPathsByPropertyKey[mappedPropertyKey];
        //TODO [value isKindOfClass:NSArray.class] 这个是判断什么呢?在哪里用到呢?在文章末尾说明1
        if ([value isKindOfClass:NSArray.class]) {
            for (NSString *keyPath in value) {
                if ([keyPath isKindOfClass:NSString.class]) continue;
                NSAssert(NO, @"%@ must either map to a JSON key path or a JSON array of key paths, got: %@.", mappedPropertyKey, value);
                return nil;
            }
        } else if (![value isKindOfClass:NSString.class]) {
            NSAssert(NO, @"%@ must either map to a JSON key path or a JSON array of key paths, got: %@.",mappedPropertyKey, value);
            return nil;
        }
    }
    //获取 model 属性的 valueTransformers 用于做类型转化
    // **该方法后文有详细的实现解读**
    _valueTransformersByPropertyKey = [self.class valueTransformersForModelClass:modelClass];
    //A new map table object which has strong references to the keys and values.
    _JSONAdaptersByModelClass = [NSMapTable strongToStrongObjectsMapTable];

    return self;
}

3.MTLModel的propertyKeys类方法,该方法会返回一个包含 model 属性列表的 NSSet 数据,但是这个 NSSet 数据不包含 被 readonly 修饰的属性,没有 ivars 变量的属性,以及 MTLModel 类自身的属性。除此之外的所有通过 @property 声明的属性都会存在 NSSet 数据中,

方法定义
/// Returns the keys for all @property declarations, except for `readonly`
/// properties without ivars, or properties on MTLModel itself.
+ (NSSet *)propertyKeys {
    // 判断 model 中 是否有属性列表的缓存,若有直接返回
    NSSet *cachedKeys = objc_getAssociatedObject(self, MTLModelCachedPropertyKeysKey);
    if (cachedKeys != nil) return cachedKeys;

    NSMutableSet *keys = [NSMutableSet set];
    //遍历 model 所有的属性,判断哪些属性是符合要求的,加入 keys 变量中
   // **该方法后文有详细的实现解读**
    [self enumeratePropertiesUsingBlock:^(objc_property_t property, BOOL *stop) {
        //获取属性名字
        NSString *key = @(property_getName(property));
        //判断哪些属性是可以做映射的,即不是 MTLPropertyStorageNone 的都可以做映射
        // **该方法后文有详细的实现解读**
        if ([self storageBehaviorForPropertyWithKey:key] != MTLPropertyStorageNone) {
             [keys addObject:key];
        }
    }];

    // It doesn't really matter if we replace another thread's work, since we do
    // it atomically and the result should be the same.
    // 给这个对象设置属性列表的缓存
    objc_setAssociatedObject(self, MTLModelCachedPropertyKeysKey, keys, OBJC_ASSOCIATION_COPY);

    return keys;
}

4.MTLModel 的enumeratePropertiesUsingBlock类方法,该方法用来遍历 model 的属性

+ (void)enumeratePropertiesUsingBlock:(void (^)(objc_property_t property, BOOL *stop))block {
    Class cls = self;
    BOOL stop = NO;
    //按 mode l的继承层级,遍历 model 的属性
    while (!stop && ![cls isEqual:MTLModel.class]) {
        unsigned count = 0;
        //获取属性列表
        objc_property_t *properties = class_copyPropertyList(cls, &count);
        cls = cls.superclass;
        if (properties == NULL) continue;
        @onExit {
            free(properties);
        };
        //block回调
        for (unsigned i = 0; i < count; i++) {
            block(properties[i], &stop);
            if (stop) break;
        }
    }
}

5.MTLModel 的 storageBehaviorForPropertyWithKey 类方法,用于判断 model 的属性是否可以用来做转化

方法定义
+ (MTLPropertyStorage)storageBehaviorForPropertyWithKey:(NSString *)propertyKey {
    //根据属性名获取属性的相关内容
    objc_property_t property = class_getProperty(self.class, propertyKey.UTF8String);
    if (property == NULL) return MTLPropertyStorageNone;
  //将属性的 runtime 表示方法 转成 Mantle 的表示方法
  // **该方法后文有详细的实现解读**
    mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
    @onExit {
        free(attributes);
    };
  //是否有 setter 和 getter 方法
    BOOL hasGetter = [self instancesRespondToSelector:attributes->getter];
    BOOL hasSetter = [self instancesRespondToSelector:attributes->setter]; 
    if (!attributes->dynamic && attributes->ivar == NULL && !hasGetter && !hasSetter) {
        // attributes 不是 dynamic ( @dynamic 就是要来告诉编译器,代码中用 @dynamic 修饰的属性,其 getter 和 setter 方法会在程序运行的时候或者用其他方式动态绑定,以便让编译器通过编译)
        // attributes->ivar 为空
        //没有 getter 方法
        //没有 setter 方法
        return MTLPropertyStorageNone;
    } else if (attributes->readonly && attributes->ivar == NULL) {
        //attributes 是 readonly
        //attributes->ivar 变量为空
        if ([self isEqual:MTLModel.class]) {
            //是否是 MTLModel 的属性
            return MTLPropertyStorageNone;
        } else {
            // Check superclass in case the subclass redeclares a property that
            // falls through
            // 检查一下超类属性,防止超类属性被子类重新声明
            return [self.superclass storageBehaviorForPropertyWithKey:propertyKey];
        }
    } else {
        return MTLPropertyStoragePermanent;
    }
}

6.EXTRuntimeExtensions 的 mtl_copyPropertyAttributes 方法,作用是将属性的runtime表示形式转成更好理解的 Mantle 表示形式。该方法比较枯燥无味且相对来说难以理解,可以略过,不影响 Mantle 解读。
该方法需要结合苹果的官方Runtime开发文档再进行单步调试才能更好的理解。

方法定义
//以属性date为例,说明整个转化过程,具体的信息可以参考苹果官方的 runtime 文档
// @property (nonatomic,strong) NSString *date 

mtl_propertyAttributes *mtl_copyPropertyAttributes (objc_property_t property) {
    //The string starts with a T followed by the @encode type and a comma(逗号), and finishes with a V followed by the name of the backing instance variable.
    //属性 date 的 runtime 表示形式为 "T@\"NSString\",&,N,V_date"
    const char * const attrString = property_getAttributes(property);
    if (!attrString) {
        fprintf(stderr, "ERROR: Could not get attribute string from property %s\n", property_getName(property));
        return NULL;
    }
    //必须以 T 开头
    if (attrString[0] != 'T') {
        fprintf(stderr, "ERROR: Expected attribute string \"%s\" for property %s to start with 'T'\n", attrString, property_getName(property));
        return NULL;
    }
    //去掉 T 变成 "@\"NSString\",&,N,V_date"
    const char *typeString = attrString + 1;//attrString代表字符串的起始地址,地址加1表示字符串截取

    // Obtains the actual size and the aligned size of an encoded type.
    // 去掉 @encode 字符串 变成 ",&,N,V_date"
    const char *next = NSGetSizeAndAlignment(typeString, NULL, NULL);
    if (!next) {
        fprintf(stderr, "ERROR: Could not read past type in attribute string \"%s\" for property %s\n", attrString, property_getName(property));
        return NULL;
    }
   // 以属性 date 为例差11个字符
    size_t typeLength = next - typeString;
    if (!typeLength) {
        fprintf(stderr, "ERROR: Invalid type in attribute string \"%s\" for property %s\n", attrString, property_getName(property));
        return NULL;
    }

    // allocate enough space for the structure and the type string (plus a NUL)
    // 将propertyAttributes变成mtl_propertyAttributes类型
    // TODO 长度的计算?为什么是这样计算?
    mtl_propertyAttributes *attributes = calloc(1, sizeof(mtl_propertyAttributes) + typeLength + 1);
    if (!attributes) {
        fprintf(stderr, "ERROR: Could not allocate mtl_propertyAttributes structure for attribute string \"%s\" for property %s\n", attrString, property_getName(property));
        return NULL;
    }

    // copy the type string
    // 复制属性的类型
    strncpy(attributes->type, typeString, typeLength);
    attributes->type[typeLength] = '\0';//字符串结尾

    // if this is an object type, and immediately followed by a quoted string...
    if (typeString[0] == *(@encode(id)) && typeString[1] == '"') {
        // we should be able to extract a class name
        // "NSString\",&,N,V_date"
        const char *className = typeString + 2;//字符串截取

        //extern char *strchr(const char *s,char c);查找字符串s中首次出现字符c的位置。
        //"\",&,N,V_date"
        next = strchr(className, '"');

        if (!next) {
            fprintf(stderr, "ERROR: Could not read class name in attribute string \"%s\" for property %s\n", attrString, property_getName(property));
            return NULL;
        }

        if (className != next) {
            // 通过内存地址相减 0x0000000104f0347e((const char *) next) - 0x0000000104f03476((const char *) className) = 8
            size_t classNameLength = next - className;
            char trimmedName[classNameLength + 1];//创建用于存放属性类型的数组

            strncpy(trimmedName, className, classNameLength);//复制属性类型到trimmedName
            trimmedName[classNameLength] = '\0';//数组末尾结束符号

            // attempt to look up the class in the runtime
            attributes->objectClass = objc_getClass(trimmedName);//设置属性类型
        }
    }
     // "\",&,N,V_date"
    if (*next != '\0') {
        // skip past any junk before the first flag
        // ",&,N,V_date"
        next = strchr(next, ',');
    }

    while (next && *next == ',') {
        //第一次循环 &
        //第一次循环 N
        //第一次循环 V
        char flag = next[1];
        next += 2;

        switch (flag) {
        case '\0':
            break;

        case 'R':
            //The property is read-only (readonly).
            attributes->readonly = YES;
            break;

        case 'C':
            //The property is a copy of the value last assigned (copy).
            attributes->memoryManagementPolicy = mtl_propertyMemoryManagementPolicyCopy;
            break;

        case '&':
            //The property is a reference to the value last assigned (retain).
            attributes->memoryManagementPolicy = mtl_propertyMemoryManagementPolicyRetain;
            break;

        case 'N':
            //The property is non-atomic (nonatomic).
            attributes->nonatomic = YES;
            break;

        case 'G':
            //The property defines a custom getter selector name. The name follows the G (for example, GcustomGetter,).
        case 'S':
            //The property defines a custom setter selector name. The name follows the S (for example, ScustomSetter:,).
            {
                const char *nextFlag = strchr(next, ',');
                SEL name = NULL;

                if (!nextFlag) {
                    // assume that the rest of the string is the selector
                    const char *selectorString = next;
                    next = "";

                    name = sel_registerName(selectorString);
                } else {
                    size_t selectorLength = nextFlag - next;
                    if (!selectorLength) {
                        fprintf(stderr, "ERROR: Found zero length selector name in attribute string \"%s\" for property %s\n", attrString, property_getName(property));
                        goto errorOut;
                    }

                    char selectorString[selectorLength + 1];

                    strncpy(selectorString, next, selectorLength);
                    selectorString[selectorLength] = '\0';

                    name = sel_registerName(selectorString);
                    next = nextFlag;
                }

                if (flag == 'G')
                    attributes->getter = name;
                else
                    attributes->setter = name;
            }

            break;

        case 'D':
            //The property is dynamic (@dynamic).
            attributes->dynamic = YES;
            attributes->ivar = NULL;
            break;

        case 'V':
            // assume that the rest of the string (if present) is the ivar name
            // V 之后的是变量名称
            if (*next == '\0') {
                // if there's nothing there, let's assume this is dynamic
                attributes->ivar = NULL;
            } else {
                //取得变量名称
                attributes->ivar = next;
                next = "";
            }

            break;

        case 'W':
             //The property is a weak reference (__weak).
            attributes->weak = YES;
            break;

        case 'P':
            //The property is eligible for garbage collection.
            attributes->canBeCollected = YES;
            break;

        case 't':
            fprintf(stderr, "ERROR: Old-style type encoding is unsupported in attribute string \"%s\" for property %s\n", attrString, property_getName(property));

            // skip over this type encoding
            while (*next != ',' && *next != '\0')
                ++next;

            break;

        default:
            fprintf(stderr, "ERROR: Unrecognized attribute string flag '%c' in attribute string \"%s\" for property %s\n", flag, attrString, property_getName(property));
        }
    }

    if (next && *next != '\0') {
        fprintf(stderr, "Warning: Unparsed data \"%s\" in attribute string \"%s\" for property %s\n", next, attrString, property_getName(property));
    }

    if (!attributes->getter) {
        // use the property name as the getter by default
        //使用默认的 getter 方法
        attributes->getter = sel_registerName(property_getName(property));
    }

    if (!attributes->setter) {
        //使用默认的 setter 方法
        const char *propertyName = property_getName(property);
        size_t propertyNameLength = strlen(propertyName);

        // we want to transform the name to setProperty: style
        size_t setterLength = propertyNameLength + 4;

        char setterName[setterLength + 1];
        strncpy(setterName, "set", 3);
        strncpy(setterName + 3, propertyName, propertyNameLength);

        // capitalize property name for the setter
        setterName[3] = (char)toupper(setterName[3]);

        setterName[setterLength - 1] = ':';
        setterName[setterLength] = '\0';

        attributes->setter = sel_registerName(setterName);
    }

    return attributes;

errorOut:
    free(attributes);
    return NULL;
}

7.在获取了 Mantle 需要的属性之后,接下来就需要做一些转化操作了。MTLJSONAdapter 的类方法 valueTransformersForModelClass 主要是将 JSONKeyPath 的值转成 model 的属性声明的类型

//对值做类型转化,值类型的转化方法由 model 提供
+ (NSDictionary *)valueTransformersForModelClass:(Class)modelClass {
    //基本判断
    NSParameterAssert(modelClass != nil);
    NSParameterAssert([modelClass conformsToProtocol:@protocol(MTLJSONSerializing)]);
    NSMutableDictionary *result = [NSMutableDictionary dictionary];
    
    //依次为每个属性拼接值类型转化方法并判断 model 是否实现了该方法
    for (NSString *key in [modelClass propertyKeys]) {
   
        // 1、判断 model 是否实现了 +<key>JSONTransformer 类型方法
        SEL selector = MTLSelectorWithKeyPattern(key, "JSONTransformer");
        //判断是否有实现该方法
        if ([modelClass respondsToSelector:selector]) {
            //取得该方法的实现,调用该方法并获取该方法的返回值
            IMP imp = [modelClass methodForSelector:selector];
            NSValueTransformer * (*function)(id, SEL) = (__typeof__(function))imp;
            NSValueTransformer *transformer = function(modelClass, selector);
            // 为属性保存 NSValueTransformer 对象
            if (transformer != nil) result[key] = transformer;
            continue;
        }
        //2、判断 model 是否实现了 +JSONTransformerForKey: 类型方法
        if ([modelClass respondsToSelector:@selector(JSONTransformerForKey:)]) {
            NSValueTransformer *transformer = [modelClass JSONTransformerForKey:key];
            // 为属性保存 NSValueTransformer 对象
            if (transformer != nil) result[key] = transformer;
            continue;
        }
        objc_property_t property = class_getProperty(modelClass, key.UTF8String);
        if (property == NULL) continue;
       //将属性的runtime 表示形式 转成 Mantle 的表示形式
        mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
        @onExit {
            free(attributes);
        };
      //3、其他值类型转化
        NSValueTransformer *transformer = nil;
        if (*(attributes->type) == *(@encode(id))) {
            Class propertyClass = attributes->objectClass;
            if (propertyClass != nil) {
                // **该方法后文有详细的实现解读**
                //如果是对象类型,那么取出类型转化的 NSValueTransformer 对象
                transformer = [self transformerForModelPropertiesOfClass:propertyClass];
            }
             //用于做属性的值类型转换
            if (transformer == nil) transformer = [NSValueTransformer mtl_validatingTransformerForClass:propertyClass ?: NSObject.class];
        } else {
            //区分BOOL类型
            transformer = [self transformerForModelPropertiesOfObjCType:attributes->type] ?: [NSValueTransformer mtl_validatingTransformerForClass:NSValue.class];
        }

        if (transformer != nil) result[key] = transformer;
    }

    return result;
}

8.MTLJSONAdapter 的 - (id)modelFromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error 方法完成字典转成 model 操作


- (id)modelFromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error {
    //判断是否实现 classForParsingJSONDictionary: 协议
    if ([self.modelClass respondsToSelector:@selector(classForParsingJSONDictionary:)]) {
        //获取Class
        Class class = [self.modelClass classForParsingJSONDictionary:JSONDictionary];
        if (class == nil) {
            if (error != NULL) {
                //错误处理
                NSDictionary *userInfo = @{
                    NSLocalizedDescriptionKey: NSLocalizedString(@"Could not parse JSON", @""),
                    NSLocalizedFailureReasonErrorKey: NSLocalizedString(@"No model class could be found to parse the JSON dictionary.", @"")
                };
                *error = [NSError errorWithDomain:MTLJSONAdapterErrorDomain code:MTLJSONAdapterErrorNoClassFound userInfo:userInfo];
            }

            return nil;
        }

        if (class != self.modelClass) {
            //实现协议判断
            NSAssert([class conformsToProtocol:@protocol(MTLJSONSerializing)], @"Class %@ returned from +classForParsingJSONDictionary: does not conform to <MTLJSONSerializing>", class);

            MTLJSONAdapter *otherAdapter = [self JSONAdapterForModelClass:class error:error];

            return [otherAdapter modelFromJSONDictionary:JSONDictionary error:error];
        }
    }

    NSMutableDictionary *dictionaryValue = [[NSMutableDictionary alloc] initWithCapacity:JSONDictionary.count];
    //取出 model 的属性 key
    for (NSString *propertyKey in [self.modelClass propertyKeys]) {
        //取出JSONKeyPath
        id JSONKeyPaths = self.JSONKeyPathsByPropertyKey[propertyKey];

        if (JSONKeyPaths == nil) continue;

        id value;
        //TODO 这个判断数组的用处?
        if ([JSONKeyPaths isKindOfClass:NSArray.class]) {
            NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
            for (NSString *keyPath in JSONKeyPaths) {
                BOOL success;
                // **该方法后文有详细的实现解读**
                id value = [JSONDictionary mtl_valueForJSONKeyPath:keyPath success:&success error:error];
                if (!success) return nil;
                if (value != nil) dictionary[keyPath] = value;
            }
            value = dictionary;
        } else {
            BOOL success;
           //取出字典中JSONKeyPaths对应的值
           // **该方法后文有详细的实现解读**
            value = [JSONDictionary mtl_valueForJSONKeyPath:JSONKeyPaths success:&success error:error];
            if (!success) return nil;
        }
        if (value == nil) continue;
        @try {
            //取出值转化的 NSValueTransformer 对象,若是对象为空则说明该 propertyKey 不需要做值转化
            NSValueTransformer *transformer = self.valueTransformersByPropertyKey[propertyKey];
            if (transformer != nil) {
                // Map NSNull -> nil for the transformer, and then back for the
                // dictionary we're going to insert into.
                if ([value isEqual:NSNull.null]) value = nil;
                //值转化操作
                if ([transformer respondsToSelector:@selector(transformedValue:success:error:)]) {
                //转化过程有回调 
                    id<MTLTransformerErrorHandling> errorHandlingTransformer = (id)transformer;
                    BOOL success = YES;
                    value = [errorHandlingTransformer transformedValue:value success:&success error:error];
                    if (!success) return nil;
                } else {
                //不需要转化过程回调,直接转化
                // **该方法后文有详细的实现解读**
                    value = [transformer transformedValue:value];
                }
                if (value == nil) value = NSNull.null;
            }
            // 保存被 NSValueTransformer 转化过的 JSONKeyPath 的值
            dictionaryValue[propertyKey] = value;
        } @catch (NSException *ex) {
           //省略错误处理代码
            NSLog(@"*** Caught exception %@ parsing JSON key path \"%@\" from: %@", ex, JSONKeyPaths, JSONDictionary);
        }
    }
    //将字典转换成 model 
    // **该方法后文有详细的实现解读**
    id model = [self.modelClass modelWithDictionary:dictionaryValue error:error];
    //进行错误验证
    return [model validate:error] ? model : nil;
}

9.NSDictionary+MTLJSONKeyPath 的 - (id)mtl_valueForJSONKeyPath:(NSString *)JSONKeyPath success:(BOOL *)success error:(NSError **)error; 方法作用在于根据 JSONKeyPath 从 JSON 字典中取出对应的值

- (id)mtl_valueForJSONKeyPath:(NSString *)JSONKeyPath success:(BOOL *)success error:(NSError **)error {
    //TODO 这个按.分割字符串是什么意思呢?在哪里用到呢?在文章末尾说明2
    NSArray *components = [JSONKeyPath componentsSeparatedByString:@"."];
    id result = self;//字典
    for (NSString *component in components) {
        // Check the result before resolving the key path component to not
        // affect the last value of the path.
        if (result == nil || result == NSNull.null) break;
        if (![result isKindOfClass:NSDictionary.class]) {
                 //错误处理代码
        }
        //以JSONKeyPath为key取出JSON字典中的值
        result = result[component];
    }

    if (success != NULL) *success = YES;

    return result;
}

10.在取得了JSONKeyPath对应的 value 之后,那么下一步操作就是要根据 model 值转化方法返回的 NSValueTransformer 对象做值转化操作

MTLValueTransformer 是一个基于 block 操作的值转化对象,实现 JSON字典-->model 的转化。而MTLReversibleValueTransformer
实现逆向转化操作。


方法定义

@implementation MTLValueTransformer

//转化操作,由 model 的 block 具体实现转化操作,返回值为转化后的值
- (id)transformedValue:(id)value {
    NSError *error = nil;
    BOOL success = YES;

    return self.forwardBlock(value, &success, &error);
}
//带回调的转化操作,由 model 的 block 具体实现转化操作,返回值为转化后的值
- (id)transformedValue:(id)value success:(BOOL *)outerSuccess error:(NSError **)outerError {
    NSError *error = nil;
    BOOL success = YES;
    //转化后的值
    id transformedValue = self.forwardBlock(value, &success, &error);

    if (outerSuccess != NULL) *outerSuccess = success;
    if (outerError != NULL) *outerError = error;

    return transformedValue;
}

@end

//反向转化 model --> JSON 字典
@implementation MTLReversibleValueTransformer
//反向转化操作,由 model 的 block 具体实现转化操作,返回值为转化后的值
- (id)reverseTransformedValue:(id)value {
    NSError *error = nil;
    BOOL success = YES;

    return self.reverseBlock(value, &success, &error);
}
//带回调的转化操作,由 model 的 block 具体实现反向转化操作,返回值为转化后的值
- (id)reverseTransformedValue:(id)value success:(BOOL *)outerSuccess error:(NSError **)outerError {
    NSError *error = nil;
    BOOL success = YES;
    //转化后的值
    id transformedValue = self.reverseBlock(value, &success, &error);

    if (outerSuccess != NULL) *outerSuccess = success;
    if (outerError != NULL) *outerError = error;
    
    return transformedValue;
}

@end

11.在处理的值转化之后,那么接下来就是要将从 JSON 字典中获取的 model 属性值赋值给对应的 model 对象了。在 MTLJSONAdapter 的 - (id)modelFromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error 的实现中的最后一部分代码就是用来生成对应的 model 对象的。


代码段
//MTLModel  代码片段

//使用字典初始化 model 
+ (instancetype)modelWithDictionary:(NSDictionary *)dictionary error:(NSError **)error {
    return [[self alloc] initWithDictionary:dictionary error:error];
}
......
//使用字典初始化 model 
- (instancetype)initWithDictionary:(NSDictionary *)dictionary error:(NSError **)error {
    self = [self init];
    if (self == nil) return nil;
       //取出NSDictionary的key
    for (NSString *key in dictionary) {
        // Mark this as being autoreleased, because validateValue may return
        // a new object to be stored in this variable (and we don't want ARC to
        // double-free or leak the old or new values).
        __autoreleasing id value = [dictionary objectForKey:key];
        if ([value isEqual:NSNull.null]) value = nil; 
        //判断 model 的这个属性是否可以采用 KVC 来给属性赋值
        BOOL success = MTLValidateAndSetValue(self, key, value, YES, error);
        if (!success) return nil;
    }

    return self;
}


//判断 model 的某个属性是否可以采用 KVC 来给属性赋值,然后根据赋值条件给予赋值
static BOOL MTLValidateAndSetValue(id obj, NSString *key, id value, BOOL forceUpdate, NSError **error) {
    __autoreleasing id validatedValue = value;
    @try {
        // 当开发者需要验证能不能用KVC设定某个值时,可以调用validateValue: forKey:这个方法来验证
        // 这个方法的默认实现是去类里面寻找是否有一个这样的方法:-(BOOL)validate<Key>:error:
        // 如果有这个方法,就以这个方法的返回值作为判断标准,没有的话就直接返回YES
        if (![obj validateValue:&validatedValue forKey:key error:error]) return NO;
        // 设置新值
        // obj 返回的 validatedValue 与 传进来的参数 value 进行对比,若不一致采用validatedValue的值
        // forceUpdate 为 YES 那么也是直接给 key 设置值
        if (forceUpdate || value != validatedValue) {
            [obj setValue:validatedValue forKey:key];
        }
        return YES;
    } @catch (NSException *ex) {
        NSLog(@"*** Caught exception setting key \"%@\" : %@", key, ex);
        //错误处理代码
    }
}

// model 的属性验证,只做验证处理,不做赋值操作
- (BOOL)validate:(NSError **)error {
    for (NSString *key in self.class.propertyKeys) {
        id value = [self valueForKey:key];
        //验证 model 是否有某个属性不能使用 KVC 赋值,若是有属性无法通过 KVC 赋值那么返回 NO。 此时 JSON 字典 --> model 过程会得到一个 nil 对象,model 转化失败
        BOOL success = MTLValidateAndSetValue(self, key, value, NO, error);
        if (!success) return NO;
    }

    return YES;
}

12.Mantle完成最后的 model 属性验证之后,返回相关的 model 对象。至此 Mantle 的 JSON 字典 --> model 过程就完成了。

13.TODO 的应用场景说明
在文中有2个 TODO 说明,限于篇幅统一放在这里说明。可以搜索 TODO 关键字找到文章做 TODO 标记文职
// TODO [value isKindOfClass:NSArray.class] 这个是判断什么呢?在哪里用到呢?

//JSON字典
NSDictionary *JSONDict = @{
                               @"code":@200,
                               @"temp" : @"59.07",
                               @"temp_max" : @"63.32"
                          };


//根据上面的 JSON字典 按照正常的处理办法
@interface TestModel : MTLModel<MTLJSONSerializing>
@property (nonatomic,assign) NSInteger code;
@property (nonatomic,strong) NSString *temp;
@property (nonatomic,strong) NSString *temp_max;
@end


//但是有些情况下可能想把 temp 和 temp_max 放到一个字典当中。
@interface TestModel : MTLModel<MTLJSONSerializing>
@property (nonatomic,assign) NSInteger code;
@property (nonatomic,strong) NSDictionary *temp;

@end
// temp 和 temp_max 放到一个字典当中 ,那么 JSONKeyPathsByPropertyKey 方法做如下实现
@implementation TestModel
+(NSDictionary *)JSONKeyPathsByPropertyKey{
    return @{
             @"code":@"code",
             @"temp":[NSArray arrayWithObjects:@"temp", @"temp_max",nil]
             };
}
@end

// TODO 这个按.分割字符串是什么意思呢?在哪里用到呢?在文章末尾说明2

//JSON字典
NSDictionary *JSONDict = @{
                               @"code":@200,
                               @"weather":@{
                                       @"temp" : @"59.07",
                                       @"temp_max" : @"63.32",
                                       @"temp_min" : @"53.01"
                                       }
                               };


//根据上面的 JSON字典 按照正常的处理办法会再给 TestModel 新建一个名为 weather 的对象属性
@interface TestModel : MTLModel<MTLJSONSerializing>
@property (nonatomic,assign) NSInteger code;
@property (nonatomic,strong) Weather *weather;
@end


//但是有些情况下并不想给 TestModel 新建对象属性,而是把 JSON 字典中所有层级的 JSONKeyPath 都放到第一层级来。
@interface TestModel : MTLModel<MTLJSONSerializing>
@property (nonatomic,assign) NSInteger code;
@property (nonatomic,strong) NSString *temp;
@property (nonatomic,strong) NSString *temp_max;
@property (nonatomic,strong) NSString *temp_min;
@end
//JSON 字典中所有层级的 JSONKeyPath 都放到第一层级,那么 JSONKeyPathsByPropertyKey 方法需要做好对应的层级关系实现
@implementation TestModel
+(NSDictionary *)JSONKeyPathsByPropertyKey{
    return @{
             @"code":@"code",
             @"temp":@"weather.temp",
             @"temp_max":@"weather.temp_max",
             @"temp_min":@"weather.temp_min",
             };
}
@end

总结

Mantle 作为一款经典的 JSON 字典 <--> model 转化模型框架,主要是利用 KVC 特性为 model 赋值,其框架设计有不少优点,比如值转化过程的设计等。阅读优秀的开源项目不仅可以扩大技术眼界也可以增加对代码细节的把控能力。希望这篇文章能够抛砖引玉,给大家对 Mantle 的了解增加一些资料。由于个人水平有限,文章若有不对之处恳请指出,我稍作修改,大家共同进步。

参考

https://github.com/Mantle/Mantle
http://southpeak.github.io/2015/01/11/sourcecode-mantle/
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Introduction/Introduction.html
http://www.jianshu.com/p/f49ddbf8a2ea
http://www.jianshu.com/p/9f039124efef
https://github.com/johnno1962/Xtrace
http://nshipster.com/nsvaluetransformer/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,594评论 18 139
  • 结合数据层返回的 JSON 数据,Mantle 在基于 MVC 模式的应用的 Model 层中发挥了重要作用,包括...
    水止云起阅读 1,253评论 0 0
  • 00005梓君20170502
    梓君瞎话阅读 233评论 2 0
  • 马拉西亚最吸引我的, 莫过于丰富的文化融合与神圣的雪邦赛道。 而当我去领略那片土地的汽车文化, 确切地讲是驾驶文化...
    酷听听书阅读 651评论 0 1