iOS Swizzle的正确使用方式(原文翻译)

文章翻译自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.

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值