演示代码地址:https://github.com/codeWillwillCode/LearnMJExtensioncss
首先,从最简单的字典开始.html
NSDictionary *dict = @{ @"name" : @"Jack", @"icon" : @"lufy.png", @"age" : @"20", @"height" : @1.55, @"money" : @"100.9", @"sex" : @(SexFemale), @"gay" : @"1" }
目标是拿到字典里的值(value)
对User
模型进行赋值.模型的属性名
对应字典的键(key)
.ios
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
最直接的方法是:git
User *user = [[User alloc] init]; user.name = dict[@"name"]; user.icon = dict[@"icon"]; ....
假如属性数量一多,人工手写大量样板代码
将耗费大量时间和精力,毫无心义.github
若是要写一个框架自动帮咱们转模型出来,大体思路以下:json
1.遍历模型中的属性
,而后拿到属性名
做为键值
去字典中寻找值
.swift
2.找到值
后根据模型的属性
的类型
将值
转成正确的类型
数组
3.赋值缓存
首先进行第一步:ruby
遍历模型中的
属性
,而后拿到属性名
做为键值
去字典中寻找值
.
方法伪代码:
[模型类 遍历属性的方法];
为了方便使用,建立一个叫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
的骨架就大体搭好了.
该部分源码请看项目实例代码中的<MJProperty的构建>
13F73F26-1195-43BC-BC98-FF2641B7DA58.png
当想要使用字典转模型的功能时,提供一个类方法方便转换,该方法放在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];
最简单的字典转模型大体完成了,固然,还有不少细节没有完善,但细节老是随着需求的不断变化而不断增长的.
该部分源码请看项目实例代码中的<简单的字典转模型>
定义一个JSON字符串转成模型:
/** * JSON字符串 -> 模型 */ void keyValues2object1(){ // 1.定义一个JSON字符串 NSString *jsonString = @"{\"name\":\"Jack\", \"icon\":\"lufy.png\", \"age\":20}"; // 2.将JSON字符串转为User模型 User *user = [User objectWithKeyValues:jsonString]; // 3.打印User模型的属性 NSLog(@"name=%@, icon=%@, age=%d", user.name, user.icon, user.age);}
这时程序会崩溃,由于没有对程序原来只对字典类型做处理:
// 若是是字符串,到这行就崩了id value = [keyValues valueForKey:property.name];
因此在这以前须要将JSON转成Foundation
框架中的对象,苹果提供了强大的NSJSONSerialization
.利用它,在刚开始传入字典/JSON字符串的时候将其进行转换.
- (instancetype)setKeyValues:(id)keyValues{ keyValues = [keyValues JSONObject]; ...... }
该方法的具体实现以下,若是是NSString
,就要先转成NSData
再进行序列化.
- (id)JSONObject{ id foundationObj; if ([self isKindOfClass:[NSString class]]) { foundationObj = [NSJSONSerialization JSONObjectWithData:[(NSString *)self dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil]; }else if ([self isKindOfClass:[NSData class]]){ foundationObj = [NSJSONSerialization JSONObjectWithData:(NSData *)self options:kNilOptions error:nil]; } return foundationObj?:self; }
该部分源码请看项目实例代码中的<JSON转模型>
定义一个模型中包含模型的复杂字典:
NSDictionary *dict = @{ @"text" : @"是啊,今每天气确实不错!", @"user" : @{ @"name" : @"Jack", @"icon" : @"lufy.png" }, @"retweetedStatus" : @{ @"text" : @"今每天气真不错!", @"user" : @{ @"name" : @"Rose", @"icon" : @"nami.png" } } };
对待这种字典的思路,应该想到递归,当碰到模型中的属性类型是一个模型类时,将字典中的值(Value)
做为字典处理.而后再调用字典转模型的方法返回一个模型类.因此在包装类型时还要有个属性表示它是不是自定义的模型类,才能做为依据继续递归.判断的方法是看它是不是来自于Foundation框架
的类.
/** 类型是否来自于Foundation框架,好比NSString、NSArray */@property (nonatomic, readonly, getter = isFromFoundation) BOOL fromFoundation;
在提取类型的方法中添加这样一条:
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]]); // 判断是不是模型类 _fromFoundation = [NSObject isClassFromFoundation:_typeClass]; }
怎么判断是否来自Foundation框架
呢? 下图展现了Foundation框架(NSObject部分)
下的类结构.
用一个NSSet
(比用NSArray
检索效率更高),返回一些经常使用基本的Foundation框架
下继承自NSObject
的类.
static NSSet *foundationClasses_; + (NSSet *)foundationClasses { if (foundationClasses_ == nil) { foundationClasses_ = [NSSet setWithObjects: [NSURL class], [NSDate class], [NSValue class], [NSData class], [NSArray class], [NSDictionary class], [NSString class], [NSAttributedString class], nil]; } return foundationClasses_; }
具体isClassFromFoundation
的逻辑由类方法实现,在上面的集合中遍历.因为几乎全部类都是继承自NSObject
,因此NSObject
不能写入上面的集合当中,须要额外判断:
+ (BOOL)isClassFromFoundation:(Class)c{ if (c == [NSObject class]) return YES; __block BOOL result = NO; [[self foundationClasses] enumerateObjectsUsingBlock:^(Class foundationClass, BOOL *stop) { if ([c isSubclassOfClass:foundationClass]) { result = YES; *stop = YES; } }]; return result; }
获得结果后,须要在setKeyValues:keyValues
这一核心方法中添加是否为模型类的判断:
// 若是不是来自foundation框架的类而且不是基本数据类型 ,则递归 if (!type.isFromFoundation && typeClass) { value = [typeClass objectWithKeyValues:value]; }
该部分源码请看项目实例代码中的<复杂字典转模型>
稍复杂的一种状况是字典里装有数组的状况.
NSDictionary *dict = @{ @"statuses" : @[ @{ @"text" : @"今每天气真不错!", @"user" : @{ @"name" : @"Rose", @"icon" : @"nami.png" } }, @{ @"text" : @"明天去旅游了", @"user" : @{ @"name" : @"Jack", @"icon" : @"lufy.png" } } ], @"ads" : @[ @{ @"image" : @"ad01.png", @"url" : @"http://www.小码哥ad01.com" }, @{ @"image" : @"ad02.png", @"url" : @"http://www.小码哥ad02.com" } ], @"totalNumber" : @"2014", @"previousCursor" : @"13476589", @"nextCursor" : @"13476599" };
上面定义了一个字典,模型StatusResult
有两个数组属性.
@interface StatusResult : BaseObject/** 存放着某一页微博数据(里面都是Status模型) */@property (strong, nonatomic) NSMutableArray *statuses;/** 存放着一堆的广告数据(里面都是Ad模型) */@property (strong, nonatomic) NSArray *ads;/** 总数 */@property (strong, nonatomic) NSNumber *totalNumber;/** 上一页的游标 */@property (assign, nonatomic) long long previousCursor;/** 下一页的游标 */@property (assign, nonatomic) long long nextCursor;@end
对于一个数组来讲,你必需要告诉方法里面装的是什么模型,才能将字典中值为数组的成员转成模型.
在MJExtension
中,提供了两种方式进行处理.
方式一,调用NSObject
分类中得类方法:
[StatusResult setupObjectClassInArray:^NSDictionary *{ return @{ @"statuses" : @"Status", // 或者 @"statuses" : [Status class], @"ads" : @"Ad" // 或者 @"ads" : [Ad class] }; }];
方式二,在模型的.m
文件中实现方法供回调:
+ (NSDictionary *)objectClassInArray { return @{ @"statuses" : @"Status", // 或者 @"statuses" : [Status class], @"ads" : @"Ad" // 或者 @"ads" : [Ad class] }; }
原理上都差很少,都是经过代码进行回调,这个主要实现方式二.
在分类中声明一个protocol
提供接口供模型类调用.
@protocol MJKeyValue <NSObject>+ (NSDictionary *) objectClassInArray;@end
在转换的代码中设置添加设置数组模型的方法:
if (!type.isFromFoundation && typeClass) { value = [typeClass objectWithKeyValues:value]; }// 看该类是否实现了objectClassInArray方法else if ([self.class respondsToSelector:@selector(objectClassInArray)]){ id objectClass; // 若是是class类型,例如@"statuses" : [Status class] objectClass = [self.class objectClassInArray][property.name]; // 若是是NSString类型,例如@"statuses" : @"Status" if ([objectClass isKindOfClass:[NSString class]]) { objectClass = NSClassFromString(objectClass); } // 若是有值 if (objectClass) { // 返回一个装了模型的数组 value = [objectClass objectArrayWithKeyValuesArray:value]; } }
这时返回的值固然是个装满模型的数组模型.思路也很简单,对数组里的每个成员都进行字典转模型的方法.若是其中的成员不是自定义模型类,那么直接返回.
+ (NSMutableArray *)objectArrayWithKeyValuesArray:(id)keyValuesArray{ if ([self isClassFromFoundation:self]) return keyValuesArray; // 若是是json字符串,转成字典 keyValuesArray = [keyValuesArray JSONObject]; NSMutableArray *modelArray = [NSMutableArray array]; // 遍历 for (NSDictionary *keyValues in keyValuesArray) { // 对其中的模型调用字典转模型方法,并添加到数组中返回 id model; model = [self objectWithKeyValues:keyValues]; if (model) { [modelArray addObject:model]; } } return modelArray; }
该部分源码请看项目实例代码中的<字典数组转模型>
@interface IDAndDescription : NSObject@property (nonatomic, copy) NSString *ID;@property (nonatomic, copy) NSString *Description;@end
实际开发中,服务器一般返回一个字段名为id
,或者description
的JSON
数据,而这两个名字在OC中有特殊含义,如上所示,在定义属性的时候并不能使用这类名称.这时属性名与字典key再也不是直接对应的关系,须要加入一层转换.
源码中key的替换也有几种方式选择,这里实现replacedKeyFromPropertyName
这一方式.
过程是在要替换key的模型类中实现replacedKeyFromPropertyName
方法,返回一个原始key和改名的key对应的字典.replacedKeyFromPropertyName
在protocol
中声明.
实际上,也就是建立了一个方法来获取属性名与字典key的对应关系.
在模型类中实现接口中的方法告知对应关系.
@implementation IDAndDescription+ (NSDictionary *)replacedKeyFromPropertyName{ return @{ @"ID" : @"id", @"Description" : @"description" }; }@end
该方法从字典中需找要替换的key,参数是property
的名字.若是字典中找不到对应的属性名,则不须要进行转换.
+ (NSString *)propertyKey:(NSString *)propertyName{ NSString *key; if ([self respondsToSelector:@selector(replacedKeyFromPropertyName)]) { key = [self replacedKeyFromPropertyName][propertyName]; } return key?:propertyName; }
在获取值(value)
的时候,要将key替换成对应的key.
id value = [keyValues valueForKey:[self.class propertyKey:property.name]];if (!value) continue;
转换完成.
将5个字典转模型的例子同时进行运行,在+ properties
方法中添加一句打印.另外以前的例子都是有内存泄露的,这里添加了free(properties)
修复了这个问题.
+ (NSArray *)properties{ NSLog(@"%@调用了properties方法",[self class]); NSMutableArray *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]; MJProperty *propertyObj = [MJProperty propertyWithProperty:property]; [propertiesArray addObject:propertyObj]; } free(properties); return propertiesArray; }
输出台输出以下:
能够看到,不少的类都不止一次调用了获取属性的方法,对于一个类来讲,要获取它的所有属性,只要获取一次就够了.获取到后将结果缓存起来,下次就没必要进行没必要要的计算.
注意:因为我写文章时手上的这份源码相对较早,缓存属性列表是经过一个全局字典来缓存的,而在最新版本的
MJExtension
中,已经换成了关联对象来实现.因为实现思路大体都是同样,而且效果相同,因此这里并不纠结用哪一种方式.
// 设置一个全局字典用来将类的属性都缓存起来static NSMutableDictionary *cachedProperties_; + (void)load { cachedProperties_ = [NSMutableDictionary dictionary]; }
将方法改写为:
+ (NSArray *)properties { NSMutableArray *cachedProperties = cachedProperties_[NSStringFromClass(self)]; if (!cachedProperties) { NSLog(@"%@调用了properties方法",[self class]); cachedProperties = [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]; MJProperty *propertyObj = [MJProperty propertyWithProperty:property]; [cachedProperties addObject:propertyObj]; } free(properties); cachedProperties_[NSStringFromClass(self)] = cachedProperties; } return cachedProperties; }
此时控制台输出:
能够看每一个类只通过一次获取所有属性.
除了缓存属性外,提取类型编码的过程也能够进一步缓存优化性能.
在下面的方法中加上一句打印:
- (void)getTypeCode:(NSString *)code { NSLog(@"%@",code); ...... }
控制台输出:
能够看到一些经常使用的类型例如NSString
屡次调用了该方法.提取类型时,只要知道类名(在这里也就是typeCode
),一个MJPropertyType
就已经能够肯定了.
重写了- initWithTypeString:
方法:
static NSMutableDictionary *cachedTypes_; + (void)load { cachedTypes_ = [NSMutableDictionary dictionary]; } + (instancetype)propertyTypeWithAttributeString:(NSString *)string{ return [[MJPropertyType alloc] initWithTypeString:string]; } - (instancetype)initWithTypeString:(NSString *)string { NSUInteger loc = 1; NSUInteger len = [string rangeOfString:@","].location - loc; NSString *typeCode = [string substringWithRange:NSMakeRange(loc, len)]; if (!cachedTypes_[typeCode]) { NSLog(@"%@",typeCode); self = [super init]; [self getTypeCode:typeCode]; cachedTypes_[typeCode] = self; } return self; }
输出结果:
该部分源码请看项目实例代码中的<key的替换与性能优化>