一、Method Swizzling简介
Method Swizzling
被业内称为黑魔法、黑科技。字面意思是方法交换,其中交换的是方法的实现
。
具体点的来说,我们用@selector
(方法选择器) 取出来的是一个方法的编号(指向方法的指针) ,用SEL
类型表示;它所指向的是一个IMP
(方法实现的指针) ,而我们交换的就是这个IMP
,从而达到方法实现交换的效果。
-
@selector(方法名)
@selector()
方法名称的描述,只取类方法的编号不记录具体的方法,具体的方法是 IMP
,取出的结果是SEL
类型。
(1)编译时,通过编译器指令@selector
来获取.
//定义一个类方法的指针,selector查找是当前类(包含子类)的方法
SEL aSelector = @selector(methodName);
(2)运行时,通过字符串来获取一个方法名 NSSelectorFromString
SEL aSelector = NSSelectorFromString(@"methodName");
Objective-C
数据结构中,存在一个name - selector
的映射表如图:
方法以
selector
作为索引。selector
的数据类型是SEL
。虽然SEL
定义成char*
,我们可以把它想象成int
。每个方法的名字对应一个唯一的 int 值。比如, 方法addObject:
可能 对应的是12
。 当寻找该方法时,使用的是selector
,而不是名字@"addObject:"
在编译的时候,只要有方法的调用,编译器都会通过 selector
来查找,所以 (假设addObject
的 selector
为12
)
[myObject addObject: param];
将会编译变成:
objc_msgSend(myObject, 12, param);
这里,
objc_msgSend()
函数将会使用myObject
的isa
指针来找到myObject
的类空间结构,并在类空间结构中查找selector 12
所对应的方法。如果没有找到,那么将使用指向父类的指针找到父类空间结构进行selector 12
的查找。 如果仍然没有找到,就继续往父类的父类一 直找,直到找到为止, 如果到了根类NSObject
中仍然找不到,将会抛出异常。
-
IMP:(Implementation缩写)
一个函数指针,保存了方法地址。
(1)它是指向一个方法具体实现的指针,每一个方法都有一个对应的IMP
,所以,我们可以直接调用方法的IMP
指针,来避免方法调用死循环的问题。
(2)当你发起一个 ObjC
消息之后,最终它会执行的那段代码,就是由IMP
这个函数指针指向了这个方法实现的。
IMP
的声明为:
typedef id(*IMP)(id, SEL,...);
IMP
是一个函数指针,这个被指向的函数包含一个接收消息的对象id
(self
指针),调用方法的选标SEL
(方法名),以及不定个数的方法参数,并返回一个id
。也就是说IMP
是消息最终调用的执行代码,是方法真正的实现代码 。我们可以像在C
语言里面一样使用这个函数指针。
//Method 是一个类实例,里面的结构体有一个方法选标 SEL – 表示该方法的名称,一个types – 表示该方法参数的类型,一个 IMP - 指向该方法的具体实现的函数指针。
typedef struct objc_method *Method;
typedef struct objc_ method {
SEL method_name; //方法名
char *method_types; //方法类型
IMP method_imp; //具体方法实施的指针
};
//获取了这个实例方法类Mehtod
Method method = class_getInstanceMethod([self class], NSSelectorFromString(@"dealloc"));
//通过实例方法类获取对应的地址IMP
IMP classResumeIMP = method_getImplementation(method);
二、Method Swizzling原理
Method Swizzing
是发生在运行时的,主要用于在运行时将两个Method
进行交换,我们可以将Method Swizzling
代码写到任何地方,但是只有在这段Method Swilzzling
代码执行完毕之后互换才起作用。
- 每一个
OC
实例对象都保存有isa
指针和实例变量,其中isa
指针所属类,类维护一个运行时可接收的方法列表(MethodLists
);方法列表(MethodLists
)中保存selector
的方法名和方法实现(IMP
,指向Method
实现的指针)的映射关系。在运行时,通过selecter
找到匹配的IMP
,从而找到的具体的实现函数。
- 开发中可以利用
Objective-C
的动态特性,在运行时替换selector
对应的方法实现(IMP
),达到给hook
的目的。下图是利用Method Swizzling
来替换selector
对应IMP
后的方法列表示意图。
三、Method Swizzling的具体用途AOP(面向切面编程)
举个例子,比如说:在所有页面添加统计功能,也就是用户进入这个页面就统计一次。
方案:
(1) 手动添加
直接简单粗暴的在每个控制器中加入统计,复制、粘贴、复制、粘贴…
上面这种方法太Low了,消耗时间而且以后非常难以维护,会让后面的开发人员骂死的。(2)继承
我们可以使用继承的方式来解决这个问题。创建一个基类,在这个基类中添加统计方法,其他类都继承自这个基类。
然而,这种方式修改还是很大,而且定制性很差。以后有新人加入之后,都要嘱咐其继承自这个基类,所以这种方式并不可取。(3)
Category
我们可以为UIViewController
建一个Category
,然后在所有控制器中引入这个Category
。当然我们也可以添加一个PCH
文件,然后将这个Category
添加到PCH
文件中。(4)
Method Swizzling
我们可以使用苹果的“黑魔法”Method Swizzling
,Method Swizzling
本质上就是对IMP
和SEL
进行交换。
Method Swizzling的常见用途:
- 页面统计(
AOP
)、NSMutableArray
的insert
等插入nil
,hook
:給全局图片名称添加前缀,分类中为已有的属性或者方法添加钩子(增加一段代码)。 - 用于记录或者存储,比方说记录
ViewController
进入次数、Btn
的点击事件、ViewController
的停留时间等等。 可以通过Runtime
获取到具体ViewController
、Btn
信息,然后传给服务器。 - 添加需要而系统没提供的方法,比方说修改
Statusbar
颜色。用于轻量化、模块化处理。 - 用
Method Swizzle
动态给指定的方法添加代码,以解决Cross-cutting concern
的编程方式叫做Aspect Oriented Programming
,将逻辑处理和事件记录的代码解耦。 -
AOP
可以把琐碎的事务从主逻辑中分离出来,作为单独的模块,它是对面向对象编程模式的一种补充。 - 比较好的
AOP
库,封装了runtime
,Method Swizzling
这些黑科技,该库只有两个API
。
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
注意要点
swizzling
应该只在+load
中完成。swizzling
应该只在dispatch_once
中完成。由于swizzling
改变了全局的状态,所以我们需要确保每个预防措施在运行时都是可用的。原子操作就是这样一个用于确保代码只会被执行一次的预防措施,就算是在不同的线程中也能确保代码只执行一次。Grand Central Dispatch
的dispatch_once
满足了所需要的需求,并且应该被当做使用swizzling
的初始化单例方法的标准。尝试先调用
class_addMethod
方法,以保证即便originalSelector
只在父类中实现,也能达到Method Swizzling
的目的。xxx_viewWillAppear:
方法中[self xxx_viewWillAppear:animated];
代码并不会造成死循环,因为Method Swizzling
之后,调用[self xxx_viewWillAppear:animated];
实际执行的代码已经是原来viewWillAppear
中的代码了。
(1)
+load
的执行时机:+load
方法会在加载类的时候就被调用,也就是iOS
应用启动的时候,就会加载所有的类,main
函数之前,就会调用每个类的+load
方法(2) 子类的
+load
方法会在它的所有父类(不包括父类分类中的+load
)的+load
方法执行之后执行
分类的+load
方法会在所有的主类的+load
方法执行之后执行(3) 不同的类之间的
+load
方法的调用顺序是不确定的(4)
+ initialize:
方法类似一个懒加载,initialize
是在类或者其子类的第一个方法被调用前调用,且默认只加载一次;+ initialize
的调用发生在+init
方法之前。
+load
和 + initialize
差异比较
+(void)load | +(void)initialize | |
---|---|---|
执行时机 | 在main函数之前执行 | 在类的方法被第一次调用时执行 |
若自身未定义,是否沿用父类的方法? | 否 | 是 |
类别中的定义 | 全都执行,但后于类中的方法 | 覆盖类中的方法,只执行一次 |
swizzling代码如下
NSObject+Swizzling.m
#import "NSObject+Swizzling.h"
#import <objc/runtime.h>
@implementation NSObject (Swizzling)
/**
交换两个对象方法的实现
@param srcClass 被替换方法的类
@param srcSel 被替换的方法编号
@param swizzledSel 用于替换的方法编号
*/
+ (void)ff_swizzleInstanceMethodWithSrcClass:(Class)srcClass
srcSel:(SEL)srcSel
swizzledSel:(SEL)swizzledSel{
Method srcMethod = class_getInstanceMethod(srcClass, srcSel);
Method swizzledMethod = class_getInstanceMethod(srcClass, swizzledSel);
if (!srcClass || !srcMethod || !swizzledMethod) return;
//加一层保护措施,如果添加成功,则表示该方法不存在于本类,而是存在于父类中,不能交换父类的方法,否则父类的对象调用该方法会crash;添加失败则表示本类存在该方法
BOOL addMethod = class_addMethod(srcClass, srcSel, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (addMethod){
//添加方法实现IMP成功后,再将原有的实现替换到swizzledMethod方法上,从而实现方法的交换,并且未影响到父类方法的实现
class_replaceMethod(srcClass, swizzledSel, method_getImplementation(srcMethod), method_getTypeEncoding(srcMethod));
}else{
//添加失败,调用交互两个方法的实现
method_exchangeImplementations(srcMethod, swizzledMethod);
}
}
@end
交换例子
#import "UIViewController+swizzling.h"
#import <objc/runtime.h>
#import "NSObject+Swizzling.h"
@implementation UIViewController (swizzling)
+ (void)load {
[super load];
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// 原方法名和替换方法名
SEL originalSelector = @selector(viewDidAppear:);
SEL swizzledSelector = @selector(swizzle_viewDidAppear:);
[NSObject ff_swizzleInstanceMethodWithSrcClass:class srcSel:originalSelector swizzledSel:swizzledSelector];
});
}
/**
页面出现的时候会进入到这里实现,即使在子类重写了viewDidAppear:方法,
那么在调用[super viewDidAppear:animated]的时候还是会进入这里。
*/
- (void)swizzle_viewDidAppear:(BOOL)animated {
//cmd在Objective-C的方法中表示当前方法的selector,正如同self表示当前方法调用的对象实例一样。
NSLog(@"%@ --- %@ (IMP = UIViewController swizzle_viewDidAppear)",self, NSStringFromSelector(_cmd));
//这里面调用自己在method swizzle交换了方法实现后就不会出现循环了
//swizzle_viewDidAppear:方法的实现其实是调用UIViewController的viewDidAppear的原生实现方法
[self swizzle_viewDidAppear:animated];
}
@end
这里只是简单介绍了一下正确swizzle
的思路,还有很多不完善的地方,推荐大家去看RSSwizzle,swizzle
思路也是动态查找父类的实现,但它的实现方式十分优雅,非常值得一看。最后,Method swizzling
虽然能给我们带来很多便捷,但是调式困难,以及使用不当带来的麻烦也是难以排查,所以还是谨慎使用。