runtime应用笔记

了解了runtime的基本原理,那么runtime究竟有什么用处呢?参考Runtime全方位装逼指南,总结了以下几点应用场景。

给category添加属性

原理

对象关联允许开发者对已经存在的类在 Category 中添加自定义的属性:

1
OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);
  • object 是源对象.
  • value 是被关联的对象.
  • key 是关联的键,objc_getAssociatedObject 方法通过不同的 key 即可取出对应的被关联对象.
  • policy 是一个枚举值,表示关联对象的行为,从命名就能看出各个枚举值的含义:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
    * The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
    * The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
    * The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
    * The association is made atomically. */
    };

要取出被关联的对象使用 objc_getAssociatedObject 方法即可,要删除一个被关联的对象,使用 objc_setAssociatedObject 方法将对应的 key 设置成 nil 即可:

1
objc_setAssociatedObject(self, associatedKey, nil, OBJC_ASSOCIATION_COPY_NONATOMIC);

objc_removeAssociatedObjects 方法将会移除源对象中所有的关联对象.

关联对象并不是存储在被关联对象本身内存中,而是存储在全局的统一的一个AssociationsManager中。其中管理着一个 AssociationsHashMap。键是设置关联对象的 object,值是保存这个 object 的所有关联对象的另一个 hashMap

示例:

新建 UIButton 的 Category,在其中设置clickBlock属性(UIButton+ClickBlock.h):

1
2
3
4
5
typedef void(^clickBlock)(void);

@interface UIButton (ClickBlock)
@property (nonatomic,copy) clickBlock click;
@end

