Method Swizzling

利用iOS运行时提供的API,我们可以实现Method Swizzling功能,利用Method Swizzling,我们可以实现hook给系统或者第三方库添加自己特定的实现;在单元测试、AOP及DI编程中更是常见。但是在使用它的时候,我们要十分的谨慎小心,NSHipster的文章和StackOverFlow上的一篇问答也都说明了这一点。Method Swizzling的局限性主要包括:

  • (1)不是线程安全的
  • (2)改变了代码本来的行为
  • (3)da潜在的命名冲突
  • (4)改变了参数_cmd,如果方法中依赖了_cmd,则可能会有问题
  • (5)继承问题,如果swizzle的顺序没有按照父->子的顺序,可能会导致父类的swizzle失效,程序Crash,或者方法在执行过程中不是按照从子->父的顺序。
  • (6)难以理解和调试

下面分析几种常见的Method Swizzling的实现方法及各自局限性。

Method Swizzling 的实现方法及其局限性

不同的实现方法分别来自于UITableView-FDTemplateLayoutCellNSHipsterJRSwizzle,以及StackOverFlow,NSHipster和JRSwizzle的实现核心逻辑差不多,我们这里就以NSHipster的实现为例,以下我们简称FD的实现,NH的实现SO的实现;几种实现共同的问题都包含上面的1,2,6,这里就不再累述。

FD的实现

我们把代码提取出来是下面这个样子:

1
2
3
4
5
6
7
void MethodSwizzle(Class self, SEL origSel_, SEL altSel_){

Method origMethod = class_getInstanceMethod(self, origSel_);
Method altMethod = class_getInstanceMethod(self, altSel_);

method_exchangeImplementations(origMethod, altMethod);
}

它的问题主要有3,4,5,命名冲突和改变_cmd会导致潜在的隐患。这里主要说下继承问题。
假设这样一种场景,父类A中实现了foo方法,在扩展中的+load中进行了swizzle,swizzling方法是A_foo,子类B在+load中也对foo进行了swizzle,swizzling方法是B_foo,在向对象B发送消息的时候,我们期望的执行顺序是B_foo -> A_foo -> foo,但是实际的执行顺序却是A_foo -> B_foo -> foo,顺序与我们的期望的不符,不过还好三个方法全都执行了。顺序异常的原因是+load方法的的执行顺序是先执行类中的实现,再执行扩展中的实现,这一点可以通过运行时开源代码中的call_load_methods看到。

第二个问题是如果foo方法是dealloc方法,并且A类中没有实现dealloc方法,FD的实现则会在ARC下导致程序的Crash。原因可能是

NH的实现

1
2
3
4
5
6
7
8
9
10
void MethodSwizzle(Class self, SEL origSel_, SEL altSel_){

Method origMethod = class_getInstanceMethod(self, origSel_);
Method altMethod = class_getInstanceMethod(self, altSel_);

if(class_addMethod(self, origSel_, method_getImplementation(altMethod), method_getTypeEncoding(altMethod)))
class_replaceMethod(self, altSel_, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
else
method_exchangeImplementations(origMethod, altMethod);
}

同样存在潜在的命名冲突及改变参数_cmd的问题
在上面第一个场景中,实际的执行顺序是B_foo -> foo,由于先进行子类的swizzle,导致父类的swizzle失效.ReactiveCocoa也曾经出现过类似的问题,现在已经解决掉了。
在第二种情况下,当foo方法是dealloc方法,并且A类中没有实现的话,程序并不会Crash。

SO的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
IMP imp = NULL;
Method method = class_getInstanceMethod(class, original);
if (method) {
const char *type = method_getTypeEncoding(method);
imp = class_replaceMethod(class, original, replacement, type);
if (!imp) {
imp = method_getImplementation(method);
}

}
if (imp && store) { *store = imp; }
return (imp != NULL);
}

@implementation NSObject (FRRuntimeAdditions)
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
return class_swizzleMethodAndStore(self, original, replacement, store);
}
@end

在上面两个场景中,实际的执行顺序和NH的实现是一样的,但是SO的实现通过静态函数解决了命名冲突的问题,通过直接调用IMP函数指针,解决了改变_cmd参数的问题。
但是这种C的写法写起来太过麻烦,而且同样存在上面说过的三者共同的问题。

RSSwizzle

有没有一个能把上面的问题全部解决的方案呢?开源库RSSwizzle基本上可以把上面的问题全部解决掉。Github地址

总结

Method Swizzle的使用和写法都非常灵活,如果确定需要这个功能的话,尽可能的把潜在问题考虑周全一点,FD的实现虽然有潜在问题,但是用在其代码中是没有问题的;如果使用场景要求较高,可以直接使用RSSwizzle开源库。