Objective-C Runtime 学习(三)

来源:ian(@ianisme)
链接:http://www.ianisme.com/ios/2019.html

本片包括运用runtime机制实现对类的持久化,类的数据转化,以及动态给类增加方法等内容。更多内容可以参考前两篇的基础内容。

类的归档篇

苍老师的资料总要整理一下吧!

创建People.h

1
2
3
4
5
6
7
8
9
10
#import <Foundation/Foundation.h>
@interface People : NSObject <NSCoding>
@property (nonatomic, copy) NSString *name; // 姓名
@property (nonatomic, strong) NSNumber *age; // 年龄
@property (nonatomic, copy) NSString *occupation; // 职业
@property (nonatomic, copy) NSString *nationality; // 国籍
@end

People.m

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
32
33
34
35
36
37
38
39
40
41
42
#import "People.h"
#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif
@implementation People
- (void)encodeWithCoder:(NSCoder *)aCoder {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([People class], &count);
for (NSUInteger i = 0; i < count; i ++) {
Ivar ivar = ivars[i];
const char *name = ivar_getName(ivar);
NSString *key = [NSString stringWithUTF8String:name];
id value = [self valueForKey:key];
[aCoder encodeObject:value forKey:key];
}
free(ivars);
}
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([People class], &count);
for (NSUInteger i = 0; i < count; i ++) {
Ivar ivar = ivars[i];
const char *name = ivar_getName(ivar);
NSString *key = [NSString stringWithUTF8String:name];
id value = [aDecoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
free(ivars);
}
return self;
}
@end

类的转换篇

服务器返回了大量苍老师的数据,手机端这边接收后如何去转换呢?当然是要将JSON转换为Model啦!

相信平时你们的项目中也用到过这些三方库,下面我们去了解下runtime实现JSON和Model互转。

创建People.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#import <Foundation/Foundation.h>
@interface People : NSObject
@property (nonatomic, copy) NSString *name; // 姓名
@property (nonatomic, strong) NSNumber *age; // 年龄
@property (nonatomic, copy) NSString *occupation; // 职业
@property (nonatomic, copy) NSString *nationality; // 国籍
// 生成model
- (instancetype)initWithDictionary:(NSDictionary *)dictionary;
// 转换成字典
- (NSDictionary *)covertToDictionary;
@end

People.m的代码如下:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#import "People.h"
#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif
@implementation People
- (instancetype)initWithDictionary:(NSDictionary *)dictionary
{
self = [super init];
if (self) {
for (NSString *key in dictionary.allKeys) {
id value = dictionary[key];
SEL setter = [self propertySetterByKey:key];
if (setter) {
// 这里还可以使用NSInvocation或者method_invoke,不再继续深究了,有兴趣google。
((void (*)(id, SEL, id))objc_msgSend)(self, setter, value);
}
}
}
return self;
}
- (NSDictionary *)covertToDictionary
{
unsigned int count = 0;
objc_property_t *properties = class_copyPropertyList([self class], &count);
if (count != 0) {
NSMutableDictionary *resultDict = [<a href="http://www.jobbole.com/members/www821839432">@{}</a> mutableCopy];
for (NSUInteger i = 0; i < count; i ++) {
const void *propertyName = property_getName(properties[i]);
NSString *name = [NSString stringWithUTF8String:propertyName];
SEL getter = [self propertyGetterByKey:name];
if (getter) {
id value = ((id (*)(id, SEL))objc_msgSend)(self, getter);
if (value) {
resultDict[name] = value;
} else {
resultDict[name] = @"字典的key对应的value不能为nil哦!";
}
}
}
free(properties);
return resultDict;
}
free(properties);
return nil;
}
#pragma mark - private methods
// 生成setter方法
- (SEL)propertySetterByKey:(NSString *)key
{
// 首字母大写,你懂得
NSString *propertySetterName = [NSString stringWithFormat:@"set%@:", key.capitalizedString];
SEL setter = NSSelectorFromString(propertySetterName);
if ([self respondsToSelector:setter]) {
return setter;
}
return nil;
}
// 生成getter方法
- (SEL)propertyGetterByKey:(NSString *)key
{
SEL getter = NSSelectorFromString(key);
if ([self respondsToSelector:getter]) {
return getter;
}
return nil;
}
@end

main.m中运行以下代码:

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
32
#import <Foundation/Foundation.h>
#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif
#import "People.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSDictionary *dict = @{
@"name" : @"苍井空",
@"age" : @18,
@"occupation" : @"老师",
@"nationality" : @"日本"
};
// 字典转模型
People *cangTeacher = [[People alloc] initWithDictionary:dict];
NSLog(@"热烈欢迎,从%@远道而来的%@岁的%@%@",cangTeacher.nationality,cangTeacher.age,cangTeacher.name,cangTeacher.occupation);
// 模型转字典
NSDictionary *covertedDict = [cangTeacher covertToDictionary];
NSLog(@"%@",covertedDict);
}
return 0;
}

最后输出内容如下:
这里写图片描述