在.m中设置click的set,get方法(UIButton+ClickBlock.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
#import "UIButton+ClickBlock.h"
#import <objc/runtime.h>

static const void *associatedKey = "associatedKey";

@implementation UIButton (ClickBlock)

//Category中的属性,只会生成setter和getter方法,不会生成成员变量

-(void)setClick:(clickBlock)click{
objc_setAssociatedObject(self, associatedKey, click, OBJC_ASSOCIATION_COPY_NONATOMIC);
[self removeTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
if (click) {
[self addTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
}
}

-(clickBlock)click{
return objc_getAssociatedObject(self, associatedKey);
}

-(void)buttonClick{
if (self.click) {
self.click();
}
}

@end

Category中的属性,只会生成setter和getter方法的声明,不会生成成员变量,而且需要自己实现方法。

其中,在set方法中使用addTarget:action:forControlEvents:给button设置了点击事件。

self.click()表示使用self.click获得block,再通过block()执行块。

为什么不直接给click赋值,而是通过runtime的objc_setAssociatedObject方法呢?
@property属性会在编译时自动生成一个实例变量_click以及其set,get方法。但是由于runtime只能添加方法,不能添加实例变量。因此,_click并没有添加进UIButton的ivar中,因而不能使用。只能通过runtime的方法,添加对应键值对。

@property在本例中只是为了在.h里声明一个getset方法。可替换成:

1
2
3
4
5
6
7
typedef void(^clickBlock)(void);

@interface UIButton (ClickBlock)
//@property (nonatomic,copy) clickBlock click;
- (clickBlock)click;
- (void)setClick:(clickBlock)click;
@end

关联对象相当于把一个对象关联到另外一个对象上。在关联后可以随时获取该关联的对象,在对象被销毁时会移除所有关联的对象。

字典与模型转换

原理

字典转模型的时候:

  1. 根据字典的 key 生成 setter 方法.
  2. 使用 objc_msgSend 调用 setter 方法为 Model 的属性赋值(或者 KVC).

模型转字典的时候:

  1. 调用 class_copyPropertyList 方法获取当前 Model 的所有属性.
  2. 调用 property_getName 获取属性名称.
  3. 根据属性名称生成 getter 方法.
  4. 使用 objc_msgSend 调用 getter 方法获取属性值(或者 KVC).

示例

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
@interface NSObject (KeyValues)

+(id)objectWithKeyValues:(NSDictionary *)aDictionary;

-(NSDictionary *)keyValuesWithObject;

@end


#import "NSObject+KeyValues.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation NSObject (KeyValues)

//字典转模型
+(id)objectWithKeyValues:(NSDictionary *)aDictionary{
id objc = [[self alloc] init];
for (NSString *key in aDictionary.allKeys) {
id value = aDictionary[key];

/*判断当前属性是不是Model*/
objc_property_t property = class_getProperty(self, key.UTF8String);
unsigned int outCount = 0;
objc_property_attribute_t *attributeList = property_copyAttributeList(property, &outCount);
objc_property_attribute_t attribute = attributeList[0];
NSString *typeString = [NSString stringWithUTF8String:attribute.value];
if ([typeString isEqualToString:@"@\"TestModel\""]) {
value = [self objectWithKeyValues:value];
}
/**********************/

//生成setter方法,并用objc_msgSend调用
NSString *methodName = [NSString stringWithFormat:@"set%@%@:",[key substringToIndex:1].uppercaseString,[key substringFromIndex:1]];
SEL setter = sel_registerName(methodName.UTF8String);
if ([objc respondsToSelector:setter]) {
((void (*) (id,SEL,id)) objc_msgSend) (objc,setter,value);
}
free(attributeList);
}
return objc;
}

//模型转字典
-(NSDictionary *)keyValuesWithObject{
unsigned int outCount = 0;
objc_property_t *propertyList = class_copyPropertyList([self class], &outCount);
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
for (int i = 0; i < outCount; i ++) {
objc_property_t property = propertyList[i];

//生成getter方法,并用objc_msgSend调用
const char *propertyName = property_getName(property);
SEL getter = sel_registerName(propertyName);
if ([self respondsToSelector:getter]) {
id value = ((id (*) (id,SEL)) objc_msgSend) (self,getter);

/*判断当前属性是不是Model*/
if ([value isKindOfClass:[self class]] && value) {
value = [value keyValuesWithObject];
}
/**********************/

if (value) {
NSString *key = [NSString stringWithUTF8String:propertyName];
[dict setObject:value forKey:key];
}
}

}
free(propertyList);
return dict;
}
@end

使用:

1
2
3
4
5
6
7
8
9
-(void)keyValuesTest{

TestModel *model = [TestModel objectWithKeyValues:dictionary];
NSLog(@"name is %@",model.name);
NSLog(@"son name is %@",model.son.name);

NSDictionary *dict = [model keyValuesWithObject];
NSLog(@"dict is %@",dict);
}

注意:

  1. 在NSObject中添加类方法,其中的self指的是TestModel这个类。
  2. objc_property_t具有两个属性,name和attribute。调用property_getAttribute将返回attribute的字符串。调用property_copyAttributeList则将字符串切分,返回一个objc_property_attribute_t类型的指针,outCount返回了属性的数量。

outCount使用了指向指针的指针的方式,使没有返回outCount的情况下,修改了outCount的值。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (NSArray *)scanBeginStr:(NSString *)beginstr endStr:(NSString *)endstr inText:(NSMutableString * *)textPointer{
NSRange range1,range2;
NSUInteger location =0,length=0;
range1.location = 0;
NSMutableString *text = *textPointer;
NSMutableArray *rangeArray = [NSMutableArray array];
while (range1.location != NSNotFound) {
range1 = [text rangeOfString:beginstr];
range2 = [text rangeOfString:endstr];
if (range1.location != NSNotFound) {
location = range1.location;
length = range2.location - range1.location - 1;
if (length > 5000)break;
[text replaceOccurrencesOfString:beginstr withString:@"" options:NSCaseInsensitiveSearch range:NSMakeRange(0, range1.location + range1.length)];
[text replaceOccurrencesOfString:endstr withString:@"" options:NSCaseInsensitiveSearch range:NSMakeRange(0, range2.location + range2.length - 1)];
}
[rangeArray addObject:@{@"location":@(location),@"length":@(length)}];
}
return rangeArray;
}

使用:通过&取指针的地址

1
NSArray *rangeArray = [self scanBegin3Str:@"<" endStr:@">" inText:&mutableText];


自动归档

原理

归档是一种很常用的文件储存方法,几乎任何类型的对象都能够被归档储存。自动归档就是动态获取model的各个属性,进行保存:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)encodeWithCoder:(NSCoder *)aCoder{
[aCoder encodeObject:self.name forKey:@"name"];
[aCoder encodeObject:self.ID forKey:@"ID"];
}

- (id)initWithCoder:(NSCoder *)aDecoder{
if (self = [super init]) {
self.ID = [aDecoder decodeObjectForKey:@"ID"];
self.name = [aDecoder decodeObjectForKey:@"name"];
}
return self;
}

如果当前 Model 有100个属性的话,就需要写100行这种代码.通过 Runtime 我们就可以轻松解决这个问题:

  1. 使用 class_copyIvarList 方法获取当前 Model 的所有成员变量.
  2. 使用 ivar_getName 方法获取成员变量的名称.
  3. 通过 KVC 来读取 Model 的属性值(encodeWithCoder:),以及给 Model 的属性赋值(initWithCoder:).

示例:

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
// TestModel 头文件
@interface TestModel : NSObject<NSCoding>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSNumber *age;
@property (nonatomic, copy) NSNumber *phoneNumber;
@property (nonatomic, copy) NSNumber *height;
@property (nonatomic, strong) NSDictionary *info;
@property (nonatomic, strong) TestModel *son;
@end




#import "TestModel.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation TestModel
- (void)encodeWithCoder:(NSCoder *)aCoder{
unsigned int outCount = 0;
Ivar *vars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar var = vars[i];
const char *name = ivar_getName(var);
NSString *key = [NSString stringWithUTF8String:name];

// 注意kvc的特性是,如果能找到key这个属性的setter方法,则调用setter方法
// 如果找不到setter方法,则查找成员变量key或者成员变量_key,并且为其赋值
// 所以这里不需要再另外处理成员变量名称的“_”前缀
id value = [self valueForKey:key];
[aCoder encodeObject:value forKey:key];
}
free(vars);
}

- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder{
if (self = [super init]) {
unsigned int outCount = 0;
Ivar *vars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar var = vars[i];
const char *name = ivar_getName(var);
NSString *key = [NSString stringWithUTF8String:name];
id value = [aDecoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
free(vars);
}
return self;
}
@end

注意自动归档由于用到的都是与 object 有关的 encode ,decode 方法。因此 model 中就不能存在非 object 类型,比如 int 就要变为 NSNumber (注意,NSNumber 类型不能直接做加减运算)。

使用:

1
2
3
4
5
6
7
8
9
10
11
12
-(void)keyedArchiverTest{

TestModel *model = [TestModel objectWithKeyValues:dictionary];

NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
path = [path stringByAppendingPathComponent:@"test"];
[NSKeyedArchiver archiveRootObject:model toFile:path];

TestModel *m = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
NSLog(@"m.name is %@",m.name);
NSLog(@"m.son name is %@",m.son.name);
}

动态方法解析与消息转发

原理

消息转发的大致过程如图:

消息转发流程

  1. 当 Runtime 系统在Cache和方法分发表中(包括超类)找不到要执行的方法时,Runtime会调用resolveInstanceMethod:resolveClassMethod:(添加实例方法实现和类方法实现)来给程序员一次动态添加方法实现的机会,指定是否动态添加方法。若返回NO,则进入下一步,若返回YES,则通过 class_addMethod 函数动态地添加方法,消息得到处理,此流程完毕.
  2. resolveInstanceMethod: 方法返回 NO 时,就会进入 forwardingTargetForSelector: 方法,这是 Runtime 给我们的第二次机会,用于指定响应这个 selector 的对象。返回nil,进入下一步;返回某个对象,则会调用该对象的方法.
  3. forwardingTargetForSelector: 返回的是nil,则我们首先要通过 methodSignatureForSelector: 来指定方法签名。 methodSignatureForSelector 方法返回nil,表示不处理,若返回方法签名,则会进入下一步.
  4. methodSignatureForSelector: 方法返回方法签名后,就会调用 forwardInvocation: 方法,我们可以通过 NSInvocation 对象做很多处理,比如修改实现方法,修改响应对象等.
  5. 如果到最后,消息还是没有得到响应,程序就会crash.

三步的流程为

  1. 增加这个方法
  2. 使用其他对象调用这个方法
  3. 任意对象调用任意方法

示例:

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

@implementation Monkey

-(void)jump{
NSLog(@"monkey can not fly, but! monkey can jump");
}

+(BOOL)resolveInstanceMethod:(SEL)sel{

/*
如果当前对象调用了一个不存在的方法
Runtime会调用resolveInstanceMethod:来进行动态方法解析
我们需要用class_addMethod函数完成向特定类添加特定方法实现的操作
返回NO,则进入下一步forwardingTargetForSelector:
*/

if(sel == @selector(fly)){
class_addMethod(self, sel, class_getMethodImplementation(self, sel_registerName("jump")), "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}

-(id)forwardingTargetForSelector:(SEL)aSelector{

/*
在消息转发机制执行前,Runtime 系统会再给我们一次重定向的机会
通过重载forwardingTargetForSelector:方法来替换消息的接受者为其他对象
返回nil则进步下一步forwardInvocation:
*/

#if 0
return nil;
#else
return [[Bird alloc] init];
#endif
}

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{

/*
获取方法签名进入下一步,进行消息转发
*/

return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

-(void)forwardInvocation:(NSInvocation *)anInvocation{

/*
消息转发
*/

return [anInvocation invokeWithTarget:[[Bird alloc] init]];
}

@end

其中 "v@:" 表示返回值和参数,这个符号涉及Type Encoding以及关于type encodings的理解–runtime programming guide。其中 class_addMethod 具体的使用方式可以参见下面一节。

一般来说可以使用method_getTypeEncoding()获取更详细的Type_Encoding,代替手动的 输入的 v@:。下面例子中也会用到。


其中v表示void返回类型@表示参数id(self)表示SEL(_cmd)@:是必须要有的。后面可以接入参类型。

举个例子:"i@:@"
i表示返回值类型int
@:和上面意义相同
@最后一个@表示有一个入参,是id类型。


NSInvocation 使用详见 API使用

转发与多继承

转发和继承相似,可以用于为Objc编程添加一些多继承的效果。就像下图那样,一个对象把消息转发出去,就好似它把另一个对象中的方法借过来或是“继承”过来一样。
runtime_transmit

这使得不同继承体系分支下的两个类可以“继承”对方的方法,在上图中WarriorDiplomat没有继承关系,但是Warriornegotiate消息转发给了Diplomat后,就好似DiplomatWarrior的超类一样。

消息转发弥补了 Objc 不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。它将问题分解得很细,只针对想要借鉴的方法才转发,而且转发机制是透明的。

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
- (void)createClass
{
Class MyClass = objc_allocateClassPair([NSObject class], "myclass", 0);
//添加一个NSString的变量,第四个参数是对齐方式,第五个参数是参数类型
if (class_addIvar(MyClass, "itest", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *))) {
NSLog(@"add ivar success");
}
//myclasstest是已经实现的函数,"v@:"这种写法见type encoding
class_addMethod(MyClass, @selector(myclasstest:), (IMP)myclasstest, "v@:");
//注册这个类到runtime系统中就可以使用他了
objc_registerClassPair(MyClass);
//生成了一个实例化对象
id myobj = [[MyClass alloc] init];
NSString *str = @"asdb";
//给刚刚添加的变量赋值
// object_setInstanceVariable(myobj, "itest", (void *)&str);在ARC下不允许使用
[myobj setValue:str forKey:@"itest"];
//调用myclasstest方法,也就是给myobj这个接受者发送myclasstest这个消息
[myobj myclasstest:10];

}
//这个方法实际上没有被调用,但是必须实现否则编译都不能通过
- (void)myclasstest:(int)a
{

}
//调用的是这个方法
static void myclasstest(id self, SEL _cmd, int a) //self和_cmd是必须的,在之后可以随意添加其他参数
{

Ivar v = class_getInstanceVariable([self class], "itest");
//返回名为itest的ivar的变量的值
id o = object_getIvar(self, v);
//成功打印出结果
NSLog(@"%@", o);
NSLog(@"int a is %d", a);
}

Method Swizzling

此部分参考自Objective-C的方法替换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
#import <objc/runtime.h> 

@implementation UIViewController (Tracking)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class aClass = [self class];

SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);

Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);

// When swizzling a class method, use the following:
// Class aClass = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(aClass, originalSelector);
// Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);

BOOL didAddMethod =
class_addMethod(aClass,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));

if (didAddMethod) {
class_replaceMethod(aClass,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}

#pragma mark - Method Swizzling

- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", self);
}

@end

原理

概述

在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。

每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的Method实现。如下图
runtime_swizzling_1

通过Swizzling需要实现的是偷换selector的IMP,如下图所示:
runtime_swizzling_2

实现过程

上面的代码通过添加一个Tracking类别到UIViewController类中,将UIViewController类的viewWillAppear:方法和Tracking类别中xxx_viewWillAppear:方法的实现相互调换。Swizzling 应该在+load方法中实现,因为+load是在一个类最开始加载时调用。dispatch_once是GCD中的一个方法,它保证了代码块只执行一次,并让其为一个原子操作,线程安全是很重要的。

实现Swizzling需要考虑两种情况,第一种是被替换方法没有在当前类中重写过,而是只在其父类中实现了;第二种情况是这个方法已经存在于当前类中,就如本例中想要替换掉UIViewController中的viewWillAppear:方法。这两种情况要区别对待。

对于第一种情况,由于当前类内没有这个方法,应当现在当前类中添加一个新的实现方法(xxx_viewWillAppear:),然后将复写的方法替换为原先的实现(viewWillAppear:).

1
2
3
4
5
BOOL didAddMethod = 
class_addMethod(aClass,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));

class_addMethod将本来不存在于被操作的Class里的swizzledMethod的实现添加在被操作的Class里,并使用originalSelector作为其选择子。如果发现方法已经存在,会失败返回。


通过上一篇runtime原理的分析,class_addMethod应该是先在类的method数组里找是否有这个SEL,如果没有就添加一个method_t


如果添加成功(当前类中没有重写过父类的该方法),再把目标类中的方法替换为旧有的实现:

1
2
3
4
5
if (didAddMethod) { 
class_replaceMethod(aClass,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));

addMethod会让当前类的方法(IMP)指向新的实现(SEL),使用replaceMethod将新的方法(IMP)指向原先的实现(SEL)。现在通过旧方法SEL来调用,就会实现新方法的IMP,通过新方法的SEL来调用,就会实现旧方法的IMP

如果添加失败了,就是第二情况(方法已经在当前类中实现过了)。这时可以通过method_exchangeImplementations直接交换两个method_tIMP:

1
2
3
else {
method_exchangeImplementations(originalMethod, overrideMethod);
}

所以本例中由于viewWillAppear:已经在UIViewController中实现过了,所以,class_addMethod失败,通过method_exchangeImplementations达到交换实现。如果要通过class_addMethod添加,需要自定义一个View继承UIViewController,再在这个类中替换viewWillAppear:

如果类中没有想被替换实现的原方法时,class_replaceMethod相当于直接调用class_addMethod向类中添加该方法的实现。

method_exchangeImplementations方法做的事情与如下的原子操作等价:

1
2
3
4
IMP imp1 = method_getImplementation(m1);
IMP imp2 = method_getImplementation(m2);
method_setImplementation(m1, imp2);
method_setImplementation(m2, imp1);

直接设置了method:m1,m2IMP,简单暴力。

对于注释了的这几行:

1
2
3
4
5
// When swizzling a class method, use the following:
// Class aClass = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(aClass, originalSelector);
// Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);

object_getClass((id)self)[self class] 返回的结果类型都是 Class,但前者为元类,后者为其本身,因为此时 selfClass 而不是实例.注意 [NSObject class][object实例 class] 的区别:

1
2
3
4
5
6
7
+ (Class)class {
return self;
}

- (Class)class {
return object_getClass(self);
}

object_getClass()方法返回对象的isa

最后,xxx_viewWillAppear:方法的定义看似是递归调用引发死循环,其实不会的。因为[self xxx_viewWillAppear:animated]消息会动态找到xxx_viewWillAppear:方法的实现,而它的实现已经被我们与viewWillAppear:方法实现进行了互换,所以这段代码不仅不会死循环,如果你把[self xxx_viewWillAppear:animated]换成[self viewWillAppear:animated]反而会引发死循环。

Demo 详见RuntimeLearn

危险性

要在 load 方法中

Method swizzling不是原子性操作。如果在+load方法里面写,是没有问题的,但是如果写在+initialize方法中就会出现一些奇怪的问题。并且在 +initialize 中写也有可能被覆盖。

如果不写在 +load 中可能会因为多线程难以保证另一个线程中不会同时调用交换方法,从而导致程序不按预期执行。

Copy父类的方法带来 hook 父类失效的问题

如果 hook 了为重写父类方法的子类的方法。那么会添加父类的方法到子类中。如果父类中也hook了该方法,就可能会产生问题。

如果父类的分类的 load 方法的先执行的时候,先发生替换,子类再替换,那么没有问题。

但是如果子类的分类的 load 方法先执行的时候,那么子类先 hook 了父类方法,父类再 hook 自己的方法。那么子类调用的时候就不会走父类实现 hook 添加的各种处理了。

主要原因就是我们能保证父类的 load 方法先于子类的 load 方法执行。但是不能保证父类的分类的 load 方法先于子类的分类的 load 方法先执行。