构建iOS-Model层(一)最简单的实现Model解析

Model层框架

在iOS中,Model层的框架很多,Github中,star数众多的有:

RestKitMantleMJExtensionJSONModelYYModel

通过研读这些技术框架的实现,想自己实现一个Model层框架。关于这些框架,YY还写了一篇评测:

iOS JSON 模型转换库评测

引言

那么,这篇博客就是入门,用基本的原理,特定的对象实现Model层解析。

问题来了!

如何将字典转换为模型?

首先,要转换,我们就需要知道模型中有哪些属性,知道了有哪些属性,就需要去字典里找对应该属性的值了。

好了,假如我在字典里找到该属性对应值了,那么赋给属性。

转换完成!

简而言之,字典转模型,总的过程分为三步:

  1. 获取模型的属性;
  2. 根据属性去字典匹配属性获得其对应的值;
  3. 将值赋给模型属性。

以一个实例来讲:

1
2
3
4
5
6
@interface JSPerson : NSObject
@property (nonatomic ,assign) NSInteger age;
@property (nonatomic ,assign) NSInteger height;
@property (nonatomic ,copy) NSString *name;
@property (nonatomic ,copy) NSString *bio;
@end

我们要将字典转为模型。将模型JSPerson中的属性ageheightnamebio抽取。

1
2
3
4
5
NSDictionary *dic = @{
@"name":@"wenghengcong",
@"age":@"25",
@"height":@"170"
};

根据上面的属性,去字典dic找对应的值,得到name属性的值是wenghengcong,以此类推。

最后,将dic里匹配的值,赋值给JSPerson模型。

这样,就完成了整个字典转模型的过程。

好了,下面开始!

获取模型,即类的属性,首先,我们知道大部分类都是继承与NSObject

其次,关键的一点是,即通过runtime来获取模型的属性。

从runtime.h 读取的Class里介绍了一个类的属性,其实在runtime里还包括该类的很多信息,比如该类的方法信息,该类的属性(包括成员变量)等。而runtime也提供了方法来获取一个类的这些信息。

在“user/include>objc>runtime.h”头文件,声明了这些方法。

属性在runtime中的结构

runtime.h文件里声明:

/// An opaque type that represents an Objective-C declared property.
typedef struct objc_property *objc_property_t;

objc_property结构体可以在objc的源码里找到:

1
2
3
4
struct old_property {
const char *name;
const char *attributes;
};

可以看到,属性包含nameattributes。其中name指的是属性名。那么attributes呢?
attributes是一个字符串,表明了该属性的特性,例如在前面提到的JSPerson类中的name属性,其对应的为T@"NSString",C,N,V_name
其中,T是一个前缀,而@"NSString该属性是NSString,而不是是基本类型int,抑或是NSArray
C则表明了内存管理语义,即copy
N则表明该属性的非原子性,即nonatomic
V_name,则是结束标志,即V_属性名结束。

如何获取属性

了解了属性的结构。那么从一个模型中获取属性是其首要的工作。

1
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)

上面的方法,就是从cls类中获取属性列表,而outCount是属性数目。

我们给NSObject(为什么是NSObjcet)添加一个分类,来获取类的属性。

1
2
3
4
@interface NSObject (JSModel)
+ (NSArray *)properties;
- (instancetype)modelWithDic:(NSDictionary *)dic;
@end

其中,properties是该属性数组,modelWithDic是字典转模型的方法。

1
2
3
4
5
6
7
8
9
10
+ (NSArray *)properties {
NSMutableArray *propertiesArr = [NSMutableArray array];
unsigned int count = 0;
objc_property_t *properties = class_copyPropertyList(self, &count);
for (int i = 0; i < count; i++) {
objc_property_t property = properties[i];
[propertiesArr addObject:(__bridge id _Nonnull)(property)];
}
return [propertiesArr copy];
}

这样就获取到了属性数组properties

如何获取属性对应的nameattributes

我们了解到,属性在运行时的结构体,那么就需要获取其name,因为将来要通过name给模型赋值。但是attributes有什么用呢?没用吗?

有用,我们会根据其表面的属性是基本类型还是Foundation类或者是自定义类,在取出字典里值赋值的时候做一些必要处理,比如内存管理方面的,比如原子性的。

好了,既然要获取,直接出方法吧,我等不及了。

1
2
const char *name = property_getName(property);
const char *attributes = property_getAttributes(property);

JSPerson类中,打印其属性,如下:
​ NSLog(@”name:%s–attributes:%s”,name,attributes);



但是,下面怎么办?
将取出的name作为key,去字典中取出key对应的value
​ id value = [dic valueForKey:propertyInfo.name];

好,取出的valueid类型,我们要知道,需要将其转换为模型中属性对应的类型,比如,bioNSString类型,需要将value转换为NSString类型,才能赋值给模型中的属性。

然而,我们在这里,直接利用KVC赋值,不需要对象类型也Ok。但类型对于后面进一步做更多的处理还是必须的。

如何完成value类型转换,转换失败了怎么办?

要转换的类型,这部分信息其实我们已经拿到了,在attributes中描述了该属性的类型,内存管理语义等。我们要做的就是从T@"NSString",C,N,V_bio中取出类型信息@"NSString"。到现在,我们有三个信息要保存了name,attributes,类型,所以,我们定义了JSClassInfo来存储这部分信息。

