Method Swizzling(转载+补充)

Method Swizzling

参考文章: http://nshipster.cn/method-swizzling/

Method swizzling 用于改变一个已经存在的 selector 的实现或者增加一个selector 。通过这一技术,我们可以在运行时通过修改类的分发表中selector对应的函数,来修改方法的实现。

#import <objc/runtime.h>

@implementation UIViewController (Tracking)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        //如果要是替换一个方法, 先判断是否存在该方法
        if (![class instancesRespondToSelector:originalSelector])
        {
            NSLog(@"该类中没有这个方法");
            return;
        }
        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);
        // Method originalMethod = class_getClassMethod(class, originalSelector);
        // Method swizzledMethod = class_getClassMethod(class, swizzledSelector);

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

#pragma mark - Method Swizzling

- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
}

@end

当你调用@selector(viewWillAppear)方法时, 就会被替换成@selector(xxx_viewWillAppear), 这样UIViewController 或其子类的实例对象在调用 viewWillAppear: 的时候会有 log 的输出

这种黑科技常用来track视图控制器的生命周期,响应事件,绘制视图或者 Foundation 框架的网络栈等方法

如何使用 method swizzling

swizzling应该只在+load中完成。

在 Objective-C 的运行时中,每个类有两个方法都会自动调用。
- +load 是在一个类被初始装载时调用
- +initialize 是在应用第一次调用该类的类方法或实例方法前调用的。

两个方法都是可选的,并且只有在方法被实现的情况下才会被调用。

swizzling 应该只在 dispatch_once 中完成

由于 swizzling 改变了全局的状态,所以我们需要确保每个预防措施在运行时都是可用的。原子操作就是这样一个用于确保代码只会被执行一次的预防措施,就算是在不同的线程中也能确保代码只执行一次。Grand Central Dispatch 的 dispatch_once 满足了所需要的需求,并且应该被当做使用 swizzling 的初始化单例方法的标准。

调用 _cmd

- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
}

在正常的使用中或许会导致无限循环, 在swizzling的过程中,方法中的[self xxx_viewWillAppear:animated]已经被重新指定到UIViewController类的-viewWillAppear:中。在这种情况下,不会产生无限循环。不过如果我们调用的是[self viewWillAppear:animated],则会产生无限循环,因为这个方法的实现在运行时已经被重新指定为xxx_viewWillAppear:了。

Selectors, Methods, & Implementations

  • Selector(typedef struct objc_selector *SEL):在运行时 Selectors 用来代表一个方法的名字。Selector 是一个在运行时被注册(或映射)的C类型字符串。Selector由编译器产生并且在当类被加载进内存时由运行时自动进行名字和实现的映射。
  • Method(typedef struct objc_method *Method):方法是一个不透明的用来代表一个方法的定义的类型。
  • Implementation(typedef id (*IMP)(id, SEL,...)):这个数据类型指向一个方法的实现的最开始的地方。该方法为当前CPU架构使用标准的C方法调用来实现。该方法的第一个参数指向调用方法的自身(即内存中类的实例对象,若是调用类方法,该指针则是指向元类对象metaclass)。第二个参数是这个方法的名字selector,该方法的真正参数紧随其后。

理解 selector, method, implementation 这三个概念之间关系的最好方式是:

在运行时,类(Class)维护了一个消息分发列表来解决消息的正确发送。每一个消息列表的入口是一个方法(Method),这个方法映射了一对键值对,其中键是这个方法的名字 selector(SEL),值是指向这个方法实现的函数指针 implementation(IMP)。 Method swizzling 修改了类的消息分发列表使得已经存在的 selector 映射了另一个实现 implementation,同时重命名了原生方法的实现为一个新的 selector。

注意:
很多人认为交换方法实现会带来无法预料的结果。然而采取了以下预防措施后, method swizzling 会变得很可靠:

  • 在交换方法实现后记得要调用原生方法的实现(除非你非常确定可以不用调用原生方法的实现):APIs 提供了输入输出的规则,而在输入输出中间的方法实现就是一个看不见的黑盒。交换了方法实现并且一些回调方法不会调用原生方法的实现这可能会造成底层实现的崩溃。
  • 避免冲突:为分类的方法加前缀,一定要确保调用了原生方法的所有地方不会因为你交换了方法的实现而出现意想不到的结果。
  • 理解实现原理:只是简单的拷贝粘贴交换方法实现的代码而不去理解实现原理不仅会让 App 很脆弱,并且浪费了学习 Objective-C 运行时的机会。阅读 Objective-C Runtime Reference 并且浏览 能够让你更好理解实现原理。
  • 持续的预防:不管你对你理解 swlzzling 框架,UIKit 或者其他内嵌框架有多自信,一定要记住所有东西在下一个发行版本都可能变得不再好使。做好准备,在使用这个黑魔法中走得更远,不要让程序反而出现不可思议的行为。

补充:
1. 获取class_getInstanceMethod实例方法, 获取class_getClassMethod类方法
2. class_addMethod

/** 
 * 在运行时为类添加 已经实现好的 方法
 * 
 * @param cls  需要添加新方法的类
 * @param name 可以理解为方法名,这个貌似随便起名,比如我们这里叫sayHello2
 * @param imp 新实现这个方法的函数, 可通过`method_getImplementation(swizzledMethod)`获得. Obj-C的方法(method)就是一个至少需要两个参数(self,_cmd)的C函数
 * @param types 一个定义该函数返回值类型和参数类型的字符串, 可通过method_getTypeEncoding(swizzledDeallocMethod) 获得, 关于Type Encodings的其他类型定义请参考[]官方文档] (https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html)
 * 
 * @return 如果这个方法申请成功返回YES, 否则NO,
 *  比如: 这个类已经存在相同名字的方法, 则返回NO, 添加失败
 *
 */
OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp, 
                                 const char *types) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

注意: class_addMethod 不能添加该类中已存在的方法, 但是能覆盖父类的方法, 想要改变已存在的方法则使用 `method_setImplementation`
  1. class_replaceMethod
/** 
 * 在运行时替换类中方法
 * 
 * @param cls 需要替换新方法的类
 * @param name 用于替换的方法选择器(新方法)
 * @param imp 原始方法的函数, 可通过`method_getImplementation(originalMethod)`获得. Obj-C的方法(method)就是一个至少需要两个参数(self,_cmd)的C函数
 * @param types 个定义该函数返回值类型和参数类型的字符串, 可通过method_getTypeEncoding(originalMethod) 获得, 关于Type Encodings的其他类型定义请参考[]官方文档] (https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html)
 * @return The previous implementation of the method identified by \e name for the class identified by \e cls.
 * 
 * @note This function behaves in two different ways:
 *  - If the method identified by \e name does not yet exist, it is added as if \c class_addMethod were called. 
 *    The type encoding specified by \e types is used as given.
 *  - If the method identified by \e name does exist, its \c IMP is replaced as if \c method_setImplementation were called.
 *    The type encoding specified by \e types is ignored.
 */
OBJC_EXPORT IMP class_replaceMethod(Class cls, SEL name, IMP imp, 
                                    const char *types) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
  1. 其他一些相关函数
1.SEL method_getName(Method m) 由Method得到SEL
2.IMP method_getImplementation(Method m)Method得到IMP函数指针
3.const char *method_getTypeEncoding(Method m)Method得到类型编码信息
4.unsigned int method_getNumberOfArguments(Method m)获取参数个数
5.char *method_copyReturnType(Method m)  得到返回值类型名称
6.IMP method_setImplementation(Method m, IMP imp)  为该方法设置一个新的实现
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值