文章翻译自https://blog.newrelic.com/engineering/right-way-to-swizzle/
感谢作者Bryce Buchanan
通常在运行时,Swizzle是通过用一个方法的实现来替换另一个方法的实现来运作的。运用Swizzle可能是因为不同的需求:重写默认方法,甚至是动态的方法加载。我曾经看到很多发出来的博客上讨论Swizzle,他们很多都提供了一些相当不好的用法。这些用法在你独自写项目的时候用起来无伤大雅,但是如果你在为一个第三方开发者提供framework的时候,Swizzle可能会让本应该运行顺利的部分出现混乱。所以,在OC中怎样才是Swizzle的正确使用方式呢?
让我们从基础说起,当我说起Swilling的时候通常是用我自定义的方法来替代原有的方法,然后在自定义的方法里调用原有的方法。OC在Runtime里是允许这样操作的。在运行时,OC的方法methods是以C语言的结构体形式出现的,一个被定义为struct objc_method的结构体:
struct objc_method {
SEL method_name
char *method_types
IMP method_imp
}
method_name就是当前调用方法的selector对应的名字,*method_types是c编码的字符串类型的参数和返回值,method_imp
是当前函数的指针(我们待会会讨论更对关于IMP的问题)。
你可以用下面的方法拿到这个对象(在OC运行时有很多拿到他们的渠道):
Method class_getClassMethod(Class aClass,SEL aSelector);
Method class_getInstanceMethod(Class aClass,SEL aSelector);
拿到Method就可以拿到Mehod内部的结构体从而改变他们内部的实现。method_imp是IMP类型,定义为 id(*IMP)(id,SEL,...),也是一个带有指针、selector和一串作为参数的带有编号变量的函数。用IMP method_setImplemention(Method method,IMP imp)可以改变method_imp,参数imp是Method结构体里面的,是方法的实现,method是你想要改变的方法,然后再返回跟method对应的原生IMP,这是Swizzle的正确用法。
Swizzle的不正确用法是什么?
下面是Swizzle的常用用法,当直接用一个方法来代替另一个方法实现的时候,会带来一些不易察觉的影响。
void method_exchangeImplementation(Method m1,MNethod m2)
为了弄清楚这些影响,让我们来看一下m1和m2在被调用前后的结构。
Method m1 { //这是原始的方法,我们想要把这个方法跟替换方法交换
SEL method_name = @selector(originalMethodName)
char *method_types = "v@:" //返回为空,参数为 id(self),selector(_cmd)
IMP method_imp = 0x000FFF
}
Method m2 { //这是进行交换的方法,我们想在原生方法调用的时候执行这个方法
SEL method_name = @selector(swizzle_originalMethodName)
char *method_types = "v@:" //返回为空,参数为 id(self),selector(_cmd)
IMP method_imp = 0x1234AABA
}
以上是方法未调用之前的结构,OC代码编译这些结构就是这样:
@implementation MyClass
- (void)originalMethodName //m1
{
//code
}
- (void)swizzle_originalMethodName //m2
{
//...code?
[self swizzle_originalMethodName]; //调用原生方法
//...code?
}
@end
然后我们调用:
m1 = class_getInstanceMethod([MyClass class],@selector(originalMethodName))
m2 = class_getInstanceMethod([MyClass class],@selector(swizzle_originalMethodName))
method_exchangeImplementation(m1,m2)
现在方法看起来将会是这样:
Method m1 { //这是原始的方法,我们想要把这个方法跟替换方法交换
SEL method_name = @selector(originalMethodName)
char *method_types = "v@:" //返回为空,参数为 id(self),selector(_cmd)
IMP method_imp = 0x1234AABA
}
Method m2 { //这是进行交换的方法,我们想在原生方法调用的时候执行这个方法
SEL method_name = @selector(swizzle_originalMethodName)
char *method_types = "v@:" //返回为空,参数为 id(self),selector(_cmd)
IMP method_imp = 0x000FFF
}
两个方法的IMP地址交换了一下,也就是说只改变了IMP。注意到如果我们想要执行原生方法我们得调用 [self swizzle_originalMethodName],如果原生方法依赖于_cmd 作为方法名,这将导致传给原生方法的_cmd的值变成@selector(swizzle_originalMethodName)。这种swizzle的方式(下面有例子)已经对正常的函数编码带来了混乱,是应该避免的。
- (void)originalMethodName //m1
{
assert([NSStringFromSelector(_cmd) isEqualToString:@"originalMethodName"]) //用method_exchangedImplementations() 进行swizzle将会失败
}
现在我们看一下用method_exchangedImplementations()函数进行swizzle的正确用法。
正确的swizzle方法
用C函数定义一个IMP方法来代替新创建的OC函数-(void)swizzle_originalMethodName
void _Swizzle_originalMethodName(id self,SEL _cmd)
{
//code
}
我们可以把这个C函数转换成一个IMP:
IMP swizzleImp = (IMP)_Swizzle_originalMethodName;
然后可以把swizzleImp传给method_setImplementation( ):
method_setImplemention(method ,swizzleImp);
上面这个方法返回的是原生的IMP:
IMP originalImp = method_setImplementation(method,swizzleImp);
现在,originalImp可以用来调用原生方法了:
originalImp(self,_cmd);
这里有一个例子:
@interface SwizzleExampleClass : NSObject
- (void) swizzleExample;
- (int) originalMethod;
@end
static IMP __original_Method_Imp;
int _replacement_Method(id self, SEL _cmd)
{
assert([NSStringFromSelector(_cmd) isEqualToString:@"originalMethod"]);
//code
int returnValue = ((int(*)(id,SEL))__original_Method_Imp)(self, _cmd);
return returnValue + 1;
}
@implementation SwizzleExampleClass
- (void) swizzleExample //call me to swizzle
{
Method m = class_getInstanceMethod([self class],
@selector(originalMethod));
__original_Method_Imp = method_setImplementation(m,
(IMP)_replacement_Method);
}
- (int) originalMethod
{
//code
assert([NSStringFromSelector(_cmd) isEqualToString:@"originalMethod"]);
return 1;
}
测试一下就能看出来:
SwizzleExampleClass* example = [[SwizzleExampleClass alloc] init];
int originalReturn = [example originalMethod];
[example swizzleExample];
int swizzledReturn = [example originalMethod];
assert(originalReturn == 1); //true
assert(swizzledReturn == 2); //true
总而言之,为了避免与其他第三方SDK造成混乱,不要用OC的方法和method_swapImplementations()来进行swizzle,而是把IMP转换成C函数。这将避免OC自身的方法带来的额外烦恼的信息(可以理解为OC的语言特性带来的问题:译者注),比如新的方法名。如果你想用Swizzle,最好的结果就是不留下痕迹。
不要忘记,所有的OC方法传递了两个隐藏的参数:对self的引用(id self),和方法名selector(SEL _cmd)。
如果IMP的调用返回值为空void,那你不得不注意了。因为ARC假定所有的IMP返回一个id,将会尝试引用一个空的和原始类型
IMP anImp; //represents objective-c function
// -UIViewController viewDidLoad;
((void(*)(id,SEL))anImp)(self,_cmd); //call with a cast to prevent
// ARC from retaining void.