原文链接:http://nshipster.com/method-swizzling/
方法调包(Method Swizzling)应用于改变某个SEL(该SEL已有实现)的方法实现。这个技术让OC中的方法调用在运行时通过改变SEL与类分发表中的函数映射关系从而来指定你真正想要调用的函数。
举个栗子,我们视图统计我们XX应用中的每个视图控制器被弹出来几次【用户行为统计。。一般都有。。。】。
我们可能需要把统计代码添加到viewDidAppear:
,这样会导致出现成吨的重复代码。继承可能是另外一个解决方案,但是需要各种各样的继承:UIViewController, UITableViewController, UINavigationController
,与此同时你仍然无法避免重复代码。
幸好我们有另外一种解决方案:类别中的方法调包(method swizzling)。代码如下:
#import <objc/runtime.h>
@implementation UIViewController (Tracking)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// When swizzling a class method, use the following:
// Class class = object_getClass((id)self);
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
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
现在,当任意UIViewController
的实例或者子类调用viewWillAppear:
时候便会打印一个LOG。
【注:用途】
往视图控制器的生命周期中注入行为,事件响应者,画图,或者加入网络层都是方法调包的用武之地。(当然还有许多使用场景,对于OC开发者来说也是装逼必备)。
继续来看应该怎么玩:
+load vs. +initialize
调包的处理应该一直在+load
中搞定。
有两个方法是在OC运行时自动被调用的。一个是+load
在每一个类在最初被加载的时候,而另外一个是+initialize
在应用第一次调用这个类或者这个实例的方法的时候。它们都是可选的,只有在被调用方法是已实现的时候被执行。
因为方法调包影响全局状态,所以最小化冲突的概率十分重要。+load
在类初始化期间是保证被加载的,在改变系统范围的行为而言提供了一点一致性。相反的是+initialize
在将被执行的时候没有这样的保障,事实上,它可能不被调用,如果应用不直接给这个类发消息的话。
dispatch_once
调包应该一直在dispatch_once
中进行处理
再次声明因为方法调包的影响是全局性的,我们需要在运行时采取能够想到的预防措施。原子性就是这样的一个预防,保证代码仅被执行一次,即便是跨线程。GCD的dispatch_once
提供了我们想要原子性与执行唯一性。
Selectors, Methods, & Implementations
在OC中,selectors
,methods
以及implementations
涉及到运行时中比较特殊的一块领域,尽管在大部分场景这些东西都可以互通,通常是指消息发送的过程。
引用下苹果官方说明:
- Selector (typedef struct objc_selector *SEL): Selectors are used to represent the name of a method at runtime. A method selector is a C
string that has been registered (or “mapped”) with the Objective-C
runtime. Selectors generated by the compiler are automatically mapped
by the runtime when the class is loaded .
- Method (typedef struct objc_method *Method): An opaque type that represents a method in a class definition.
- Implementation (typedef id (*IMP)(id, SEL, …)): This data type is a pointer to the start of the function that implements the method.
This function uses standard C calling conventions as implemented
for the current CPU architecture. The first argument is a pointer
to self (that is, the memory for the particular instance of this
class, or, for a class method, a pointer to the metaclass). The
second argument is the method selector. The method arguments
follow.
最好的理解这些概念的方式如下:
一个类(Class)维护着一张(方法)分发表来在运行时处理消息分发;表中的每个入口都是一个方法(Method),其映射从一个专有的名字,也就是选择器(SEL)到一个具体实现(IMP),也就是指向C函数的一个指针。
调包一个方法就是该笔爱你一个类得分发表也就是调包方式改变SEL与IMP的映射关系。
【ASEL -> AIMP BSEL -> BIMP 调包后 ASEL -> BIMP BSEL -> AIMP】
调用_cmd
下列代码似乎会导致死循环:
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", NSStringFromClass([self class]));
}
但是好神奇啊,居然不会耶。在调包的处理中,xxx_viewWillAppear:
被重新分配到UIViewController -viewWillAppear:
的原始实现。递归调用是会问题的,但是在这个场景是没问题的哦,因为在运行时的时候xxx_viewWillAppear:
真正的实现已经被调包了。(意译)
这里有个代码规范,记得在调包方法名字中加前缀,同理在其他类别中。
思索
调包一直被认作为一种巫术技术(黑魔法),容易导致不可预料的行为和结果,尽管不是最安全的,但是如果你遵循如下法则,安全性还是可以接受的:
- Always invoke the original implementation of a method (unless you have a good reason not to)
- Avoid collisions
- Understand what’s going on
- Proceed with caution
总结是Method Swizzling好用但要少用。。。。