Runtime之黑魔法

像一些巧妙地伎俩、hack手段或者变通的解决方案,人们总是倾向于创造机会来使用他们—特别是在刚刚接触的时候。不要为了使用而使用,尽可能的在理解并领悟之后再做出正确的方案,避免自己陷入一知半解的尴尬处境。

关联对象

在编码的时候会需要对一个既有类添加一个属性,通常最稳妥的方法是对这个类进行继承,使用子类化进行属性的读取,从而达到目的,但是,对于单单一个属性来说创建一个子类实在是没有必要,还有可能是有时候类的实例可能由某种机制创建的,而开发者无法令这种机制创建自己的子类实例化,而在runtime中一个神奇的方案可以很简单的就解决问题,关联对象(Associated Object)。

可以给既有对象关联其他对象,通过“键”来区分关联的对象,关联的对象指明“存储策略”,来维护响应的内存管理。在关联对象的时候使用的内存管理语义如下,

关联类型 对应的@property属性
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC retain,nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC copy,nonatomic
OBJC_ASSOCIATION_RETAIN retain
OBJC_ASSOCIATION_COPY copy

基本的关联对象方法有三个方法,

1
id objc_getAssociatedObject(id object, const void *key)

id object获取谁的关联对象。

const void *key根据这个唯一的key获取关联对象。

返回的id对象就是object通过key关联的对象

1
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

objc_AssociationPolicy policy 就是关联对象的时候要指定的内存管理语义

1
void objc_removeAssociatedObject(id object)

该方法会将指定对象下的所有关联对象进行移除,使对象恢复成原始状态,所以为了使移除了不必要移除的对象,我们还是进行objc_setAssociatedObject传nil来移除关联值(key)。

由于关联对象的set和get都是通过key进行的,所以要保证针对同一个关联对象要使用的是相同的指针,因此在设置关联对象值的时候通常使用静态全局变量做键。

1
2
static char someKey;
return objc_getAssociatedObject(self, &someKey);

在一般的对系统提供的类进行单次的属性添加,会直接使用get和set函数进行关联对象,而在一个自定义的类中使用关联对象的时候,还是建议进行如下设置,这样方便使用。SDWebImage中在UIImageView的分类方法中就是使用如下类似方法对UIImageView添加了一个url属性。

//添加关联对象

1
2
3
- (void)addAssociatedObject:(id)object{
objc_setAssociatedObject(self, @selector(getAssociatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

//获取关联对象

1
2
3
- (id)getAssociatedObject{
return objc_getAssociatedObject(self, _cmd);
}

注意:这里面我们把getAssociatedObject方法的地址作为唯一的key,_cmd代表当前调用方法的地址,等效于@selector()。

在运用关联对象的时候我的使用场景是基于tableView中的cell进行某些操作弹出UIAlertView,在点击确定的时候进行某些联网操作,联网操作需要cell位置下的某些数据,但由于确定的点击方法实在代理方法中的,导致cell对应的属性不能够和UIAlertView进行一同传递。这时候就使用关联对象对UIAlertView进行关联一个对象,然后在代理方法中进行获取继而进行下一步的操作。

这里说一句题外话,提醒视图(UIAlertView、UIActionSheetView)的某些无视视图层级的展示功能注定他们不会是一个好的控件,在iOS8之后提醒视图都已经不能够用了,推荐使用UIAlertController进行替换,使用UIAlertController可以在响应的位置进行后续操作,不需要使用代理以及视图的show了,而是使用控制器的present或者push。

相信看到前面的对关联对象的内存管理语义的设定就能想到未来可预知的内存问题,严重的会导致程序崩溃。这还只是简单的关联对象,别忘了block也属于一种对象,也可以使用这种技术进行关联对象,但又由于block有时候需要保留某些变量,造成循环引用,引发难以查找的bug。因此在没有办法的时候关联对象才是解决问题的办法,不要为了使用而使用

方法交换

一个问题:由于oc对象在收到消息之后,究竟会调用何种方法需要在运行期才会解析出来,那么给定的选择子名称对应的方法是不是也可以在运行期进行改变?

一些场景:对App的用户行为进行追踪和分析。简单说,就是当用户看到某个View或者点击某个Button的时候,就把这个事件记下来;统一对视图控制器的view进行背景色设置。

每个类里都有一个Dispatch Table,将方法的名字(SEL)跟方法的实现(IMP,指向 C 函数的指针)一一对应。Swizzle 一个方法其实就是在程序运行时在方法列表里做点改动,让这个方法的名字(SEL)对应到另个IMP 。所以可以利用 method_exchangeImplementations 来交换2个方法中的IMP,因为根本目的就是交换方法之间的IMP

参考Mattt大神在NSHipster上的文章自己写的代码。

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 "UIViewController+swizzling.h"
#import @implementation UIViewController (swizzling)
//load方法会在类第一次加载的时候被调用
//调用的时间比较靠前,适合在这个方法里做方法交换
+ (void)load{
//方法交换应该被保证,在程序中只会执行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//获得viewController的生命周期方法的selector
SEL systemSel = @selector(viewWillAppear:);
//自己实现的将要被交换的方法的selector
SEL swizzSel = @selector(swiz_viewWillAppear:);
//两个方法的Method
Method systemMethod = class_getInstanceMethod([self class], systemSel);
Method swizzMethod = class_getInstanceMethod([self class], swizzSel);
//首先动态添加方法,实现是被交换的方法,返回值表示添加成功还是失败
BOOL isAdd = class_addMethod(self, systemSel, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));
if (isAdd) {
//如果成功,说明该类中不存在这个方法的实现,有可能是在父类中进行了实现
//将被交换方法的实现替换到这个并不存在的实现
class_replaceMethod(self, swizzSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
}else{
//否则,交换两个方法的实现
method_exchangeImplementations(systemMethod, swizzMethod);
}
});
}
- (void)swiz_viewWillAppear:(BOOL)animated{
//这时候调用自己,看起来像是死循环
//但是其实自己的实现已经被替换了
[self swiz_viewWillAppear:animated];
NSLog(@"swizzle");
}
@end

简单的使用方法调换就是这个样子,但是由此方法引发的一些思考

  1. 如果真的是想要调用swiz_viewWillAppear的时候怎么办,由于已经进行过了方法交换,我们永远不可能进行[self swiz_viewWillAppear]
    由于调方法的目的是发送消息,所以解决方法就是直接这样

    1
    objc_msgSend(self, @selector(swiz_viewWillAppear));
  2. 多个具有继承关系的类都进行了swizzle,会发生什么情况。

    多个有继承关系的类的对象swizzle时,先从父对象开始。 这样才能保证子类方法拿到父类中的被swizzle的实现。在+(void)load中swizzle不会出错,就是因为load类方法会默认从父类开始调用。

  3. 如果子类中是没有进行实现swizzle,但是父类中实现了?

    这就是class_addMethod的作用,要先尝试添加原 selector 是为了做一层保护,因为如果这个类没有实现systemSel,但其父类实现了,那class_getInstanceMethod会返回父类的方法。这样method_exchangeImplementations替换的是父类的那个方法,这当然不是你想要的。所以我们先尝试添加systemSel,如果已经存在,再用method_exchangeImplementations把原方法的实现跟新的方法实现给交换掉。

如果使用恰当,Method swizzling 还是很安全的。一个简单安全的方法是:仅在load中swizzle。谨慎使用。