iOS开源库源码解析之Mantle

来自Leo的原创博客,转载请著名出处

我的StackOverflow

这个源码解析系列的文章


前言

iOS开发中,不管是哪种设计模式,Model层都是不可或缺的。而Model层的第三方库常用的库有以下几个

JSON data到对象的转换原理都差不多,一般的顺序如下

  • 根据Runtime,动态的获取属性的类型和属性的名字,(如果需要,做一次Json的key的Mapping
  • 创建对应的对象实例
  • 根据KVC(NSKeyValueCoding协议)来为属性设置值

Mantle就是这样的一个库,个人比较喜欢Mantle,而且在GithubStar也是提到的几个库中最多的。Mantle除了提供JSON和对象的相互转化,继承自MTLModel的对象还自动实现了

  • NSCopying
  • NSCoding
  • isEqual
  • hash

等几个工具方法。


本文的讲解顺序

首先会讲解几个Runtime的基础知识,不理解这个,也就没办法掌握这几个JSON到Model转化的原理

  1. 介绍Runtime如何获取某一个类的全部属性的名字
  2. 介绍Runtime如何动态获取属性的类型

然后,会讲解Mantle本身

  1. 类的组织架构关系
  2. JSON到对象的处理流程(对象到JSON的过程类似)
  3. NSValueTransformer
  4. 如何自动实现NSCoding,NSCopying,hash等方法
  5. 异常处理
  6. 其他认为有用的,例如编译选项等

本文会很长,希望读者看完后能有些许收获,如果发现有任何地方有问题,欢迎指正,我会及时修改。


利用Runtime动态获取类的属性

首先,写两个类

@interface Base : NSObject
@property (copy,nonatomic)NSString * baseProperty;
@end

@interface Demo : Base
@property (nonatomic,strong)NSDate * createAt;
@property (nonatomic,copy)NSString * name;
@property (nonatomic,assign)CGFloat count;
@end

然后, 写一个方法来Log Property

-(void)logAllPropertys{
    uint count;
    objc_property_t * propertys = class_copyPropertyList(Demo.class,&count);
    @try {
        for (int i = 0; i < count ; i++) {
            objc_property_t  property = propertys[i];
            NSLog(@"%@",@(property_getName(property)));
        }
    }@finally {
        free(propertys);
    }
}

执行这个方法的Log

2016-05-26 22:51:48.996 LearnMantle[4670:165290] createAt
2016-05-26 22:51:49.001 LearnMantle[4670:165290] name
2016-05-26 22:51:49.001 LearnMantle[4670:165290] count

不难发现class_copyPropertyList仅仅是获取了当前类的属性列表,并没有获取基类的属性对象。所以对上述方法进行修改

-(void)logAllPropertys{
    Class cls = Demo.class;
    while (![cls isEqual:NSObject.class]) {
        uint count;
        objc_property_t * propertys;
        @try {
            propertys = class_copyPropertyList(cls,&count);
            cls = cls.superclass;
            for (int i = 0; i < count ; i++) {
                objc_property_t  property = propertys[i];
                NSLog(@"%@",@(property_getName(property)));
            }
        }@finally {
            free(propertys);
        }
    }
}

这里又个Tips:

class_copyPropertyList返回一个数组,这个数字必须要手动释放,所以用Try-Catch-Finally包裹起来。后面会介绍,Mantle如何用更简洁的方式来实现。


利用Runtime来获取属性的attributes

关键方法property_getAttributes,返回个一个C类型的字符串。

我们先声明一个这样的属性

@property (nonatomic,readonly,copy)id name;

然后,打印出它的attributes信息

    NSLog(@"%@",@(property_getAttributes(class_getProperty(self.class,@"name".UTF8String))));

可以看到Log是

2016-05-28 10:09:10.476 LearnMantle[731:17207] T@,R,C,N,V_name

这里的Attributes字符串是编码后的字符串,分为三个部分

  1. [email protected],T表示开头,后面跟着属性的类型,@表示id类型
  2. VnameV表示中间部分的结束,后面跟ivar名字,自动合成呢的情况下前面加下划线
  3. 中间R,C,N用逗号隔开,表示属性的描述,R表示readonlyC表示CopyN表示Nonatomic

Mantle和ReactiveCocoa都是采用了extobjc这个OC的Runtime工具类将属性的详细信息提取到一个结构体里的,原理都是一样的。提取完成的结构体是mtl_propertyAttributes


Matnle的类的组织架构

按照文件的方式,

  • MTLJSONAdapter.h,定义了协议MTLJSONSerializing和适配器类MTLJSONAdapter,这两个协议/类定义了接口来实现JSON-MTLModel的转换。
  • MTLModel.h,定义了协议MTLModel和基类MTLModel,基类MTLModel实现了isEqual,NSCopyinghash几个方法。
  • MTLModel+NSCoding.h,MTLModel的类别,让其支持NSCoding协议
  • MTLValueTransformer.hNSValueTransformer的子类,定义了将一个value转变成另一个value的接口。例如,返回的一个2020-01-01T15:33:30字符串,利用转换block转换成NSDate
  • 其它的都是工具类,提供工具方法,不全列出来了。

JSON->对象的处理过程

以下面代码调用为例(为了看起来不那么臃肿,省略不必要的代码)

Demo * demo = [MTLJSONAdapter modelOfClass:[Demo class] fromJSONDictionary:json error:&error];

看看这个方法的具体实现,就知道分为两个大的过程

+ (id)modelOfClass:(Class)modelClass fromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error {
    //1.根据modelClass初始化一个adapter
    MTLJSONAdapter *adapter = [[self alloc] initWithModelClass:modelClass];
    //2.adapter解析实际的JSON数据
    return [adapter modelFromJSONDictionary:JSONDictionary error:error];
}

现在看看整个第一大步,initWithModelClass,Mantle做了什么,

1.1,断言检查,并保存modelClass

    NSParameterAssert(modelClass != nil);
    NSParameterAssert([modelClass conformsToProtocol:@protocol(MTLJSONSerializing)]);
    //...
    _modelClass = modelClass;

1.2,获取所有的属性名字,获取MTLJSONSerialing中JSONKeyPathsByPropertyKey方法提供的属性名字->JSON key的映射,并进行合法性检查

    //属性名->JSON key的映射
    JSONKeyPathsByPropertyKey = [modelClass JSONKeyPathsByPropertyKey];
    //所有的属性集合
    NSSet *propertyKeys = [self.modelClass propertyKeys];
    //每一个属性进行检查
    for (NSString *mappedPropertyKey in _JSONKeyPathsByPropertyKey) {
        //检查属性名->JSON Key映射的属性名是否合法
        if (![propertyKeys containsObject:mappedPropertyKey]) {
            NSAssert(NO, @"%@ is not a property of %@.", mappedPropertyKey, modelClass);
            return nil;
        }
        //获取对应的JSON key
        id value = _JSONKeyPathsByPropertyKey[mappedPropertyKey];
        //如果是Array(支持JSON key是Array)
        if ([value isKindOfClass:NSArray.class]) {
            //Array中的每一个Key必须是String类型
            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]) {
            //检查JSON key是否时Array类型
            NSAssert(NO, @"%@ must either map to a JSON key path or a JSON array of key paths, got: %@.",mappedPropertyKey, value);
            return nil;
        }
    }

