从YYModel源码中可以学到什么:前篇

YYModel是一个高性能模型库。为了了解底层实现,笔者初步打算用两篇文章学习YYModel源码。本文是第一篇文章,初步分析YYModel整体架构及为使用者暴露出来的接口,相当于一个使用教程。第二篇文章从YYModel源码中可以学到什么:前篇一步步分析YYModel是如何转换成Model的。 接下来我们开始吧!

YYModel一个高性能模型框架。

作者在Github上给出的性能对比图(iphone 6 y:时间)

YYModel:具体以下特点:高性能、自动类型转换、类型安全、非侵入性、轻量等。

关于如何使用YYModel查看文档和示例【传送门】

本文主要任务,分析YYModel的整体架构,实现思路,涉及到的知识点。

版本:1.0.4

文件结构

YYModel文件目录

YYModel,只有5个文件。接下我们会具体看这五个文件都做了什么工作。

  • YYModel.h头文件,通过#import该文件使用库。
  • YYClassInfo.h 根据名字应该能猜出,关于Class信息的文件。
  • NSObject + YYModel.h 这个NSObject的一个Category。还定义了一些内部类。

YYModel头文件

该文件只是一个头文件,代码很少。

1
2
3
4
5
6
7
8
9
10
#if __has_include(<YYModel/YYModel.h>)
FOUNDATION_EXPORT double YYModelVersionNumber;
FOUNDATION_EXPORT const unsigned char YYModelVersionString[];
#import <YYModel/NSObject+YYModel.h>
#import <YYModel/YYClassInfo.h>
#else
#import "NSObject+YYModel.h"
#import "YYClassInfo.h"
#endif

拓展:

  1. FOUNDATION_EXPORT是用来定义常量的,另外一个经常用到的#define定义常量。

    那么两者的区别?
    假设分别使用两者定义字符串常量,前者可以通过==来判断字符串是否相等,后者则需要使用isEqualToString:来判断。因为,前者比较的是字符串指针地址,后者比较每个字符,因此前者效率更高。

  2. __has_include()

    1
    2
    3
    4
    5
    6
    	#if __has_include(<UIKit/UIKit.h>)
    // 包含
    #else
    // 不包含
    #endif

    判断UIKit库是否存在。

YYClassInfo

YYClassInfo文件中定义四个类,涉及到Runtime知识,请看这篇文章博文或者直接查看objc4源码

YYClassInfo

  • YYClassIvarInfo

    该类对应实例变量信息(ivars),包含:名称,偏移量,类型编码,类型;其中类型请查看【官方文档Type Encodings】

  • YYClassMethodInfo

    方法的信息类,包含,方法名称,SEL,IMP,参数类型编码,返回值类型编码等。

  • YYClassPropertyInfo

    属性信息类,包含名称,类型,类型编码,ivar名称,类,协议列表,setter/getter等。

  • YYClassInfo:类信息。

拓展

Ivar, Method, Property, SEL, IMP都是什么?

理解这些需要对runtime了解,上面给出了博文链接,这里简单复习一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct objc_method *Method; // 方法
typedef struct objc_ivar *Ivar; // 实例变量
typedef struct objc_category *Category; //分类
typedef struct objc_property *objc_property_t; // 属性

struct objc_class {
Class isa
Class super_class // 指向父类
const char * name // 名称
long version
long info
long instance_size
struct objc_ivar_list * ivars // 实例变量表
struct objc_method_list * * methodLists //方法表
struct objc_cache * cache
struct objc_protocol_list * protocols //协议表

};
/* Use `Class` instead of `struct objc_class *` */

Ivar指实例变量,存放在实例变量表中。Method方法,存放在方法表中。

接下再看一下Objc_method结构体

1
2
3
4
5
struct objc_method {
SEL method_name
char * method_types
IMP method_imp
}

SEL指方法名称,IMP指方法实现。

NSObject + YYModel

该文件定义三个分类和一个协议,以及两个内部类,下面是.h文件中提供的接口。

NSObject分类

提供了一些datamodel转换的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 根据接收到JSON创建一个实例,该方法是线程安全的。
// json对象可以是 NSDictionary,NSString,NSData.
+ (nullable instancetype)yy_modelWithJSON:(id)json;

