重新认识 Method Swizzling
Tags
iOS
Date
Apr 7, 2021
 
notion image

method_exchangeImplementations

方法交换的注意点:
  1. 如果是类簇,类名要正确
  1. 如果是类方法,要使用object_getClass获得元类
  1. 如果子类方法交换时该方法仅存在于父类,因为class_getInstanceMethod会搜索superclass,直接执行method_exchangeImplementations会使得父类的方法会替换,不符合预期,甚至会crash。
  1. 偶数次执行相同的method_exchangeImplementations导致交换无效,如需保证仅执行一次则使用dispatch_once
  1. 同样,可以使用method_exchangeImplementations恢复方法交换。
  1. 如果有多次不同的selector执行了method_exchangeImplementations,则实际调用顺序与swizzle的顺序相反。
  1. 如果swizzledMethod写在分类里,请使用特定的前缀保证不会导致分类方法覆盖。
  1. 如果originalMethodswizzledMethod不在同一个originalClass中,则需要先通过class_addMethodswizzledMethod加到originalClass中。
  1. 如非直接替换,则在swizzledMethod中应调用原生方法originalMethod的实现。由于方法已替换,所以代码上调用的是swizzledSelector
  1. 方法交换适用于类方法或者所有实例的实例方法,如需仅对指定实例生效,可使用Aspect
  1. 方法交换由于selector不同,会导致_cmd不同。

class_addMethod与class_replaceMethod

关键代码如下。
BOOL didAddMethod =
    class_addMethod(class,
        originalSelector,
        method_getImplementation(swizzledMethod),
        method_getTypeEncoding(swizzledMethod));

if (didAddMethod) {
    class_replaceMethod(class,
        swizzledSelector,
        method_getImplementation(originalMethod),
        method_getTypeEncoding(originalMethod));
} else {
    method_exchangeImplementations(originalMethod, swizzledMethod);
}
当子类方法交换时该方法仅存在于父类时, class_addMethod成功,didAddMethodYES,则调用class_replaceMethod,而不是method_exchangeImplementations,避免父类被污染。

JRSwizzle

JRSwizzle主要实现了在Mac OS X, iOS, Objective-C不同平台不同版本不同架构的方法交换。
而对于目前主流的iOS的objc2,关键代码如下。
class_addMethod(self,
				origSel_,
				class_getMethodImplementation(self, origSel_),
				method_getTypeEncoding(origMethod));
class_addMethod(self,
				altSel_,
				class_getMethodImplementation(self, altSel_),
				method_getTypeEncoding(altMethod));

method_exchangeImplementations(class_getInstanceMethod(self, origSel_), class_getInstanceMethod(self, altSel_));
通过先对目标类添加一次原方法和替换方法,避免父类被污染的问题。

IMPPointer

为了避免使用method_exchangeImplementations出现的命名冲突和_cmd变化的问题,可以使用IMPIMP指针,而不是使用新的selector
typedef IMP *IMPPointer;

static void MethodSwizzle(id self, SEL _cmd, id arg1);
static void (*MethodOriginal)(id self, SEL _cmd, id arg1);

static void MethodSwizzle(id self, SEL _cmd, id arg1) {
    // do custom work
    MethodOriginal(self, _cmd, arg1);
}

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);
}
 

RSSwizzle

RSSwizzle提出了method_exchangeImplementations的几个问题
  1. 线程不安全
  1. swizzle的时候如果方法不存在,从父类复制实现到子类。会导致父类的方法替换无效。
  1. 方法交换导致_cmd变化
  1. 替换方法的selector存在命名冲突的可能
 
开发者头条
为什么有这篇博文 不知道何时开始iOS面试开始流行起来询问什么是 Runtime,于是 iOSer 一听 Runtime 总是就提起 MethodSwizzling,开口闭口就是黑科技。但其实如果读者留意过C语言的 Hook 原理其实会发现所谓的钩子都是框架或者语言的设计者预留给我们的工具,而不是什么黑科技,MethodSwizzling 其实只是一个简单而有趣的机制罢了。然而就是这样的机制,在日常中却总能成为万能药一般的被肆无忌惮的使用。 很多 iOS 项目初期架构设计的不够健壮,后期可扩展性差。于是 iOSer 想起了 MethodSwizzling 这个武器,将项目中一个正常的方法 hook 的满天飞,导致项目的质量变得难以控制。曾经我也爱在项目中滥用 MethodSwizzling,但在踩到坑之前总是不能意识到这种糟糕的做法会让项目陷入怎样的险境。于是我才明白学习某个机制要去深入的理解机制的设计,而不是跟风滥用,带来糟糕的后果。最后就有了这篇文章。 在 iOS 平台常见的 hook 的对象一般有两种: 对于 C/C+ +的 hook 常见的方式可以使用 facebook 的fishhook框架,具体原理可以参考深入理解Mac OS X & iOS 操作系统这本书。 对于 Objective-C Methods 可能大家更熟悉一点,本文也只讨论这个。 相信很多人使用过 JRSwizzle 这个库,或者是看过 http://nshipster.cn/method-swizzling/ 的博文。 上述的代码简化如下。 + (BOOL)jr_swizzleMethod:(SEL)origSel_
 
RSSwizzle的实现只用到class_replaceMethod,并没有用method_exchangeImplementations
The previous implementation of the method identified by name for the class identified by cls.
由于class_replaceMethod的返回值是替换前的方法,如果是NULL,则表明该子类本身没有实现该方法,则在实际运行调用原方法时直接调用super的方法。使得既不污染父类,也不改变子类调用父类的默认行为。如果父类也有方法替换,也可以同时生效。
if (NULL == imp){
    // If the class does not implement the method
    // we need to find an implementation in one of the superclasses.
    Class superclass = class_getSuperclass(classToSwizzle);
    imp = method_getImplementation(class_getInstanceMethod(superclass,selector));
}
整个过程只用了block和IMP的操作,没有新增SEL,所以不存在方法命名冲突和_cmd变化的问题。
缺点是需要自己额外声明方法签名(参数个数、参数类型和返回值类型),比较繁琐。
 

Loading Comments...