iOS Method Swizzling 注意事项

149 阅读5分钟

runtime中的交换方法method_exchangeImplementations存在的问题

  Method method1 = class_getInstanceMethod(self, @selector(eat));
  Method method2 = class_getInstanceMethod(self, @selector(anotherEat));
  method_exchangeImplementations(method1, method2);

先看下上面这段简单的方法替换代码。

方法替换到底做了什么?

上面的代码首先是通过方法名获取了两个方法,然后将两个方法的实现替换了。

方法的底层结构

  struct method_t {
SEL method_name 
IMP method_imp  
char * types  
}  

我们知道在类对象和元类对象中都有一个方法列表,分别存储着实例方法和类方法,存储着的方法底层结构如上面代码所示,它是一个结构体,在结构体中有三个参数,他们的含义分别是: method_name:方法的名字
method_imp:保存着一个指针,指向了函数的具体实现。实际方法替换真正替换的是method_imp即方法的具体实现。
types:能代表这个方法的字符,用法可以忽略。

直接使用method_exchangeImplementations方法的问题

我们再看看直接使用method_exchangeImplementations方法进行方法的替换有什么问题呢?
假设有一个Father类,有一个继承自Father类的Son类,Father类中有一个eat方法,Son类中没有重写eat方法,现在在Son的一个分类中对Son的eat方法进行替换。
看代码:

  + (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    
    Method method1 = class_getInstanceMethod(self, @selector(eat));
    Method method2 = class_getInstanceMethod(self, @selector(anotherEat));
    method_exchangeImplementations(method1, method2);
    
});
}

- (void)anotherEat{
  NSLog(@"self:%@", self);
  [self anotherEat];
  NSLog(@"替换之后的吃的方法...");
}

创建Father、Son的实例并调用实例方法

Son * s = [Son new];
[s eat];

Father * f = [Father new];
[f eat];

打印:

 runtime的两个方法替换[18967:1520161] self:<Son: 0x281749590>
 runtime的两个方法替换[18967:1520161] 爸爸吃东西...
 runtime的两个方法替换[18967:1520161] 替换之后的吃的方法...
 runtime的两个方法替换[18967:1520161] self:<Father: 0x2817491e0>
 runtime的两个方法替换[18967:1520161] -[Father anotherEat]: unrecognized selector sent to instance 0x2817491e0

程序发生了崩溃,报的错是在Father类中找不到anotherEat方法。Son实例调用eat方法是完全没有问题的。我们先分析一下Son。

分析Son

当在Son的分类中替换了Son的eat和anotherEat两个方法的实现后,Son实例调用eat实际调用的是anotherEat方法的具体实现,调用anotherEat实际调用的是eat方法的具体实现。anotherEat方法名在Son的类对象列表中能找到,所以是没问题的。
再来分析Father

分析Father

Father类中的eat方法的具体实现(imp)被替换成了anotherEat方法的具体实现,当调用Father实例的eat方法时就会走anotherEat方法的具体实现,如果anotherEat方法中不调用anotherEat方法也是没有问题的,但如果调用了anotherEat(如放数组越界中还是要调用系统的方法的),由于Father类对象中的方法列表中并没有名字为anotherEat的这个方法,所以报了找不到这个方法的错误。
盗用别人的一个图:

image.png

替换了父类某个方法的具体实现,但并没有改变该方法的方法名,从而可能导致崩溃。

那么我们要想替换两个方法的实现同时避免出现以上问题该怎么办呢?这个时候就需要我们利用runtime的动态解析即动态添加方法。

既然直接使用method_exchangeImplementations有问题,那么该怎么解决这个问题呢?我们可以同时使用class_replaceMethod来弥补method_exchangeImplementations存在的问题
看代码:

 + (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    
    Method method1 = class_getInstanceMethod(self, @selector(eat));
    Method method2 = class_getInstanceMethod(self, @selector(anotherEat));
    
    BOOL didAddMethod = class_addMethod(self, @selector(eat), method_getImplementation(method2), method_getTypeEncoding(method2));
    if (didAddMethod) {
        class_replaceMethod(self,
        @selector(anotherEat),
        method_getImplementation(method1),
        method_getTypeEncoding(method1));
    }else{
        method_exchangeImplementations(method1, method2);
    }
    
});
}

class_addMethod是runtime中的动态添加方法,如果能够添加成功说明本类并没有实现这个要添加的方法,如果不能添加成功说明本类实现了这个方法。实现方法包括新增的方法和重写父类方法。
如果重写了父类的方法那么只是将Son中的eat方法和anotherEat的具体实现进行了替换并不会替换父类的eat方法的
具体实现,那么直接使用method_exchangeImplementations是完全没有问题的。
我们来分析下class_replaceMethod方法

class_replaceMethod

  class_replaceMethod(self,
    @selector(anotherEat),
    method_getImplementation(method1),
    method_getTypeEncoding(method1));

这块代码是将本类Son的anotherEat具体实现替换成了eat的具体实现,而并没有改变Father类中eat方法的具体实现。且已经在本类Son中动态添加了方法名为eat的方法,eat方法的具体实现是方法名为anotherEat的具体实现。
看打印:

 runtime的两个方法替换[19134:1529903] self:<Son: 0x282b69240>
 runtime的两个方法替换[19134:1529903] 爸爸吃东西...
 runtime的两个方法替换[19134:1529903] 替换之后的吃的方法...
 runtime的两个方法替换[19134:1529903] 爸爸吃东西...

使用了class_addMethod和class_replaceMethod能保证子类的分类替换方法并不会替换掉父类的方法的具体实现。
如果我们确保本类中要被替换掉方法不在父类中或者子类重写了要被替换掉的方法,那么我们可以直接使用method_exchangeImplementations。

方法替换的应用

方法替换在开发中的应用主要是用来拦截系统的方法,如防止数组、字典越界造成的崩溃。

为什么要使用dispatch_onece呢?

load方法虽然在didFinishLaunch方法之前只被调用了一次,但为了防止再手动调用load方法让两个方法再替换回去,所以就使用了dispatch_onece

简单一句话:

只替换本类中的的某个方法的具体实现,不去替换父类中某个方法的具体实现,避免可能导致的崩溃。

文章参考:《runtime中的交换方法class_replaceMethod》