Hello, World!

runtime(1)

字数统计: 3.4k阅读时长: 15 min
2019/10/28 Share

动态获取类对象信息

使用Objective-C语言提供的与runtime相关的函数,动态的获取类的所有信息包括成员变量、函数列表和协议列表等,如下所示。

先定义一个继承自NSObject的类CTMObject。

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
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@protocol CTMObjectDelegate1 <NSObject>

@end

@protocol CTMObjectDelegate2 <NSObject>

@end

@protocol CTMObjectDelegate4 <NSObject>

@end

@interface CTMObject : NSObject <CTMObjectDelegate1, CTMObjectDelegate2> {
NSNumber *_weight; // 成员变量 对象类型
int _age; // 成员变量 基本数据类型
}

@property (nonatomic, copy) NSString *name; // 属性 对象类型

@property (nonatomic, strong, readonly) NSNumber *height; // 只读属性 对象类型

// 类方法
+ (void)start:(NSString *)logo;

// 实例方法
- (NSString *)finish:(BOOL)isOK;

@end

NS_ASSUME_NONNULL_END
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
#import "CTMObject.h"

@protocol CTMObjectDelegate3 <NSObject>

@end

@interface CTMObject()<CTMObjectDelegate3> {
NSNumber *_privateWeight; // 私有成员变量 对象类型
int _privateAge; // 私有成员变量 基本数据类型
}

@property (nonatomic, copy) NSString *privateName; // 私有属性 对象类型

@property (nonatomic, strong, readonly) NSNumber *privateHeight; // 私有只读属性 对象类型

@end

@implementation CTMObject

+ (void)start:(NSString *)logo {

}

- (NSString *)finish:(BOOL)isOK {
return @"";
}

@end

runtime动态获取类信息,如下所示。

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
- (void)demo {
// isa
NSLog(@"元类: %@", object_getClass(CTMObject.class));
NSLog(@"根元类: %@", object_getClass(object_getClass(CTMObject.class)));
NSLog(@"根元类: %@", object_getClass(object_getClass(object_getClass(CTMObject.class))));
// super_class
NSLog(@"super_class: %@", class_getSuperclass(CTMObject.class));
// name
NSLog(@"name: %s", class_getName(CTMObject.class));
// version
NSLog(@"version: %d", class_getVersion(CTMObject.class));
// instance_size
NSLog(@"instance_size: %ld", class_getInstanceSize(CTMObject.class));

// ivars
unsigned int outCountV = 0;
Ivar *ivars = class_copyIvarList(CTMObject.class, &outCountV);// 获取成员变量 (公 + 私)
for (int i = 0; i < outCountV; ++i) {
Ivar ivar = ivars[i];
const char * name = ivar_getName(ivar);
const char * type = ivar_getTypeEncoding(ivar); // @"NSNumber"
NSLog(@"ivar:%s --> %s", name, type);
}

// property
unsigned int outCountPL = 0;
objc_property_t *pts = class_copyPropertyList(CTMObject.class, &outCountPL);// 获取属性 (公 + 私)
for (int i = 0; i < outCountPL; ++i) {
objc_property_t pt = pts[i];
const char * name = property_getName(pt); // 获取属性名
const char * type = property_getAttributes(pt); // 获取属性特性描述字符串 T@"NSString",C,N,V_name T@"NSNumber",R,N,V_height
NSLog(@"property:%s --> %s", name, type);
}

// methodLists
unsigned int outCountM = 0;
Method *methods = class_copyMethodList(CTMObject.class, &outCountM); // 获取成员实例方法
for (int i = 0; i < outCountM; ++i) {
Method method = methods[i];
SEL name = method_getName(method);
NSLog(@"method:%s --> %s", sel_getName(name), method_getTypeEncoding(method));
// v16@0:8
// v:代表 void @:代表OC对象 : :代表 SEL
}

// protocols
unsigned int outCountP = 0;
Protocol __unsafe_unretained **pros = class_copyProtocolList(CTMObject.class, &outCountP); // 获取协议
for (int i = 0; i < outCountP; ++i) {
Protocol *p = pros[i];
NSLog(@"protocol:%s", protocol_getName(p));
}
}