1.3 获取所有的NSValueTransformer,来方便做值转换(例如:服务器JSON返回的是2015-10-01T13:15:15,转换成NSDate)

    _valueTransformersByPropertyKey = [self.class valueTransformersForModelClass:modelClass];

用过Mantle的都知道,mantle利用”属性名+JSONTransformer”的方法名字来提供NSValueTransformer,

这里Mantle用了一些Runtime稍微高级点的东西,所以这个方法我会详细讲解

+ (NSDictionary *)valueTransformersForModelClass:(Class)modelClass {
    //...
    for (NSString *key in [modelClass propertyKeys]) {//对每一个key检查NSValueTransformer
        //根据属性名字+JSONTransformer来合成一个Selector
        SEL selector = MTLSelectorWithKeyPattern(key, "JSONTransformer");
        if ([modelClass respondsToSelector:selector]) {//如果提供了Transformer方法
            //获取IMP指针,也就是实际方法的执行体
            IMP imp = [modelClass methodForSelector:selector];
            //OC方法转换为C方法的时候,前两个参数是_cmd,和SEL,所以,这里做一个强制转化,方便下一行执行
            NSValueTransformer * (*function)(id, SEL) = (__typeof__(function))imp;
            //获取transformer,保存到Dictionary
            NSValueTransformer *transformer = function(modelClass, selector);
            if (transformer != nil) result[key] = transformer;
            continue;
        }
        //检查是否通过协议方法JSONTransformerForKey来提供NSValueTransformer
        if ([modelClass respondsToSelector:@selector(JSONTransformerForKey:)]) {
            //...
        }
        //把一个属性的类型,关键字,属性名字提取到一个结构体中
        objc_property_t property = class_getProperty(modelClass, key.UTF8String);
        if (property == NULL) continue;
        mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
        @onExit {
            free(attributes);
        };
        NSValueTransformer *transformer = nil;
        //如果某一个属性是id类型
        if (*(attributes->type) == *(@encode(id))) {
            //获得该属性的实际类名
            Class propertyClass = attributes->objectClass;
            if (propertyClass != nil) {
                //获取该类名型提供的NSValueTransformer,即类是否提供了keyJSONTransformer方法
                transformer = [self transformerForModelPropertiesOfClass:propertyClass];
            }
            //如果该类型也是一个MTLModel,并且实现了MTLJSONSerializing,获取该对象的NSValueTransformer,也就是保证了在MTLModel的一个属性也是一个MTLModel的时候能够正常工作
            if (nil == transformer && [propertyClass conformsToProtocol:@protocol(MTLJSONSerializing)]) {
                transformer = [self dictionaryTransformerWithModelClass:propertyClass];
            }
            //如果仍然没有获取到transformer,验证对于modalClass是否可转换
            if (transformer == nil) transformer = [NSValueTransformer mtl_validatingTransformerForClass:propertyClass ?: NSObject.class];
        } else {
        //不是ID类型,则是值类型的transformer
            transformer = [self transformerForModelPropertiesOfObjCType:attributes->type] ?: [NSValueTransformer mtl_validatingTransformerForClass:NSValue.class];
        }

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

    return result;
}

再看看第二大步,Adapter如何解析JSON

即这个方法

- (id)modelFromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error {
//...
}

2.1,检查是否实现了聚类方式解析JSON,例如解析这样的JSON

[
    {
        "key1":"value1",
        "key2":"value2"
    },
    {
        "key3":"value3",
        "key4":"value4"

    }
]

对应代码块

    if ([self.modelClass respondsToSelector:@selector(classForParsingJSONDictionary:)]) {
        //...
    }

2.2,对于每一个Property的名字,即propertyKey,获取对应的JSON key。根据JSON key 来获取对应的值,主要掉用mtl_valueForJSONKeyPath:success:error:

这个方法很简单,比如对应json的keyPath是person.name.first

先分解成person,name,first,然后一层一层的获取json[person][name][first],只不过Mantle在解析的时候,用了个for循环,来给用户反馈,到底错误在哪里。个人感觉用以下两个KVC的方法更简洁一点

//验证是否可用KVC
- validateValue:forKeyPath:error:
//用KVC来获取值
- valueForKeyPath:

2.3,对于2.2种,获取到的值,利用1.3的NSValueTransformer进行转换,这里只知道NSValueTransformer能够把一个值转换成另一个值就行了,后面会详细讲解如何转换的。

Tips:

这里要提到的是,Mantle采用了条件编译方式来处理异常,即debug模式下会抛出异常给开发者,但是release模式下,不会崩溃

#if DEBUG
    @throw ex;
#else
    //...
#endif

2.4 根据以上三步得到的值字典,对每一个key利用KVC进行设置值,KVC设置值之前,调用

[obj validateValue:&validatedValue forKey:key error:error]

来验证是否可以KVC


NSValueTransformer

官方文档

NSValueTranformer是一个抽象的基类,利用Cocoa Bindings技术来进行值的相互转换

既然是一个抽象基类,那么使用的时候要继承这个基类,然后实现必要的方法,从而才能进行相应的值转换。

例如,实现一个简单的NSDate<->NSString转换的Transformer

@interface LHValueTransformer : NSValueTransformer

@end

@implementation LHValueTransformer

+(BOOL)allowsReverseTransformation{
    return YES;
}
+(Class)transformedValueClass{
    return [NSString class];
}
-(NSDateFormatter *)dateFormatter{
    NSDateFormatter * formatter = [[NSDateFormatter alloc] init];
    formatter.dateFormat = @"yyyy-MM-dd HH:mm:ss";
    return formatter;
}
-(id)transformedValue:(id)value{
    NSAssert([value isKindOfClass:[NSDate class]], @"Should a NSDate value");

    return [[self dateFormatter] stringFromDate:value];
}
-(id)reverseTransformedValue:(id)value{
    NSAssert([value isKindOfClass:[NSString class]], @"Should be a NSString value");
    return [[self dateFormatter] dateFromString:value];
}
@end

然后,这样掉用

    NSValueTransformer * trans = [[LHValueTransformer alloc] init];

    NSDate * date = [NSDate date];
    NSString * str = [trans transformedValue:date];
    NSDate * date2 = [trans reverseTransformedValue:str];

MTLValueTransformer就是这样的一个子类,只不过它提供了正反两个转换的block作为接口。


isEqual,NSCopying,hash

实现NSCopying和hash很简单,就是基类根据Runtime动态的获取所有的属性,然后对应的进行操作就可以了


#pragma mark NSCopying

- (instancetype)copyWithZone:(NSZone *)zone {
    MTLModel *copy = [[self.class allocWithZone:zone] init];
    [copy setValuesForKeysWithDictionary:self.dictionaryValue];
    return copy;
}

#pragma mark NSObject

- (NSString *)description {
    NSDictionary *permanentProperties = [self dictionaryWithValuesForKeys:self.class.permanentPropertyKeys.allObjects];

    return [NSString stringWithFormat:@"<%@: %p> %@", self.class, self, permanentProperties];
}

- (NSUInteger)hash {
    NSUInteger value = 0;
    //每个value取hash值
    for (NSString *key in self.class.permanentPropertyKeys) {
        value ^= [[self valueForKey:key] hash];
    }

    return value;
}

- (BOOL)isEqual:(MTLModel *)model {
    if (self == model) return YES;
    if (![model isMemberOfClass:self.class]) return NO;

    for (NSString *key in self.class.permanentPropertyKeys) {
        id selfValue = [self valueForKey:key];
        id modelValue = [model valueForKey:key];
        //每一个value取isEqual
        BOOL valuesEqual = ((selfValue == nil && modelValue == nil) || [selfValue isEqual:modelValue]);
        if (!valuesEqual) return NO;
    }

    return YES;
}

NSCoding

NSCoding的支持有些复杂,源代码MTLModel+NSCoding.m

对于initWithCoder:

1. 根据Runtime,获取所有的属性名字

2. 对于每一个属性,检查是否响应decodeWithCoder:modelVersion:,也就是说,支持属性也是MTLModel对象,如果是,则调用decodeWithCoder:modelVersion:解析这个MTLModel

3. 如果不是MTLModel子类,则调用decodeObjectForKey来解析,这里的key就是属性的名字

encodeWithCoder类似,不做讲解


异常处理

Mantle中,有一些

@try{}
@catch{}
@finally{}

并且在catch模块中

#if DEBUG
    @throw ex;
#else
    //其它处理
#endif

这样能够方便调试错误,并且在运行时的时候不崩溃。

同时,你还能看到这样的代码

mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
@onExit {
    free(attributes);
};

这里的@onExit是一个宏定义,保证代码在在当前域返回(return,break,异常)始终能执行到。其实本质就是把代码放到了finally里


__attribute__

__attribute__机制能够为方法,变量,类型增加额外的属性。

增加的额外属性,能够让编译器进行额外的检查,从而提供额外的提示

比如

@property (nonatomic, strong, readonly) id<MTLJSONSerializing> model __attribute__((unavailable("Replaced by -modelFromJSONDictionary:error:")));

+ (NSArray *)JSONArrayFromModels:(NSArray *)models __attribute__((deprecated("Replaced by +JSONArrayFromModels:error:"))) NS_SWIFT_UNAVAILABLE("Replaced by +JSONArrayFromModels:error:");

就分别提示model当前不可用unavailable,和JSONArrayFromModels方法被deprecated

后面有时间了,系统的整理下所有的__attribute__,今天很晚了,先这样吧


后续

下一篇会写React Native的博客,然后MBProgressHud或者AFN的源码分析

时间: 2024-10-22 06:00:09

iOS开源库源码解析之Mantle的相关文章

iOS开源库源码解析之SDWebImage

来自Leo的原创博客,转载请著名出处 我的stackoverflow 这个源码解析系列的文章 AsnycDispalyKit SDWebImage(本文) 前言 SDWebImage是iOS开发中十分流行的库,大多数的开发者在下载图片或者加载网络图片并且本地缓存的时候,都会用这个框架.这个框架相对来说,源代码还是比较少的.本文会详细的讲解这些类的架构关系和原理. 本文会先介绍类的整体架构关系,先有一个宏观的认识.然后讲解sd_setImageWithURL的加载逻辑,因为这是SDWebImage

iOS开源库源码解析之AsnycDispalyKit

来自Leo的原创博客,转载请著名出处 我的stackoverflow 前言 最近心血来潮,想研究下FaceBook的AsnycDispalyKit的源代码,学习一些界面优化的技术以及编码风格.这篇文章,会详细的记录下我认为对新手有用的部分.后面有空的时候,继续研究其他几个iOS开发很流行的库-AFNetworking,SDWebImage,MBProgressHud,Mantle等`.AsnycDisplayKit是一个非常庞大的库,所以我尽量捞干的讲. 关于AsyncDisplayKit 文档

Android 开源项目源码解析(第二期)

Android 开源项目源码解析(第二期) 阅读目录 android-Ultra-Pull-To-Refresh 源码解析 DynamicLoadApk 源码解析 NineOldAnimations 源码解析 SlidingMenu 源码解析 Cling 源码解析 BaseAdapterHelper 源码分析 Side Menu.Android 源码解析 DiscreteSeekBar 源码解析 CalendarListView 源码解析 PagerSlidingTabStrip 源码解析 公共

co函数库源码解析

一.co函数是什么 co 函数库是著名程序员 TJ Holowaychuk 于2013年6月发布的一个小工具,用于 Generator 函数的自动执行.短小精悍只有短短200余行,就可以免去手动编写Generator 函数执行器的麻烦 二.co函数怎么用 举个栗子就能清楚的知道如何使用co函数 1 function* gen(){ 2 var f1 = yield func1; 3 var f2 = yield fnuc2; 4 //sth to do 5 }; 手动执行和co函数执行的写法如下

Android 常用开源框架源码解析 系列 (十)Rxjava 异步框架

一.Rxjava的产生背景 一.进行耗时任务 传统解决办法: 传统手动开启子线程,听过接口回调的方式获取结果 传统解决办法的缺陷: 随着项目的深入.扩展.代码量的增大会产生回调之中套回调的,耦合度高度增加的不利场景.对代码维护和扩展是很严重的问题. RxJava本质上是一个异步操作库 优点: 使用简单的逻辑,处理复杂 ,困难的异步操作事件库;在一定程度上替代handler.AsyncTask等等 二.传统的观察者模式 使用场景 1.一个方面的操作依赖于另一个方面的状态变化 2.如果在更改一个对象

Android 常用开源框架源码解析 系列 (九)dagger2 呆哥兔 依赖注入库

一.前言 依赖注入定义 目标类中所依赖的其他的类的初始化过程,不是通过手动编码的方式创建的. 是将其他的类已经初始化好的实例自动注入的目标类中. "依赖注入"也是面向对象编程的 设计模式 -----组合的配套使用 作用 :降低程序的耦合,耦合就是因为类之间的依赖关系所引起的 产生场景:在一个对象里去创建另一个对象的实例 问题:过多的类,对象之间的依赖会造成代码难以维护. 不符合开闭原则的对象的引用写法:错误示例: public class ClassA { classB b ; pub

Android常用库源码解析

图片加载框架比较 共同优点 都对多级缓存.线程池.缓存算法做了处理 自适应程度高,根据系统性能初始化缓存配置.系统信息变更后动态调整策略.比如根据 CPU 核数确定最大并发数,根据可用内存确定内存缓存大小,网络状态变化时调整最大并发数等. 支持多种数据源支持多种数据源,网络.本地.资源.Assets 等 不同点 Picasso所能实现的功能,Glide都能做,无非是所需的设置不同.但是Picasso体积比起Glide小太多. Glide 不仅是一个图片缓存,它支持 Gif.WebP.缩略图.Gl

Android 常用开源框架源码解析 系列 (十一)picasso 图片框架

一.前言 Picasso 强大的图片加载缓存框架 api加载方式和Glide 类似,均是通过链式调用的方式进行调用 1.1.作用 Picasso 管理整个图片加载.转换.缓存等策略 1.2.简单调用: Picasso .with(this 传入一个单例,上下文).load("url"/file文件/资源路径) .into() 1.2.1 .一些简单的链式调用参数 .placeholder(R.drawable.xx)  //网络未加载完成的时候显示的本地资源图片 .error(R.dr

Android 开源项目源码解析之DynamicLoadApk 源码解析

1. 功能介绍 1.1 简介 DynamicLoadApk 是一个开源的 Android 插件化框架. 插件化的优点包括:(1) 模块解耦,(2) 动态升级,(3) 高效并行开发(编译速度更快) (4) 按需加载,内存占用更低等等. DynamicLoadApk 提供了 3 种开发方式,让开发者在无需理解其工作原理的情况下快速的集成插件化功能. 宿主程序与插件完全独立 宿主程序开放部分接口供插件与之通信 宿主程序耦合插件的部分业务逻辑 三种开发模式都可以在 demo 中看到. 1.2 核心概念