最简单的字典
首先,从最简单的字典开始.
NSDictionary *dict = @{
@"name" : @"Jack",
@"icon" : @"lufy.png",
@"age" : @"20",
@"height" : @1.55,
@"money" : @"100.9",
@"sex" : @(SexFemale),
@"gay" : @"1"
}
目标是拿到字典里的值(value)
对User
模型进行赋值.模型的属性名
对应字典的键(key)
.
typedef enum {
SexMale,
SexFemale
} Sex;
@interface User : NSObject
/** 名称 */
@property (copy, nonatomic) NSString *name;
/** 头像 */
@property (copy, nonatomic) NSString *icon;
/** 年龄 */
@property (assign, nonatomic) unsigned int age;
/** 身高 */
@property (copy, nonatomic) NSString *height;
/** 财富 */
@property (strong, nonatomic) NSNumber *money;
/** 性别 */
@property (assign, nonatomic) Sex sex;
/** 同性恋 */
@property (assign, nonatomic, getter=isGay) BOOL gay;
@end
最直接的方法是:
User *user = [[User alloc] init];
user.name = dict[@"name"];
user.icon = dict[@"icon"];
....
假如属性数量一多,人工手写大量样板代码
将耗费大量时间和精力,毫无意义.
如果要写一个框架自动帮我们转模型出来,大致思路如下:
1.遍历模型中的属性
,然后拿到属性名
作为键值
去字典中寻找值
.
2.找到值
后根据模型的属性
的类型
将值
转成正确的类型
3.赋值
首先进行第一步:
遍历模型中的
属性
,然后拿到属性名
作为键值
去字典中寻找值
.
方法伪代码:
[模型类 遍历属性的方法];
为了方便使用,创建一个叫NSObject+Property
的分类.写一个获取所有属性的方法.
@interface NSObject (Property)
+ (NSArray *)properties;
@end
假设我们看不见一个类的.h
和.m
,有什么办法可以获取它所有的实例变量呢?答案是通过运行时机制
.当在实现+ (NSArray *)properties
方法时,需要导入运行时库.然后使用库中的API提供的函数得到一个类的方法列表.
注:在旧版本的MJExtension中,获取成员变量是通过class_copyIvarList来获取的类的所有实例变量,根据MJ源码中的说明:"在 swift 中,由于语法结构的变化,使用 Ivar 非常不稳定,经常会崩溃!",所以改用了获取成员属性的方法.
另外,不管是获取成员属性还是实例变量,都不能获取到父类的列表.(本人忽略了对父类成员属性的获取,后期更新中会更新这一失误).
// Any instance variables declared by superclasses are not included.
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
返回的是叫objc_property_t
的一个结构体指针,并且通过传入值引用能够得到属性的个数.
#import "NSObject+Property.h"
#import <objc/runtime.h>
@implementation NSObject (Property)
+ (NSArray *)properties{
NSArray *propertiesArray = [NSMutableArray array];
// 1.获得所有的属性
unsigned int outCount = 0;
objc_property_t *properties = class_copyPropertyList(self, &outCount);
// .....
return propertiesArray;
}
@end
来到这里已经获取到了属性列表,那么objc_property_t
指向的结构体内部是怎样的呢.通过搜寻<objc/runtime.h>
头文件并看不到objc_property_t
的定义的.但好在runtime
开源,我们搜寻到了相关的定义.
typedef struct property_t *objc_property_t;
struct property_t {
const char *name;
const char *attributes;
};
由于知道了结构体的内部构造,就可以获取内部的成员变量.例如以下方法:
typedef struct property_t {
const char *name;
const char *attributes;
} *propertyStruct;
@implementation NSObject (Property)
+ (NSArray *)properties{
NSArray *propertiesArray = [NSMutableArray array];
// 1.获得所有的属性
unsigned int outCount = 0;
objc_property_t *properties = class_copyPropertyList(self, &outCount);
for (int i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
NSLog(@"name:%s---attributes:%s",((propertyStruct)property)->name,((propertyStruct)property)->attributes);
}
return propertiesArray;
}
@end
在外部调用+ (NSArray *)properties
方法能够打印出一个类的全部属性,如:
NSArray *propertyArray = [User properties];
得到控制台输出:
从输出中可以看到该结构体的name
成员表示成员属性的名字,attributes
表示成员属性中的一些特性(如是什么类,原子性还是非原子性,是strong还是weak还是copy,生成的成员变量名等信息)...
从苹果的官方文档(Objective-C Runtime Programming Guide)可以得知,attributes
是一个类型编码字符串.可以使用property_getAttributes
函数获得这个类型编码字符串.这个字符串以T
作为开始,接上@encode
类型编码和一个逗号,以V
接上实例变量名作为结尾,在它们之间是一些其他信息,以逗号分割.具体内容可以看官方文档中详细的表格.
在实际赋值过程中,我们并不用关心该属性的内存管理语义,生成的成员变量名,或者其他什么信息.在attributes
中,只需要知道它所属的类
或者是什么基本数据类型
,即T
至第一个逗号之前
中间的内容,如果是类
的话还需要将@"
和"
去掉.
实际上,框架提供的运行时库已经给我们提供获取属性名
和属性特性
的函数了.通过下面方式也能打印出相同结果.
NSLog(@"name:%s---attributes:%s",property_getName(property),
property_getAttributes(property));
从runtime
源码中可以看到这两个函数的内部是这样实现的:
const char *property_getName(objc_property_t prop)
{
return prop->name;
}
const char *property_getAttributes(objc_property_t prop)
{
return prop->attributes;
}
再回顾前面说的思路,这时会更清晰:
1.拿到模型的属性名(注意属性名和成员变量名的区别),和对应的数据类型.
2.用该属性名作为键去字典中寻找对应的值.
3.拿到值后将值转换为属性对应的数据类型.
4.赋值.
现在已经进行到第一步,并且拿到了属性名
,但是数据类型
还要进一步截取,截取方法如下:
for (int i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
// 为了以后方便,将C字符串转换成OC对象
NSString *name = @(property_getName(property));
NSString *attributes = @(property_getAttributes(property));
NSUInteger loc = 1;
NSUInteger len = [attributes rangeOfString:@","].location - loc;
NSString *type = [attributes substringWithRange:NSMakeRange(loc, len)];
NSLog(@"%@",type);
}
控制台结果显示我们能够截取到其中的类型了.
该部分源码请看项目实例代码中的<打印类型>
回归我们拿到这些数据类型
的初衷,是为了是用字典中的值的类型
与模型中属性的类型
进行对比,想要对比,需要拿到属性的类型
,因此需要将这些编码转换成一个表示类型的类,创建一个类用来包装类型.
/**
* 包装一种类型
*/
@interface MJPropertyType : NSObject
/** 是否为id类型 */
@property (nonatomic, readonly, getter=isIdType) BOOL idType;
/** 是否为基本数字类型:int、float等 */
@property (nonatomic, readonly, getter=isNumberType) BOOL numberType;
/** 是否为BOOL类型 */
@property (nonatomic, readonly, getter=isBoolType) BOOL boolType;
/** 对象类型(如果是基本数据类型,此值为nil) */
@property (nonatomic, readonly) Class typeClass;
@end
OC对象可以通过Class
来表示类型,而基本数据类型只能用布尔来标识.
把这些名字和类型遍历出来,肯定是为了以后有用,所以需要把它们存起来,由于它们是一个"整体",所以还是设计一个类将他们包装起来比较好.创建一个包装成员属性的类—MJProperty
.
@interface MJProperty : NSObject
/** 成员属性的名字 */
@property (nonatomic, readonly) NSString *name;
/** 成员属性的类型 */
@property (nonatomic, readonly) MJPropertyType *type;
@end
这时,代码就可以进行重构了,将属于不同类的功能封装到对应的类上,让MJProperty
提供一个类方法用于返回一个将objc_property_t
进行包装的类.
for (int i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
MJProperty *propertyObj = [MJProperty propertyWithProperty:property];
}
propertyWithProperty:
方法的实现如下:
+ (instancetype)propertyWithProperty:(objc_property_t)property{
return [[MJProperty alloc] initWithProperty:property];
}
- (instancetype)initWithProperty:(objc_property_t)property{
if (self = [super init]) {
_name = @(property_getName(property));
_type = [MJPropertyType propertyTypeWithAttributeString:@(property_getAttributes(property))];;
}
return self;
}
MJPropertyType
也提供类方法用于包装类型:
+ (instancetype)propertyTypeWithAttributeString:(NSString *)string{
return [[MJPropertyType alloc] initWithTypeString:string];
}
- (instancetype)initWithTypeString:(NSString *)string
{
if (self = [super init])
{
NSUInteger loc = 1;
NSUInteger len = [string rangeOfString:@","].location - loc;
NSString *type = [string substringWithRange:NSMakeRange(loc, len)];
NSLog(@"%@",type);
}
return self;
}
重构完成之后,结构显得更加清晰.更有利于接下来的工作.下面继续完成type
的提取.
该部分源码请看项目实例代码中的<重构>
上面获取到的这些类型,是类型编码,在苹果文档中告诉了我们编码对应的类型:
根据这个对应关系的图表,我们将常用的几个编码定义成常量字符串或者宏表示它所对应的类型,便于编码和阅读:
/**
* 成员变量类型(属性类型)
*/
NSString *const MJPropertyTypeInt = @"i";
NSString *const MJPropertyTypeShort = @"s";
NSString *const MJPropertyTypeFloat = @"f";
NSString *const MJPropertyTypeDouble = @"d";
NSString *const MJPropertyTypeLong = @"q";
NSString *const MJPropertyTypeChar = @"c";
NSString *const MJPropertyTypeBOOL1 = @"c";
NSString *const MJPropertyTypeBOOL2 = @"b";
NSString *const MJPropertyTypePointer = @"*";
NSString *const MJPropertyTypeIvar = @"^{objc_ivar=}";
NSString *const MJPropertyTypeMethod = @"^{objc_method=}";
NSString *const MJPropertyTypeBlock = @"@?";
NSString *const MJPropertyTypeClass = @"#";
NSString *const MJPropertyTypeSEL = @":";
NSString *const MJPropertyTypeId = @"@";
设置完后,就可以进行提取类型了.
- (instancetype)initWithTypeString:(NSString *)string
{
if (self = [super init])
{
NSUInteger loc = 1;
NSUInteger len = [string rangeOfString:@","].location - loc;
NSString *typeCode = [string substringWithRange:NSMakeRange(loc, len)];
[self getTypeCode:typeCode];
NSLog(@"%@",typeCode);
}
return self;
}
- (void)getTypeCode:(NSString *)code
{
if ([code isEqualToString:MJPropertyTypeId]) {
_idType = YES;
} else if (code.length > 3 && [code hasPrefix:@"@\""]) {
// 去掉@"和",截取中间的类型名称
_code = [code substringWithRange:NSMakeRange(2, code.length - 3)];
_typeClass = NSClassFromString(_code);
_numberType = (_typeClass == [NSNumber class] || [_typeClass isSubclassOfClass:[NSNumber class]]);
}
// 是否为数字类型
NSString *lowerCode = _code.lowercaseString;
NSArray *numberTypes = @[MJPropertyTypeInt, MJPropertyTypeShort, MJPropertyTypeBOOL1, MJPropertyTypeBOOL2, MJPropertyTypeFloat, MJPropertyTypeDouble, MJPropertyTypeLong, MJPropertyTypeChar];
if ([numberTypes containsObject:lowerCode]) {
_numberType = YES;
if ([lowerCode isEqualToString:MJPropertyTypeBOOL1]
|| [lowerCode isEqualToString:MJPropertyTypeBOOL2]) {
_boolType = YES;
}
}
}
至此,一个MJProperty
的骨架就大致搭好了.
当想要使用字典转模型的功能时,提供一个类方法方便转换,该方法放在NSObject+keyValue2object
分类中,该分类负责字典转模型的方法实现.
@implementation NSObject (keyValue2object)
+ (instancetype)objectWithKeyValues:(id)keyValues{
if (!keyValues) return nil;
return [[[self alloc] init] setKeyValues:keyValues];
}
- (instancetype)setKeyValues:(id)keyValues{
NSArray *propertiesArray = [self.class properties];
for (MJProperty *property in propertiesArray) {
MJPropertyType *type = property.type;
Class typeClass = type.typeClass;
if (type.isBoolType) {
NSLog(@"bool");
}else if (type.isIdType){
NSLog(@"ID");
}else if (type.isNumberType){
NSLog(@"Number");
}else{
NSLog(@"%@",typeClass);
}
}
return self;
}
@end
打印结果:
然后进行下一步----2.用该属性名作为键去字典中寻找对应的值.
id value = [keyValues valueForKey:property.name];
if (!value) continue;
接下来是第三步:3.拿到值后将值的类型转换为属性对应的数据类型.
首先处理数字类型,如果模型的属性是数字类型,即type.isNumberType == YES
.如果字典中的值是字符串类型的,需要将其转成NSNumber
类型.如果本来就是基本数据类型,则不用进行任何转换.
if (type.isNumberType){
// 字符串->数字
if ([value isKindOfClass:[NSString class]])
value = [[[NSNumberFormatter alloc]init] numberFromString:value];
}
其中有一种情况,是需要进行特殊处理的.当模型的属性是char
类型或者bool
类型时,获取到的编码都为c
,并且bool
还有可能是B
编码,它们都对应_boolType
.因为数字类型包含布尔类型,所以bool
类型要在数字类型的条件下进行额外判断.
if (type.isNumberType){
NSString *oldValue = value;
// 字符串->数字
if ([value isKindOfClass:[NSString class]]){
value = [[[NSNumberFormatter alloc] init] numberFromString:value];
if (type.isBoolType) {
NSString *lower = [oldValue lowercaseString];
if ([lower isEqualToString:@"yes"] || [lower isEqualToString:@"true"] ) {
value = @YES;
} else if ([lower isEqualToString:@"no"] || [lower isEqualToString:@"false"]) {
value = @NO;
}
}
}
}
然后处理其他类型转成字符串类型的情况.
else{
if (typeClass == [NSString class]) {
if ([value isKindOfClass:[NSNumber class]]) {
if (type.isNumberType)
// NSNumber -> NSString
value = [value description];
}else if ([value isKindOfClass:[NSURL class]]){
// NSURL -> NSString
value = [value absoluteString];
}
}
}
最后,进行赋值.
[self setValue:value forKey:property.name];
最简单的字典转模型大致完成了,当然,还有很多细节没有完善,但细节总是随着需求的不断变化而不断增加的.
原文链接:http://www.jianshu.com/p/d2ecef03f19e