该代码的关键之处主要有以下几点:
  * import runtime 相关的头文件 objc/message.h。
  * 使用object_getClass函数来获取类对象的isa指针所指向的对象。
  * 使用class_getSuperclass函数来获取类对象的父类。
  * 使用class_getName函数来获取类对象的类名。
  * 使用class_getVersion函数来获取类对象的版本。
  * 使用class_getInstanceSize函数来获取类对象实例的大小。
  * 使用class_copyIvarList函数来获取类对象成员变量列表(共有+私有)。
  * 使用class_copyPropertyList函数来获取类对象属性列表(共有+私有)。
  * 使用class_copyMethodList函数来获取类对象成员方法列表(共有实例方法+私有实例方法)。
  * 使用class_copyProtocolList函数来获取类对象成员协议列表。

最终得到运行结果如下。

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
元类: CTMObject
根元类: NSObject
根元类: NSObject
super_class: NSObject
name: CTMObject
version: 0
instance_size: 72

ivar:_weight --> @"NSNumber"
ivar:_age --> i
ivar:_privateWeight --> @"NSNumber"
ivar:_privateAge --> i
ivar:_name --> @"NSString"
ivar:_height --> @"NSNumber"
ivar:_privateName --> @"NSString"
ivar:_privateHeight --> @"NSNumber"

property:privateName --> T@"NSString",C,N,V_privateName
property:privateHeight --> T@"NSNumber",R,N,V_privateHeight
property:name --> T@"NSString",C,N,V_name
property:height --> T@"NSNumber",R,N,V_height
property:hash --> TQ,R
property:superclass --> T#,R
property:description --> T@"NSString",R,C
property:debugDescription --> T@"NSString",R,C

method:privateFinish: --> @20@0:8B16
method:privateName --> @16@0:8
method:setPrivateName: --> v24@0:8@16
method:privateHeight --> @16@0:8
method:.cxx_destruct --> v16@0:8
method:name --> @16@0:8
method:setName: --> v24@0:8@16
method:height --> @16@0:8
method:finish: --> @20@0:8B16

protocol:CTMObjectDelegate3
protocol:CTMObjectDelegate1
protocol:CTMObjectDelegate2

  
根据运行结果可以得出如下结论:
  * 类在runtime面前是透明的,通过runtime可以获取到类的所有信息。
  * 成员变量和属性,不论公有还是私有,都可以获取到。
  * 类对象中只保存实例方法,类方法保存在对应的元类中。

类对象动态新增信息

使用Objective-C语言提供的与runtime相关的函数,动态的给已有类添加新信息,如下所示。

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#import <objc/runtime.h>
#import "CTMObject.h"

@implementation AddMemberController

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self demo];
}

- (void)eatWithPersonName:(NSString *)name {
NSLog(@"Person %@ start eat.", name);
}

- (void)highCopy {
NSLog(@"highCopy iCanDoIt.");
}

