了解了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
11typedef 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
5typedef 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
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
7typedef void(^clickBlock)(void);
@interface UIButton (ClickBlock)
//@property (nonatomic,copy) clickBlock click;
- (clickBlock)click;
- (void)setClick:(clickBlock)click;
@end
关联对象相当于把一个对象关联到另外一个对象上。在关联后可以随时获取该关联的对象,在对象被销毁时会移除所有关联的对象。
字典与模型转换
原理
字典转模型的时候:
- 根据字典的 key 生成 setter 方法.
- 使用 objc_msgSend 调用 setter 方法为 Model 的属性赋值(或者 KVC).
模型转字典的时候:
- 调用 class_copyPropertyList 方法获取当前 Model 的所有属性.
- 调用 property_getName 获取属性名称.
- 根据属性名称生成 getter 方法.
- 使用 objc_msgSend 调用 getter 方法获取属性值(或者 KVC).
示例
1 | @interface NSObject (KeyValues) |
使用: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);
}
注意:
- 在NSObject中添加类方法,其中的
self
指的是TestModel
这个类。 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 我们就可以轻松解决这个问题:
- 使用 class_copyIvarList 方法获取当前 Model 的所有成员变量.
- 使用 ivar_getName 方法获取成员变量的名称.
- 通过 KVC 来读取 Model 的属性值(encodeWithCoder:),以及给 Model 的属性赋值(initWithCoder:).
示例:
1 | // TestModel 头文件 |
注意自动归档由于用到的都是与 object 有关的 encode
,decode
方法。因此 model 中就不能存在非 object 类型,比如 int 就要变为 NSNumber
(注意,NSNumber
类型不能直接做加减运算)。
使用:
1 | -(void)keyedArchiverTest{ |
动态方法解析与消息转发
原理
消息转发的大致过程如图:
- 当 Runtime 系统在
Cache
和方法分发表中(包括超类)找不到要执行的方法时,Runtime会调用resolveInstanceMethod:
或resolveClassMethod:
(添加实例方法实现和类方法实现)来给程序员一次动态添加方法实现的机会,指定是否动态添加方法。若返回NO
,则进入下一步,若返回YES
,则通过class_addMethod
函数动态地添加方法,消息得到处理,此流程完毕. resolveInstanceMethod:
方法返回NO
时,就会进入forwardingTargetForSelector:
方法,这是 Runtime 给我们的第二次机会,用于指定响应这个selector
的对象。返回nil
,进入下一步;返回某个对象,则会调用该对象的方法.- 若
forwardingTargetForSelector:
返回的是nil
,则我们首先要通过methodSignatureForSelector:
来指定方法签名。methodSignatureForSelector
方法返回nil
,表示不处理,若返回方法签名,则会进入下一步. - 当
methodSignatureForSelector:
方法返回方法签名后,就会调用forwardInvocation:
方法,我们可以通过NSInvocation
对象做很多处理,比如修改实现方法,修改响应对象等. - 如果到最后,消息还是没有得到响应,程序就会crash.
三步的流程为
- 增加这个方法
- 使用其他对象调用这个方法
- 任意对象调用任意方法
示例:
1 |
|
其中 "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编程添加一些多继承的效果。就像下图那样,一个对象把消息转发出去,就好似它把另一个对象中的方法借过来或是“继承”过来一样。
这使得不同继承体系分支下的两个类可以“继承”对方的方法,在上图中Warrior
和Diplomat
没有继承关系,但是Warrior
将negotiate
消息转发给了Diplomat
后,就好似Diplomat
是Warrior
的超类一样。
消息转发弥补了 Objc 不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。它将问题分解得很细,只针对想要借鉴的方法才转发,而且转发机制是透明的。
Runtime-动态创建类添加属性和方法
1 | - (void)createClass |
Method Swizzling
此部分参考自Objective-C的方法替换、Objective-C Runtime等系列文章
例子
1 |
|
原理
概述
在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。
每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的Method实现。如下图
通过Swizzling需要实现的是偷换selector的IMP,如下图所示:
实现过程
上面的代码通过添加一个Tracking
类别到UIViewController
类中,将UIViewController
类的viewWillAppear:
方法和Tracking
类别中xxx_viewWillAppear:
方法的实现相互调换。Swizzling 应该在+load
方法中实现,因为+load
是在一个类最开始加载时调用。dispatch_once
是GCD中的一个方法,它保证了代码块只执行一次,并让其为一个原子操作,线程安全是很重要的。
实现Swizzling需要考虑两种情况,第一种是被替换方法没有在当前类中重写过,而是只在其父类中实现了;第二种情况是这个方法已经存在于当前类中,就如本例中想要替换掉UIViewController
中的viewWillAppear:
方法。这两种情况要区别对待。
对于第一种情况,由于当前类内没有这个方法,应当现在当前类中添加一个新的实现方法(xxx_viewWillAppear:
),然后将复写的方法替换为原先的实现(viewWillAppear:
).
1 | BOOL didAddMethod = |
class_addMethod
将本来不存在于被操作的Class里的swizzledMethod
的实现添加在被操作的Class里,并使用originalSelector
作为其选择子。如果发现方法已经存在,会失败返回。
通过上一篇runtime原理的分析,class_addMethod
应该是先在类的method数组里找是否有这个SEL
,如果没有就添加一个method_t
。
如果添加成功(当前类中没有重写过父类的该方法),再把目标类中的方法替换为旧有的实现:1
2
3
4
5if (didAddMethod) {
class_replaceMethod(aClass,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
addMethod
会让当前类的方法(IMP)指向新的实现(SEL),使用replaceMethod
再将新的方法(IMP)指向原先的实现(SEL)。现在通过旧方法SEL
来调用,就会实现新方法的IMP
,通过新方法的SEL
来调用,就会实现旧方法的IMP
。
如果添加失败了,就是第二情况(方法已经在当前类中实现过了)。这时可以通过method_exchangeImplementations
直接交换两个method_t
的IMP
:1
2
3else {
method_exchangeImplementations(originalMethod, overrideMethod);
}
所以本例中由于viewWillAppear:
已经在UIViewController中实现过了,所以,class_addMethod
失败,通过method_exchangeImplementations
达到交换实现。如果要通过class_addMethod
添加,需要自定义一个View继承UIViewController,再在这个类中替换viewWillAppear:
。
如果类中没有想被替换实现的原方法时,class_replaceMethod
相当于直接调用class_addMethod
向类中添加该方法的实现。
method_exchangeImplementations
方法做的事情与如下的原子操作等价:1
2
3
4IMP imp1 = method_getImplementation(m1);
IMP imp2 = method_getImplementation(m2);
method_setImplementation(m1, imp2);
method_setImplementation(m2, imp1);
直接设置了method
:m1
,m2
的IMP
,简单暴力。
对于注释了的这几行: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
,但前者为元类,后者为其本身,因为此时 self
为 Class
而不是实例.注意 [NSObject class]
与 [object实例 class]
的区别:
1 | + (Class)class { |
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 方法先执行。