前言
在開發(fā)中必不可少的模型與字典互轉(zhuǎn)喘先,但是一直以來都是使用他人的庫(kù),從來沒有研究其原理或者說深究其所以然⊥⒘#現(xiàn)在窘拯,在這里我們一起來學(xué)習(xí)通過runtime
完成模型與字典的互轉(zhuǎn)。
聲明Model
在開始介紹詳細(xì)API之前坝茎,我們先來聲明一個(gè)模型類TestModel
涤姊,這個(gè)類提供了根據(jù)字典轉(zhuǎn)換成模型類對(duì)象的功能,還提供了將模型類轉(zhuǎn)換成字典的功能:
//
// TestModel.h
// RuntimeDemo
//
// Copyright ? 2017年 . All rights reserved.
//
#import <Foundation/Foundation.h>
@protocol EmptyPropertyProperty <NSObject>
// 設(shè)置默認(rèn)值嗤放,若為空思喊,則取出來的就是默認(rèn)值
- (NSDictionary *)defaultValueForEmptyProperty;
@end
@interface TestModel : NSObject <EmptyPropertyProperty>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, strong) NSNumber *count;
@property (nonatomic, assign) int commentCount;
@property (nonatomic, strong) NSArray *summaries;
@property (nonatomic, strong) NSDictionary *parameters;
@property (nonatomic, strong) NSSet *results;
@property (nonatomic, strong) TestModel *testModel;
// 只讀屬性
@property (nonatomic, assign, readonly) NSString *classVersion;
// 通過這個(gè)方法來實(shí)現(xiàn)自動(dòng)生成model
- (instancetype)initWithDictionary:(NSDictionary *)dictionary;
// 轉(zhuǎn)換成字典
- (NSDictionary *)toDictionary;
// 測(cè)試
+ (void)test;
@end
我們這里聲明了幾種類型,主要是模型中的數(shù)組次酌、字典酗钞、集合娇澎、對(duì)象屬性,我們最后要轉(zhuǎn)換對(duì)象成字典。
實(shí)現(xiàn)代碼
在我們講解之前骇扇,先把代碼全部放出來档押,我們?cè)僖灰恢v解:
#import "TestModel.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation TestModel
- (instancetype)initWithDictionary:(NSDictionary *)dictionary {
if (self = [super init]) {
for (NSString *key in dictionary.allKeys) {
id value = [dictionary objectForKey:key];
if ([key isEqualToString:@"testModel"]) {
TestModel *testModel = [[TestModel alloc] initWithDictionary:value];
value = testModel;
self.testModel = testModel;
continue;
}
SEL setter = [self propertySetterWithKey:key];
if (setter != nil) {
((void (*)(id, SEL, id))objc_msgSend)(self, setter, value);
}
}
}
return self;
}
- (NSDictionary *)toDictionary {
unsigned int outCount = 0;
objc_property_t *properties = class_copyPropertyList([self class], &outCount);
if (outCount != 0) {
NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithCapacity:outCount];
for (unsigned int i = 0; i < outCount; ++i) {
objc_property_t property = properties[i];
const void *propertyName = property_getName(property);
NSString *key = [NSString stringWithUTF8String:propertyName];
// 繼承于NSObject的類都會(huì)有這幾個(gè)在NSObject中的屬性
if ([key isEqualToString:@"description"]
|| [key isEqualToString:@"debugDescription"]
|| [key isEqualToString:@"hash"]
|| [key isEqualToString:@"superclass"]) {
continue;
}
// 我們只是測(cè)試窖维,不做通用封裝啦吧,因此這里不額外寫方法做通用處理,只是寫死測(cè)試一下效果
if ([key isEqualToString:@"testModel"]) {
if ([self respondsToSelector:@selector(toDictionary)]) {
id testModel = [self.testModel toDictionary];
if (testModel != nil) {
[dict setObject:testModel forKey:key];
}
continue;
}
}
SEL getter = [self propertyGetterWithKey:key];
if (getter != nil) {
// 獲取方法的簽名
NSMethodSignature *signature = [self methodSignatureForSelector:getter];
// 根據(jù)方法簽名獲取NSInvocation對(duì)象
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
// 設(shè)置target
[invocation setTarget:self];
// 設(shè)置selector
[invocation setSelector:getter];
// 方法調(diào)用
[invocation invoke];
// 接收返回的值
__unsafe_unretained NSObject *propertyValue = nil;
[invocation getReturnValue:&propertyValue];
// id propertyValue = [self performSelector:getter];
if (propertyValue == nil) {
if ([self respondsToSelector:@selector(defaultValueForEmptyProperty)]) {
NSDictionary *defaultValueDict = [self defaultValueForEmptyProperty];
id defaultValue = [defaultValueDict objectForKey:key];
propertyValue = defaultValue;
}
}
if (propertyValue != nil) {
[dict setObject:propertyValue forKey:key];
}
}
}
free(properties);
return dict;
}
free(properties);
return nil;
}
- (SEL)propertyGetterWithKey:(NSString *)key {
if (key != nil) {
SEL getter = NSSelectorFromString(key);
if ([self respondsToSelector:getter]) {
return getter;
}
}
return nil;
}
- (SEL)propertySetterWithKey:(NSString *)key {
NSString *propertySetter = key.capitalizedString;
propertySetter = [NSString stringWithFormat:@"set%@:", propertySetter];
// 生成setter方法
SEL setter = NSSelectorFromString(propertySetter);
if ([self respondsToSelector:setter]) {
return setter;
}
return nil;
}
#pragma mark - EmptyPropertyProperty
- (NSDictionary *)defaultValueForEmptyProperty {
return @{@"name" : [NSNull null],
@"title" : [NSNull null],
@"count" : @(1),
@"commentCount" : @(1),
@"classVersion" : @"0.0.1"};
}
+ (void)test {
NSMutableSet *set = [NSMutableSet setWithArray:@[@"可變集合", @"字典->不可變集合->可變集合"]];
NSDictionary *dict = @{@"name" : @"技術(shù)博客",
@"title" : @"http://www.henishuo.com",
@"count" : @(11),
@"results" : [NSSet setWithObjects:@"集合值1", @"集合值2", set , nil],
@"summaries" : @[@"sm1", @"sm2", @{@"keysm": @{@"stkey": @"字典->數(shù)組->字典->字典"}}],
@"parameters" : @{@"key1" : @"value1", @"key2": @{@"key11" : @"value11", @"key12" : @[@"三層", @"字典->字典->數(shù)組"]}},
@"classVersion" : @(1.1),
@"testModel" : @{@"name" : @"技術(shù)博客",
@"title" : @"http://www.henishuo.com",
@"count" : @(11),
@"results" : [NSSet setWithObjects:@"集合值1", @"集合值2", set , nil],
@"summaries" : @[@"sm1", @"sm2", @{@"keysm": @{@"stkey": @"字典->數(shù)組->字典->字典"}}],
@"parameters" : @{@"key1" : @"value1", @"key2": @{@"key11" : @"value11", @"key12" : @[@"三層", @"字典->字典->數(shù)組"]}},
@"classVersion" : @(1.1)}};
TestModel *model = [[TestModel alloc] initWithDictionary:dict];
NSLog(@"%@", model);
NSLog(@"model->dict: %@", [model toDictionary]);
}
@end
細(xì)節(jié)講解
注意到這一行代碼了嗎璃搜?這是將對(duì)應(yīng)的值賦值給對(duì)應(yīng)的屬性:
((void (*)(id, SEL, id))objc_msgSend)(self, setter, value)
我們需要明確一點(diǎn)拖吼,objc_msgSend
函數(shù)是用于發(fā)送消息的,而這個(gè)函數(shù)是可以很多個(gè)參數(shù)的这吻,但是我們必須手動(dòng)轉(zhuǎn)換成對(duì)應(yīng)類型參數(shù)吊档,比如上面我們就是強(qiáng)制轉(zhuǎn)換objc_msgSend
函數(shù)類型為帶三個(gè)參數(shù)且返回值為void
函數(shù),然后才能傳三個(gè)參數(shù)橘原。如果我們直接通過objc_msgSend(self, setter, value)
是報(bào)錯(cuò)籍铁,說參數(shù)過多。
(void (*)(id, SEL, id)
這是C
語言中的函數(shù)指針趾断,如果不了解C
拒名,沒有關(guān)系,我們只需要記住參數(shù)列表前面是一個(gè)(*)
這樣的就是對(duì)應(yīng)函數(shù)指針了芋酌。
生成Setter方法
我們要通過objc_msgSend
函數(shù)來發(fā)送消息給對(duì)象增显,然后通過屬性的setter
方法來賦值,那么我們要生成setter
選擇器:
- (SEL)propertySetterWithKey:(NSString *)key {
NSString *propertySetter = key.capitalizedString;
propertySetter = [NSString stringWithFormat:@"set%@:", propertySetter];
// 生成setter方法
SEL setter = NSSelectorFromString(propertySetter);
if ([self respondsToSelector:setter]) {
return setter;
}
return nil;
}
這里就是生成屬性的setter
選擇器。我們知道同云,系統(tǒng)生成屬性的setter
方法的規(guī)范是setKey:
這樣的格式糖权,因此我們只要按照同樣的規(guī)則生成setter
就可以了。另外我們還需要判斷是否可以響應(yīng)此setter
方法炸站。
模型中有模型屬性
對(duì)于本demo中星澳,模型中還有模型屬性,那么我們應(yīng)該如何來賦值呢旱易?其實(shí)禁偎,我們需要注意一點(diǎn),一定要給模型屬性分配內(nèi)存阀坏,否則看起來賦值了如暖,但是對(duì)象還是空。
if ([key isEqualToString:@"testModel"]) {
TestModel *testModel = [[TestModel alloc] initWithDictionary:value];
value = testModel;
self.testModel = testModel;
continue;
}
這里是在for
循環(huán)中的忌堂,我們一定要注意加上continue
盒至,否則下邊可能會(huì)將其值設(shè)置為空哦。
生成Getter方法
我們可以通過NSSelectorFromString
函數(shù)來生成SEL
選擇器士修,當(dāng)然我們也可以通過@selector()
生成SEL
選擇器枷遂,但是我們這里只能使用前者:
- (SEL)propertyGetterWithKey:(NSString *)key {
if (key != nil) {
SEL getter = NSSelectorFromString(key);
if ([self respondsToSelector:getter]) {
return getter;
}
}
return nil;
}
模型轉(zhuǎn)字典
首先,我們需要先獲取所有的屬性李命,以便獲取屬性值:
unsigned int outCount = 0;
objc_property_t *properties = class_copyPropertyList([self class], &outCount);
objc_property_t
類型代表屬性登淘,它是一個(gè)結(jié)構(gòu)體。通過函數(shù)class_copyPropertyList
可以獲取對(duì)象的屬性列表及屬性的個(gè)數(shù)封字。
在遍歷屬性列表時(shí),我們通過這樣獲取名稱:
objc_property_t property = properties[i];
const void *propertyName = property_getName(property);
NSString *key = [NSString stringWithUTF8String:propertyName];
由于繼承于NSObject
的對(duì)象耍鬓,都有這幾個(gè)屬性阔籽,因此我們需要過濾掉:
// 繼承于NSObject的類都會(huì)有這幾個(gè)在NSObject中的屬性
if ([key isEqualToString:@"description"]
|| [key isEqualToString:@"debugDescription"]
|| [key isEqualToString:@"hash"]
|| [key isEqualToString:@"superclass"]) {
continue;
}
調(diào)用getter方法獲取值
我們要通過runtime獲取值,常用的有兩種方式:
- 通過performSelector方法
- 通過NSInvocation對(duì)象
下面是通過NSInvocation
的方法牲蜀,流程可以這樣:先獲取方法簽名笆制,然后根據(jù)方法簽名生成NSInvocation
對(duì)象,設(shè)置target
涣达、SEL
在辆,然后調(diào)用,最后獲取返回值:
SEL getter = [self propertyGetterWithKey:key];
if (getter != nil) {
// 獲取方法的簽名
NSMethodSignature *signature = [self methodSignatureForSelector:getter];
// 根據(jù)方法簽名獲取NSInvocation對(duì)象
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
// 設(shè)置target
[invocation setTarget:self];
// 設(shè)置selector
[invocation setSelector:getter];
// 方法調(diào)用
[invocation invoke];
// 接收返回的值
__unsafe_unretained NSObject *propertyValue = nil;
[invocation getReturnValue:&propertyValue];
// id propertyValue = [self performSelector:getter];
}
如果是通過performSelector
方式度苔,一行代碼就可以了匆篓,但是會(huì)提示有內(nèi)存泄露的風(fēng)險(xiǎn),通常使用上面這種方式寇窑,而不是直接使用下面這種方式:
id propertyValue = [self performSelector:getter];
我們這里還做了額外的處理鸦概,當(dāng)屬性值獲取到是空的時(shí)候,我們可以通過協(xié)議指定默認(rèn)值甩骏。當(dāng)值為空時(shí)窗市,我們就會(huì)使用默認(rèn)值:
if (propertyValue == nil) {
if ([self respondsToSelector:@selector(defaultValueForEmptyProperty)]) {
NSDictionary *defaultValueDict = [self defaultValueForEmptyProperty];
id defaultValue = [defaultValueDict objectForKey:key];
propertyValue = defaultValue;
}
}
模型屬性轉(zhuǎn)換成字典
我們這里的模型有一個(gè)屬性也是一個(gè)模型先慷,因此我們需要額外處理一下:
// 我們只是測(cè)試,不做通用封裝咨察,因此這里不額外寫方法做通用處理论熙,只是寫死測(cè)試一下效果
if ([key isEqualToString:@"testModel"]) {
if ([self respondsToSelector:@selector(toDictionary)]) {
id testModel = [self.testModel toDictionary];
if (testModel != nil) {
[dict setObject:testModel forKey:key];
}
continue;
}
}
注意,這里是寫死的哦摄狱,因?yàn)槲覀冞@里寫死了testModel
脓诡,只是為了簡(jiǎn)化,如果要封裝成通用的方法二蓝,那么就需要做更多的工作了誉券。不過我們這里的目的是學(xué)習(xí)如何轉(zhuǎn),而不是封裝成能用的庫(kù)刊愚。因此踊跟,研究明白其原理才是目的。
測(cè)試
我們測(cè)試一下這樣復(fù)雜的結(jié)構(gòu):
+ (void)test {
NSMutableSet *set = [NSMutableSet setWithArray:@[@"可變集合", @"字典->不可變集合->可變集合"]];
NSDictionary *dict = @{@"name" : @"技術(shù)博客",
@"title" : @"http://www.henishuo.com",
@"count" : @(11),
@"results" : [NSSet setWithObjects:@"集合值1", @"集合值2", set , nil],
@"summaries" : @[@"sm1", @"sm2", @{@"keysm": @{@"stkey": @"字典->數(shù)組->字典->字典"}}],
@"parameters" : @{@"key1" : @"value1", @"key2": @{@"key11" : @"value11", @"key12" : @[@"三層", @"字典->字典->數(shù)組"]}},
@"classVersion" : @(1.1),
@"testModel" : @{@"name" : @"技術(shù)博客",
@"title" : @"http://www.henishuo.com",
@"count" : @(11),
@"results" : [NSSet setWithObjects:@"集合值1", @"集合值2", set , nil],
@"summaries" : @[@"sm1", @"sm2", @{@"keysm": @{@"stkey": @"字典->數(shù)組->字典->字典"}}],
@"parameters" : @{@"key1" : @"value1", @"key2": @{@"key11" : @"value11", @"key12" : @[@"三層", @"字典->字典->數(shù)組"]}},
@"classVersion" : @(1.1)}};
TestModel *model = [[TestModel alloc] initWithDictionary:dict];
NSLog(@"%@", model);
NSLog(@"model->dict: %@", [model toDictionary]);
}
打印效果
模型轉(zhuǎn)字典的效果:
2017-12-29 16:02:15.207 RuntimeDemo[40233:1083396] model->dict: {
classVersion = "0.0.1",
count = 11,
parameters = {
key1 = "value1",
key2 = {
key11 = "value11",
key12 = (
"三層",
"字典->字典->數(shù)組",
),
},
},
results = {(
"集合值2",
"集合值1",
{(
"可變集合",
"字典->不可變集合->可變集合",
)},
)},
title = "http://www.henishuo.com",
commentCount = 1,
testModel = {
classVersion = "0.0.1",
count = 11,
parameters = {
key1 = "value1",
key2 = {
key11 = "value11",
key12 = (
"三層",
"字典->字典->數(shù)組",
),
},
},
results = {(
"集合值2",
"集合值1",
{(
"可變集合",
"字典->不可變集合->可變集合",
)},
)},
title = "http://www.henishuo.com",
commentCount = 1,
name = "技術(shù)博客",
summaries = (
"sm1",
"sm2",
{
keysm = {
stkey = "字典->數(shù)組->字典->字典",
},
},
),
},
name = "技術(shù)博客",
summaries = (
"sm1",
"sm2",
{
keysm = {
stkey = "字典->數(shù)組->字典->字典",
},
},
),
}