// 字典转Model
+ (nullable instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary;

// 通过json设置属性, 无效的数据会被忽略
- (BOOL)yy_modelSetWithJSON:(id)json;
- (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic;

// model转json对象(NSDictionary/NSArray), NSData, NSString
- (nullable id)yy_modelToJSONObject;
- (nullable NSData *)yy_modelToJSONData;
- (nullable NSString *)yy_modelToJSONString;

#pragma mark - 其他快捷方法
// 拷贝(NSCoping协议)
- (nullable id)yy_modelCopy;

// 编码和解码(对应NSCoding协议的两个方法)
- (void)yy_modelEncodeWithCoder:(NSCoder *)aCoder;
- (id)yy_modelInitWithCoder:(NSCoder *)aDecoder;

// NSObject协议
// 哈希值
- (NSUInteger)yy_modelHash;
// 相等判断
- (BOOL)yy_modelIsEqual:(id)model;
// Debug描述
- (NSString *)yy_modelDescription;

以上NSObject分类中提供的接口,具体实现稍后学习。

1
2
3
4
5
6
7
// 直接添加以下代码即可自动完成
- (void)encodeWithCoder:(NSCoder *)aCoder { [self yy_modelEncodeWithCoder:aCoder]; }
- (id)initWithCoder:(NSCoder *)aDecoder { self = [super init]; return [self yy_modelInitWithCoder:aDecoder]; }
- (id)copyWithZone:(NSZone *)zone { return [self yy_modelCopy]; }
- (NSUInteger)hash { return [self yy_modelHash]; }
- (BOOL)isEqual:(id)object { return [self yy_modelIsEqual:object]; }
- (NSString *)description { return [self yy_modelDescription]; }

NSArray分类

从json-array中创建一个数组, 其实也是遍历循环调用字典转Model。

1
+ (nullable NSArray *)yy_modelArrayWithClass:(Class)cls json:(id)json;

NSDictionary分类

从json创建字典。

1
+ (nullable NSDictionary *)yy_modelDictionaryWithClass:(Class)cls json:(id)json;

YYModel协议

YYModel协议,通过实现响应的方法,可以提供白名单,黑名单,自定义属性名称等功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 自定义属性名称,可以将json中的名称映射到自定义的名称,可以解决冲突例如`id`。
+ (nullable NSDictionary<NSString *, id> *)modelCustomPropertyMapper;

// 如果属性是一个容器对象,例如NSArray/NSSet/NSDictonary,实现该方法可以返回一个映射字典(property -> class)
+ (nullable NSDictionary<NSString *, id> *)modelContainerPropertyGenericClass;

+ (nullable Class)modelCustomClassForDictionary:(NSDictionary *)dictionary;

// 白名单和黑名单(若实现,忽略黑名单,只处理白名单)
+ (nullable NSArray<NSString *> *)modelPropertyBlacklist;
+ (nullable NSArray<NSString *> *)modelPropertyWhitelist;

- (NSDictionary *)modelCustomWillTransformFromDictionary:(NSDictionary *)dic;

// 数据验证和自定义转换
// 当JSON转为Model完成后,会调用该方法。可以在该方法中进行校验工作,返回NO该Model被忽略,也可以完成一些转换工作。
- (BOOL)modelCustomTransformFromDictionary:(NSDictionary *)dic;
- (BOOL)modelCustomTransformToDictionary:(NSMutableDictionary *)dic;

拓展

  1. 在源码中会发现有一对NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END宏。

    在Swift中存在Option类型,可以使用!?声明变量,但是在OC中没有这个特性。出现新的关键词用于OC转Swift时区分能否为空。

nullable && nonnull

nullable指对象可以为NULL。

nonnull指对象不可以为NULL。

如果不遵循这一规则,编译器就会给出警告。

为了简化书写,在NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END宏之间的代码,默认都是nonnull。所以我们只需指定哪些nullable的指针就可以了。
StackOverflow关于该宏的问题。

其他知识点

接下来补充一些知识点,或许对以后开发有帮助。

NS_OPTIONS && NS_ENUM

这是两个简单方便的宏定义,从iOS6开始,他们取代了原来的enum

例如:

1
2
3
4
5
6
typedef NS_ENUM(NSInteger, UITableViewCellStyle) {
UITableViewCellStyleDefault,
UITableViewCellStyleValue1,
UITableViewCellStyleValue2,
UITableViewCellStyleSubtitle
};

其中第一个元素存储类型。第二个参数是名字。

另外,enum也可以被定义为按位掩码。用简单的OR和AND数学运算既可实现一个整型值的编码。请看这篇文章《NS_ OPTIONS && NS _ENUM - NShipster》

小结

本章主要整理YYModel整体框架以及开发者提供的接口,并没有涉及到内部实现。接下来的文章,我将会一步步分析源码实现。

参考

Github - ibireme/YYModel