相信通过前面的学习,这些代码不用写过多的注释你也可以看懂了,我把假设是网络返回的苍老师的资料转化为了model,然后又将model转回字典。这是一个JSON转Model相互转换的一个思路,大家稍后运行Demo细细品味。

动态增加类的方法篇

这个实例主要是验证一下上文《5.2 消息动态解析》

第一首:

添加sing实例方法,但是不提供方法的实现。验证当找不到方法的实现时,动态添加方法。
创建People.h

1
2
3
4
5
6
7
8
9
#import <Foundation/Foundation.h>
@interface People : NSObject
@property (nonatomic, copy) NSString *name;
- (void)sing;
@end

创建People.m

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
#import "People.h"
#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif
@implementation People
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
// 我们没有给People类声明sing方法,我们这里动态添加方法
if ([NSStringFromSelector(sel) isEqualToString:@"sing"]) {
class_addMethod(self, sel, (IMP)otherSing, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void otherSing(id self, SEL cmd)
{
NSLog(@"%@ 唱歌啦!",((People *)self).name);
}

在main.m中运行以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#import <Foundation/Foundation.h>
#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif
#import "People.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
People *cangTeacher = [[People alloc] init];
cangTeacher.name = @"苍老师";
[cangTeacher sing];
}
return 0;
}

结果如下:
这里写图片描述

我们没有提供苍老师唱歌的方法实现,因此在调用此方法的时候,会调用resolveInstanceMethod方法,我们动态添加了方法。我们也可以返回No,继续向下传递。(此处请返回《5.2 消息动态解析》第一步品味下)

第二首

外面的小鸟在唱歌,但是苍老师的歌声盖过了小鸟,我们只能听到苍老师唱歌了。
这里我们不声明sing方法,将调用途中动态更换调用对象。
在第一首代码的基础上,创建Bird的model
Bird.h

1
2
3
4
5
6
7
#import <Foundation/Foundation.h>
@interface Bird : NSObject
@property (nonatomic, copy) NSString *name;
@end

Bird.m

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
32
33
34
35
36
37
#import "Bird.h"
#import "People.h"
@implementation Bird
// 第一步:我们不动态添加方法,返回NO,进入第二步;
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
return NO;
}
// 第二部:我们不指定备选对象响应aSelector,进入第三步;
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return nil;
}
// 第三步:返回方法选择器,然后进入第四部;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if ([NSStringFromSelector(aSelector) isEqualToString:@"sing"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
// 第四部:这步我们修改调用对象
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
// 我们改变调用对象为People
People *cangTeacher = [[People alloc] init];
cangTeacher.name = @"苍老师";
[anInvocation invokeWithTarget:cangTeacher];
}
@end

main.m运行代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#import <Foundation/Foundation.h>
#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif
#import "People.h"
#import "Bird.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Bird *bird = [[Bird alloc] init];
bird.name = @"小小鸟";
((void (*)(id, SEL))objc_msgSend)((id)bird, @selector(sing));
}
return 0;
}

运行结果如下:
这里写图片描述

成功更换了对象,把对象更换为苍老师了。(此处请返回《5.2 消息动态解析》品味)

第三首

苍老师不想唱歌想跳舞了。
这里我是实现不提供声明,不修改调用对象,但是将sing方法修改为dance方法。
创建People.h

1
2
3
4
5
#import <Foundation/Foundation.h>
@interface People : NSObject
@end

People.m

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#import "People.h"
#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif
@implementation People
// 第一步:我们不动态添加方法,返回NO,进入第二步;
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
return NO;
}
// 第二部:我们不指定备选对象响应aSelector,进入第三步;
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return nil;
}
// 第三步:返回方法选择器,然后进入第四部;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if ([NSStringFromSelector(aSelector) isEqualToString:@"sing"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
// 第四部:这步我们修改调用方法
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
[anInvocation setSelector:@selector(dance)];
// 这还要指定是哪个对象的方法
[anInvocation invokeWithTarget:self];
}
// 若forwardInvocation没有实现,则会调用此方法
- (void)doesNotRecognizeSelector:(SEL)aSelector
{
NSLog(@"消息无法处理:%@", NSStringFromSelector(aSelector));
}
- (void)dance
{
NSLog(@"跳舞!!!come on!");
}
@end

在main.m中运行如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#import <Foundation/Foundation.h>
#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif
#import "People.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
People *cangTeacher = [[People alloc] init];
((void(*)(id, SEL)) objc_msgSend)((id)cangTeacher, @selector(sing));
}
return 0;
}

结果如图:
这里写图片描述

成功更换了方法,苍老师由唱歌改为跳舞了(此处请返回《5.2 消息动态解析》品味)

总结

好吧,我承认我骗了你,当你读到这里你肯定花了不止1小时。都是我的错,不是因为你笨,之所以说1小时是为了让你有信心,有耐心继续下去。读到这里恭喜你已经在iOS开发的道路上又向前了一步!同时我也要感谢以下参考文献以及文章,是他们让我更好的理解了runtime,再次表示感谢!这篇文章断断续续写了将近一周的时间,您可以读到这里就是对我最大的鼓励,谢谢!