// 需求:动态的给CTMObject类,添加成员变量_sex、成员方法newMethod、属性hometown、协议CTMObjectDelegate4
- (void)demo {

Class clazz = CTMObject.class;

// 1.为CTMObject类动态添加成员变量 _sex
BOOL res = class_addIvar(clazz, "_sex", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
NSLog(@"%@", res ? @"ivar 添加成功": @"ivar 添加失败");

// 2.为CTMObject类动态添加成员方法 newMethod
Method method1 = class_getInstanceMethod([self class], @selector(eatWithPersonName:));
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
BOOL resM = class_addMethod(clazz, @selector(newMethod), class_getMethodImplementation([self class], @selector(eatWithPersonName:)), method_getTypeEncoding(method1));
#pragma clang diagnostic pop
NSLog(@"%@", resM ? @"Method 添加成功": @"Method 添加失败");

// 2.1.为CTMObject类动态添加 和已存在方法finish:同名的成员方法
Method method2 = class_getInstanceMethod(clazz, @selector(highCopy));
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
BOOL resO = class_addMethod(clazz, @selector(finish:), class_getMethodImplementation([self class], @selector(highCopy)), method_getTypeEncoding(method2));
#pragma clang diagnostic pop
NSLog(@"%@", resO ? @"同名Method 添加成功": @"同名Method 添加失败");

// 3.为CTMObject类动态添加属性 hometown
NSString *propertyName = @"hometown";
// 先判断该属性存在不,不存在再添加
Ivar ivar = class_getInstanceVariable(clazz, [[NSString stringWithFormat:@"_%@", propertyName] UTF8String]);
if (ivar == nil) {
objc_property_attribute_t type = {"T", [[NSString stringWithFormat:@"@\"%@\"", NSStringFromClass(NSString.class)] UTF8String]}; // type = NSString
objc_property_attribute_t ownership0 = {"C", ""}; // C = copy
objc_property_attribute_t ownership = {"N", ""}; // N = nonatomic
objc_property_attribute_t backingivar = {"V", [[NSString stringWithFormat:@"_%@", propertyName] UTF8String] }; // variable name
// Type encoding must be first,Backing ivar must be last。
objc_property_attribute_t attrs[] = {type, ownership0, ownership, backingivar};
BOOL result = class_addProperty(clazz, [propertyName UTF8String], attrs, 4);
NSLog(@"%@", result ? @"property 添加成功": @"property 添加失败");
if (result) {
// 添加get和set方法
class_addMethod(clazz, NSSelectorFromString(propertyName), (IMP)getter, "@@:");
class_addMethod(clazz, NSSelectorFromString([NSString stringWithFormat:@"set%@:", [propertyName capitalizedString]]), (IMP)setter, "v@:@");
}
else {
// 如果不能添加成功说明本类已有该property,那就替换。
class_replaceProperty(clazz, [propertyName UTF8String], attrs, 4);
// 添加get和set方法
class_addMethod(clazz, NSSelectorFromString(propertyName), (IMP)getter, "@@:");
class_addMethod(clazz, NSSelectorFromString([NSString stringWithFormat:@"set%@:",[propertyName capitalizedString]]), (IMP)setter, "v@:@");
}
}

// 4.为CTMObject类动态添加协议 CTMObjectDelegate4
BOOL rstP = class_addProtocol(clazz, @protocol(CTMObjectDelegate4));
NSLog(@"%@", rstP ? @"protocol 添加成功": @"protocol 添加失败");

[self auth];
}

- (void)auth {
CTMObject *obj = [[CTMObject alloc] init];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
// 使用动态新增的方法newMethod
[obj performSelector:@selector(newMethod) withObject:@"kobe"];
#pragma clang diagnostic pop
// 使用动态新增的属性hometown
[obj performSelector:NSSelectorFromString([NSString stringWithFormat:@"set%@:", [@"hometown" capitalizedString]]) withObject:@"kobe"];
NSLog(@"%@", [obj performSelector:NSSelectorFromString(@"hometown")]);

}

id getter(id self, SEL _cmd) {
NSString *key = NSStringFromSelector(_cmd);
Ivar ivar = class_getInstanceVariable([self class], "_dictCustomerProperty"); //basicsViewController里面有个_dictCustomerProperty属性
NSMutableDictionary *dictCustomerProperty = object_getIvar(self, ivar);
return [dictCustomerProperty objectForKey:key];
}

void setter(id self, SEL _cmd, id newValue) {
// 移除set
NSString *key = [NSStringFromSelector(_cmd) stringByReplacingCharactersInRange:NSMakeRange(0, 3) withString:@""];
// 首字母小写
NSString *head = [key substringWithRange:NSMakeRange(0, 1)];
head = [head lowercaseString];
key = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:head];
// 移除后缀 ":"
key = [key stringByReplacingCharactersInRange:NSMakeRange(key.length - 1, 1) withString:@""];

Ivar ivar = class_getInstanceVariable([self class], "_dictCustomerProperty"); //basicsViewController里面有个_dictCustomerProperty属性
NSMutableDictionary *dictCustomerProperty = object_getIvar(self, ivar);
if (!dictCustomerProperty) {
dictCustomerProperty = [NSMutableDictionary dictionary];
object_setIvar(self, ivar, dictCustomerProperty);
}
[dictCustomerProperty setObject:newValue forKey:key];
}