1
2
3
4
5
6
7
8
@interface JSClassPropertyInfo : NSObject
@property (nonatomic, assign, readonly) objc_property_t property; ///< property
@property (nonatomic, strong, readonly) NSString *name;
@property (nonatomic, assign, readonly) JSEncodingType type;
@property (nonatomic, strong, readonly) NSString *typeEncoding;
@property (nullable, nonatomic, assign, readonly) Class cls; ///< may be nil
- (instancetype)initWithProperty:(objc_property_t)property;
@end

其中,property保存了从模型中获取到的原始的属性,通过方法initWithProperty初始化,将property结构中的信息,逐一保存在*nametype等中。

这样,重写NSObjectproperties方法,即:

1
2
3
4
5
6
7
8
9
10
11
12
+ (NSArray *)properties {
NSMutableArray *propertiesArr = [NSMutableArray array];
unsigned int count = 0;

objc_property_t *properties = class_copyPropertyList(self, &count);
for (int i = 0; i < count; i++) {
objc_property_t property = properties[i];
JSClassPropertyInfo *propertyInfo = [[JSClassPropertyInfo alloc]initWithProperty:property];
[propertiesArr addObject:propertyInfo];
}
return [propertiesArr copy];
}

另外,要着重说明的是,type是指属性为基础数据类型,或者id等特俗类型使用的,其定义为:

1
2
3
4
5
6
7
8
9
typedef NS_OPTIONS(NSUInteger, JSEncodingType) {
JSEncodingTypeMask = 0xFF, ///< mask of type value
JSEncodingTypeUnknown = 0, ///< unknown
JSEncodingTypeVoid = 1, ///< void
JSEncodingTypeBool = 2, ///< bool
JSEncodingTypeInt = 3, ///< char / BOOL
JSEncodingTypeObject = 4, ///< id
JSEncodingTypeClass = 5, ///< Class
};

当然,上面的JSEncodingType是不完整的,因为,我们只针对JSPerson模型编写这套转换层。
比如,JSPerson中的heightattributesTi,N,V_height。其中,i就是表示int。关于类型编码符号,见“参考”2,3,4。
那么,像其他NSStringNSArray和自定义类型保存在哪里?保存在cls里。到此,已经基本上将类型转换工作完成。代码入下:

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
- (instancetype)initWithProperty:(objc_property_t)property {
if(!property) return nil;
self = [self init];
_property = property;
const char *name = property_getName(property);
if (name) {
_name = [NSString stringWithUTF8String:name];
}
const char *attributes = property_getAttributes(property);
if (attributes) {
_typeEncoding = [NSString stringWithUTF8String:attributes];
NSArray *typeArr = [_typeEncoding componentsSeparatedByString:@","];
NSString *typeStr;
if (typeArr) {
typeStr = [typeArr[0] substringFromIndex:1];
}

if (typeStr.length >= 3) {
NSUInteger len = [typeStr length];
typeStr = [typeStr substringWithRange:NSMakeRange(2, len-3)];
_cls = NSClassFromString(typeStr);
}else{
_type = JSEncodingGetType(typeStr);
_cls = nil;
}
}
return self;
}

其中,转换type

1
2
3
4
5
6
7
8
9
10
11
12
JSEncodingType JSEncodingGetType(NSString *typeEncoding) {
if ([typeEncoding isEqualToString:@"q"]) {
return JSEncodingTypeInt;
}else if ([typeEncoding isEqualToString:@"B"]){
return JSEncodingTypeBool;
}else if ([typeEncoding isEqualToString:@"v"]){
return JSEncodingTypeBool;
}else if ([typeEncoding isEqualToString:@"#"]){
return JSEncodingTypeBool;
}
return JSEncodingTypeUnknown;
}

转换失败了,怎么办,目前只做最简单的处理,我们不考虑容错。之后会补充这部分。

赋值

到现在为止,我们已经从获取属性,到取出字典中值,以及类型转换都已经完成。最后一步,赋值:
​ - (instancetype)modelWithDic:(NSDictionary )dic {
​ NSArray
properties = [self.class properties];
​ for (int i = 0; i < properties.count ; i++) {
​ JSClassPropertyInfo *propertyInfo = properties[i];
​ id value = [dic valueForKey:propertyInfo.name];
​ if (!value) continue;
​ if (propertyInfo.type == JSEncodingTypeInt){
​ // 字符串->数字
​ if ([value isKindOfClass:[NSString class]])
​ value = [[[NSNumberFormatter alloc]init] numberFromString:value];
​ }else if([value isKindOfClass:[NSString class]]){
​ }
​ [self setValue:value forKey:propertyInfo.name];
​ }
​ return self;
​ }

需要说明的是,其实并不需要将字符串转换为数字这一步骤,因为KVC会帮我们完成这一步,之所以要将这一步列出,主要是因为根据前面的类型来做一定的处理,而且之后由于性能的原因,我们不会采用KVC,此时,属性的类型就将发挥作用。

代码

git

参考

  1. 跟着MJExtension实现简单的字典转模型框架

  2. Type Encodings

  3. Objective-C Runtime Programming Guide-Type Encodings

  4. Objective-C Runtime Programming Guide-NextPrevious
    Declared Properties