Objective-C中的Method Swizzling
了解Method Swizzling前应该先来了解一下Runtime与方法相关的一些内容。
1.基础数据类型
SEL
SEL,选择器,是表示一个方法的selector的指针,定义如下:
typedef struct objc_selector *SEL;
方法的selector用于表示运行时方法的名字。Objective-C在编译时,会根据每一个方法的名字,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL。
SEL reloadSel = @selector(reloadData);
SEL的本质是一个指向方法的指针(是根据方法名hash化了的KEY值,能唯一代表一个方法)。两个方法只要方法名相同,那么方法的SEL就是一样的。每一个方法都对应着一个SEL。同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也不行。不同的类可以拥有相同的selector,不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP。
IMP
IMP是一个函数指针,指向方法实现的首地址。定义如下:
id (*IMP)(id, SEL, ...)
第一个参数是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),第二个参数是方法选择器(selector),后面是方法的实际参数列表。
SEL就是为了查找方法的最终实现IMP的。由于每个方法对应唯一的SEL,因此我们可以通过SEL方便快速准确地获得它所对应的 IMP。取得IMP后,就获得了执行这个方法代码的入口点,然后就可以像调用普通的C语言函数一样来使用这个函数指针了。
通过取得IMP,我们可以跳过Runtime的消息传递机制,直接执行IMP指向的函数实现,这样省去了Runtime消息传递过程中所做的一系列查找操作,会比直接向对象发送消息高效一些。
Method
Method用于表示类定义中的方法,定义如下:
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; // 方法实现
}
这个结构体中包含一个SEL和IMP,相当于在SEL和IMP之间作了一个映射。有了SEL,就可以找到对应的IMP,从而调用方法的实现代码。
2.方法相关操作函数
Runtime提供了一系列的方法来处理与方法相关的操作。包括方法本身及SEL。
// 调用指定方法的实现
id method_invoke ( id receiver, Method m, ... );
// 调用返回一个数据结构的方法的实现
void method_invoke_stret ( id receiver, Method m, ... );
// 获取方法名
SEL method_getName ( Method m );
// 返回方法的实现
IMP method_getImplementation ( Method m );
// 获取描述方法参数和返回值类型的字符串
const char * method_getTypeEncoding ( Method m );
// 获取方法的返回值类型的字符串
char * method_copyReturnType ( Method m );
// 获取方法的指定位置参数的类型字符串
char * method_copyArgumentType ( Method m, unsigned int index );
// 通过引用返回方法的返回值类型字符串
void method_getReturnType ( Method m, char *dst, size_t dst_len );
// 返回方法的参数的个数
unsigned int method_getNumberOfArguments ( Method m );
// 通过引用返回方法指定位置参数的类型字符串
void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );
// 返回指定方法的方法描述结构体
struct objc_method_description * method_getDescription ( Method m );
// 设置方法的实现
IMP method_setImplementation ( Method m, IMP imp );
// 交换两个方法的实现
void method_exchangeImplementations ( Method m1, Method m2 );
接下来真正来看Method Swizzling的内容。
3.Method Swizzling
Method Swizzling是改变一个selector的实际实现的技术。通过它,我们可以在运行时通过修改类的分发表中selector对应的函数,来修改方法的实现。
当我们需要在整个工程中修改系统某个类某个方法的实现时,一种方法是创建一个子类重写这个方法,然后其他类都继承这个类,不过这种方法在工程开始之初还可以,但是如果是在一个已有工程的基础上新加需求,如果修改全部的类都继承这个类,改动量就比较大了。这时候就可以用Method Swizzling来实现。
举个例子:
@implementation UIViewController (LoadView)
+ (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(viewDidLoad:);
SEL swizzledSelector = @selector(s_viewDidLoad:);
Method original_ViewDidLoad = class_getInstanceMethod(class, originalSelector);
Method swizzled_ViewDidLoad = class_getInstanceMethod(class, swizzledSelector);
method_exchangeImplementations(original_ViewDidLoad, swizzled_ViewDidLoad);
});
}
- (void)s_viewDidLoad {
[self initLoadView];
[self s_viewDidLoad];
}
- (void)initLoadView {
// add load view
}
@end
在上面的例子中,我们通过method swizzling修改了UIViewController的@selector(viewDidLoad:)对应的函数指针,让它的实现指向了我们自定义的s_viewDidLoad的实现。这样,当UIViewController及其子类的对象调用viewDidLoad时,都会执行initLoadView来添加load view。
通过上面的例子,可以总结出几个method swizzling的用法和注意事项:
1. method swizzling要在+load中执行
由于method swizzling会影响到类的全局状态,而+load能保证在类的初始化过程中被加载,且能够保证这种改变应用级别的行为的一致性,所以要在+load中执行。
2. method swizzling要在dispatch_once中执行
同样由于method swizzling会影响到类的全局状态,所以需要在运行时采取一些预防措施。原子性就是这样一种措施,可以确保代码只被执行一次,不管有多少个线程。GCD的dispatch_once完全可以确保这种行为。
3. 为了避免冲突,要给自定义的方法加前缀,从而可以避免和所依赖的代码库出现命名冲突。
4. 无限循环,上面的例子中,在s_viewDidLoad中调用了[self s_viewDidLoad:animated],咋看上去会导致死循环。但是,在swizzling的过程中,s_viewDidLoad已经被重定向UIViewController类的viewDidLoad:中,不会产生无限循环。但是如果我们调用的是[self viewDidLoad:animated],则会产生无限循环,因为这个方法的实现在运行时已经被重新指定为s_viewDidLoad了。