@end

该代码的关键之处主要有以下几点:
  * import runtime 相关的头文件 objc/message.h。
  * 使用class_addMethod函数给类添加成员方法。
  * 使用class_getInstanceMethod函数获取Method。
  * 使用class_getInstanceVariable函数获取Ivar。
  * 使用class_addProperty函数给类添加属性。
  * 使用class_addProtocol函数给类添加协议。
注意:class_addIvar函数不能给已有类添加成员变量。

最终,得到的运行结果如下:

1
2
3
4
5
6
7
ivar 添加失败
Method 添加成功
同名Method 添加失败
property 添加成功
protocol 添加成功

Person kobe start eat.

根据运行结果,可得出如下结论:
1,已存在的类,不可以动态添加成员变量。
2,已存在的类,可以动态添加成员方法,且能够遍历到动态添加的方法,但是不能和已有成员方法重名。
3,已存在的类,可以动态添加属性。
优点:能够在已有的类中添加property,且能够遍历到动态添加的属性。
缺点:比较麻烦,getter和setter需要自己写,且值也需要自己存储,如上面的代码,是把setter中的值存储到了_dictCustomerProperty里面,在getter中再从_dictCustomerProperty读出值。
4,已存在的类,可以动态添加协议,且能够遍历到动态添加的协议。

动态创建类对象

使用Objective-C语言提供的与runtime相关的函数,动态的创建一个新的类,如下所示。

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
#import <objc/message.h>

@implementation RTIsaViewController {
Class _clazz;
}

- (void)viewDidLoad {
[super viewDidLoad];
[self demo];
}

// 动态创建类
- (void) demo {

// 创建一个名为CustomView的类,它是UIView的子类。
_clazz = objc_allocateClassPair(UIView.class, "CustomView", 0);

// 为CustomView类添加report方法
class_addMethod(_clazz, @selector(report), (IMP)report, "v@:");

// 为CustomView类添加_name成员变量
class_addIvar(_clazz, [@"_name" UTF8String], sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));

// 注册该类 注册之后,不能再添加成员变量了
objc_registerClassPair(_clazz);

[self auth];
}

- (void)auth {
// 创建CustomView实例
id obj = [[_clazz alloc] init];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
// 调用report方法
[obj performSelector:@selector(report)];
#pragma clang diagnostic pop

// 设置_name成员变量值
[obj setValue:@"kobe" forKey:@"_name"];
// 获取_name成员变量值
NSLog(@"%@", [obj valueForKey:@"_name"]);
}

void report(id self, SEL _cmd) {
NSLog(@"This object is %@", self);
NSLog(@"Class is %@, and super is %@.", [self class], [self superclass]);

Class curClass = [self class];
for (int i = 0; i < 5; ++i) {
NSLog(@"Following the isa pointer %d times gives %p.", i, curClass);
curClass = object_getClass(curClass);
}

NSLog(@"NSObject Class is %p.", [NSObject class]);
NSLog(@"NSObject meta class is %p.", object_getClass([NSObject class]));
}

@end

该代码的关键之处主要有以下几点:
  * import runtime 相关的头文件 objc/message.h。
  * 使用objc_allocateClassPair函数创建新的类。
  * 使用class_addMethod函数给类添加实例方法。
  * 使用class_addIvar函数给类添加成员变量。
  * 使用objc_registerClassPair函数来注册新的类。
  * 使用object_getClass函数来获取类对象的isa指针所指向的对象。

最终,得到的运行结果如下:

1
2
3
4
5
6
7
8
9
10
This object is <CustomView: 0x7f9720c23ec0; frame = (0 0; 0 0); layer = <CALayer: 0x60000082e380>>
Class is CustomView, and super is UIView.
Following the isa pointer 0 times gives 0x600000639050.
Following the isa pointer 1 times gives 0x6000006390e0.
Following the isa pointer 2 times gives 0x7fff89c1ecd8.
Following the isa pointer 3 times gives 0x7fff89c1ecd8.
Following the isa pointer 4 times gives 0x7fff89c1ecd8.
NSObject Class is 0x7fff89c1ed00.
NSObject meta class is 0x7fff89c1ecd8.
kobe

将上面的运行结果中的内存地址与对应的类画在下图中,可以清楚看到:
1,从CustomView对象开始,在连续取了3次isa指针所指向的对象后,isa指针指向的地址变成了0x7fff89c1ecd8,也就是NSObject元类的地址。之后第四次取isa指针所指的对象时,其结果仍然为0x7fff89c1ecd8,这说明NSObject元类的isa指针确实是指向它自己的。

2,作为对比,我们在代码最后获取了NSObject类的isa指针地址,其值是0x7fff89c1ecd8,这说明所有的元类对象的isa指针,都是指向NSObject元类的。

isa关系图

3,已存在的类中不能添加成员变量,所有必须通过objc_allocateClassPair动态创建一个类,才能调用class_addIvar添加Ivar,最后通过objc_registerClassPair注册class。

Method Swizzling

方法交换的本质是交换两个方法的IMP。

Objective-C提供了以下API来动态替换类方法或实例方法的实现:

  • class_replaceMethod
  • method_exchangeImplementations
  • method_setImplementation

具体使用方式如下。

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
#import "ExChangeMethodController.h"
#import <objc/runtime.h>

@interface ExChangeMethodController ()

@end

@implementation ExChangeMethodController

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self methodOne];
[self methodTwo];
[self methodThree];
});
}

// 1.交换两个方法的实现
+ (void)methodOne {
Method method1 = class_getInstanceMethod(self.class, @selector(viewWillAppear:));
Method method2 = class_getInstanceMethod(self.class, @selector(__viewWillAppear:));
method_exchangeImplementations(method1, method2);
}

- (void)__viewWillAppear:(BOOL)animated {
NSLog(@"交换两个方法的实现");
[self __viewWillAppear:YES];
}

// 2.给方法设置实现方式
+ (void)methodTwo {

Method method = class_getInstanceMethod(self.class, @selector(viewDidAppear:));

void(*originalIMP)(id self, SEL _cmd) = nil;
originalIMP = (typeof(originalIMP))method_getImplementation(method);

id block = ^(id self, SEL _cmd) {
NSLog(@"给方法设置实现方式");
originalIMP(self, _cmd);
};

IMP newIMP = imp_implementationWithBlock(block);
method_setImplementation(method, newIMP);
}

// 3.替换方法定义
+ (void)methodThree {
Method replaceMethod = class_getInstanceMethod(self.class, @selector(kobe));

class_replaceMethod(self.class, @selector(james), method_getImplementation(replaceMethod), method_getTypeEncoding(replaceMethod));

id obj = [[self.class alloc] init];
[obj james];
}

- (void)kobe {
NSLog(@"kobe 替换方法定义");
}

- (void)james {
NSLog(@"james 替换方法定义");
}

@end

该代码的关键之处主要有以下几点:
  * import runtime 相关的头文件 objc/runtime.h。
  * 使用class_getInstanceMethod函数获取类对象的Method。
  * 使用method_exchangeImplementations函数交换两个方法的实现。
  * 使用method_getImplementation函数获取Method的实现IMP。
  * 使用method_getTypeEncoding函数获取Method的类型。
  * 使用method_setImplementation函数设置一个方法的实现。
  * 使用class_replaceMethod函数替换类方法的定义。

注意:1,class_replaceMethod,当类中没有想替换的原方法时,该方法会调用class_addMethod来为该类增加一个新方法,正因为如此,class_replaceMethod在调用是需要传入types函数。

2,method_exchangeImplementations的内部实现其实是调用了两次method_setImplementation方法,从苹果的文档中能清晰的了解到,如下如所示。

由以上的区别可以总结出这三个API的使用场景:

  • class_replaceMethod,当需要替换的方法有可能不存在时,可以考虑使用该方法。

  • method_exchangeImplementations,当需要交换两个方法的实现时使用。

  • method_setImplementation是最简单的用法,当仅仅需要为一个方法设置其实现方式时使用。

CATALOG
  1. 1. 动态获取类对象信息
  2. 2. 类对象动态新增信息
  3. 3. 动态创建类对象
  4. 4. Method Swizzling