美文网首页
每日一问11——Method Swizzling

每日一问11——Method Swizzling

作者: 巫师学徒 | 来源:发表于2017-09-12 17:20 被阅读34次

Method Swizzling——方法欺骗。Method Swizzling是改变一个selector的实际实现的技术。通过这一技术,我们可以在运行时通过修改类的分发表中selector对应的函数,来修改方法的实现。

应用场景

例如,我们想跟踪在程序中每一个view controller展示给用户的次数:当然,我们可以在每个view controller的viewDidAppear中添加跟踪代码;但是这太过麻烦,需要在每个view controller中写重复的代码。创建一个子类可能是一种实现方式,但需要同时创建UIViewController, UITableViewController, UINavigationController及其它UIKit中view controller的子类,这同样会产生许多重复的代码。

实现示例
@implementation UIViewController (Test)

+(void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(my_viewWillAppear:);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        BOOL didAddMethod = class_addMethod(class, swizzledSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        
        if(didAddMethod) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        }
        else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)my_viewWillAppear:(BOOL)animated {
    [self my_viewWillAppear:animated];
    NSLog(@"调用了自己的方法");
}

@end

上面的代码将我们的方法my_viewWillAppear与viewWillAppear方法对应的指针函数交换了。


swizzling2.jpeg

所以,当调用viewWillAppear时会调用我们自己定义的方法。我们可以在一处地方注入我们新的操作而不用写得到处都是。

Swizzling应该总是在+load中执行

在Objective-C中,运行时会自动调用每个类的两个方法。+load
会在类初始加载时调用,+initialize会在第一次调用类的类方法或实例方法之前被调用。这两个方法是可选的,且只有在实现了它们时才会被调用。由于method swizzling会影响到类的全局状态,因此要尽量避免在并发处理中出现竞争的情况。+load能保证在类的初始化过程中被加载,并保证这种改变应用级别的行为的一致性。相比之下,+initialize在其执行时不提供这种保证–事实上,如果在应用中没为给这个类发送消息,则它可能永远不会被调用。

Swizzling应该总是在dispatch_once中执行

与上面相同,因为swizzling会改变全局状态,所以我们需要在运行时采取一些预防措施。原子性就是这样一种措施,它确保代码只被执行一次,不管有多少个线程。GCD的dispatch_once可以确保这种行为,我们应该将其作为method swizzling的最佳实践。

调用_cmd
- (void)my_viewWillAppear:(BOOL)animated {
    [self my_viewWillAppear:animated];
    NSLog(@"调用了自己的方法");
}

咋看上去是会导致无限循环的。但令人惊奇的是,并没有出现这种情况。在swizzling的过程中,方法中的[self xxx_viewWillAppear:animated]已经被重新指定到UIViewController类的-viewWillAppear:中。在这种情况下,不会产生无限循环。不过如果我们调用的是[self viewWillAppear:animated],则会产生无限循环,因为这个方法的实现在运行时已经被重新指定为xxx_viewWillAppear:了。

class_replaceMethod与method_exchangeImplementations的区别。

Objective-C 提供了以下 API 来动态替换类方法或实例方法的实现:

  • class_replaceMethod 替换类方法的定义
  • method_exchangeImplementations 交换 2 个方法的实现
  • method_setImplementation 设置 1 个方法的实现

class_replaceMethod在苹果的文档(如下图所示)中能看到,它有两种不同的行为。当类中没有想替换的原方法时,该方法会调用class_addMethod来为该类增加一个新方法,也因为如此,class_replaceMethod在调用时需要传入types参数

method_exchangeImplementations 的内部实现相当于调用了 2 次method_setImplementation方法

例子优化版本
+(void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(my_viewWillAppear:);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        if(!originalMethod || !swizzledMethod) {
            return ;
        }
        
        IMP originalIMP = method_getImplementation(originalMethod);
        IMP swizzledIMP = method_getImplementation(swizzledMethod);
        const char* originalType = method_getTypeEncoding(originalMethod);
        const char* swizzledType = method_getTypeEncoding(swizzledMethod);
        
        class_replaceMethod(class, swizzledSelector, originalIMP, originalType);
        class_replaceMethod(class, originalSelector, swizzledIMP, swizzledType);
    });
}

因为class_replaceMethod方法其实能够覆盖到class_addMethodmethod_setImplementation两种场景, 对于第一个class_replaceMethod来说, 如果viewWillAppear:实现在父类, 则执行class_addMethod, 否则就执行method_setImplementation将原方法的IMP指定新的代码块; 而第二个class_replaceMethod完成的工作便只是将新方法的IMP指向原来的代码.

其他姿势的Method Swizzling
类方法的Method Swizzling
+(void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        SEL originalSelector = @selector(dictionary);
        SEL swizzlidSelector = @selector(my_dictionary);
        
        Method originalMethod = class_getClassMethod(class, originalSelector);
        Method swizzlidMethod = class_getClassMethod(class, swizzlidSelector);
        if(!originalMethod || !swizzlidMethod) {
            return ;
        }
        
        IMP originalIMP = method_getImplementation(originalMethod);
        IMP swizzlidIMP = method_getImplementation(swizzlidMethod);
        const char* originalType = method_getTypeEncoding(originalMethod);
        const char* swizzlidType = method_getTypeEncoding(swizzlidMethod);
        
        Class metaClass = objc_getMetaClass(class_getName(class));
        class_replaceMethod(metaClass, swizzlidSelector, originalIMP, originalType);
        class_replaceMethod(metaClass, originalSelector, swizzlidIMP, swizzlidType);
    });
}

+ (NSDictionary *)my_dictionary {
    id result = [self my_dictionary];
    NSLog(@"调用了自己的dic");
    return result;
}

其实基本没有什么区别,区别只是在获取Method调用的方法是class_getClassMethod ,还有就是最后class_replaceMethod时,第一个参数传入的是元类。这么做的主要原因是类方法的methodList存储的位置在元类中。

类簇中的Method Swizzling

关于类簇可以参见这篇博客。

在上面的代码中我们实现了对NSDictionary中的+ (id)dictionary方法的交换,但如果我们用类似代码尝试对- (id)objectForKey:(id)key方法进行交换后, 你便会发现这似乎并没有什么用.

id obj1 = [NSArray alloc]; // __NSPlacehodlerArray *
id obj2 = [NSMutableArray alloc];  // __NSPlacehodlerArray *
id obj3 = [obj1 init];  // __NSArrayI *
id obj4 = [obj2 init];  // __NSArrayM *

原因就是alloc以后产生的并不是我们想的那个类,而是一个中间类,我们的实例方法是存储在中间类的方法列表里。所以针对类簇的Method Swizzling问题就转变为如何对这些类簇中的私有类做Method Swizzling

+ (void)load {
    Class originalClass = NSClassFromString(@"__NSDictionaryM");
    Class swizzledClass = [self class];
    SEL originalSelector = @selector(setObject:forKey:);
    SEL swizzledSelector = @selector(safe_setObject:forKey:);
    Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);
    
    IMP originalIMP = method_getImplementation(originalMethod);
    IMP swizzledIMP = method_getImplementation(swizzledMethod);
    const char *originalType = method_getTypeEncoding(originalMethod);
    const char *swizzledType = method_getTypeEncoding(swizzledMethod);
    
    class_replaceMethod(originalClass,swizzledSelector,originalIMP,originalType);
    class_replaceMethod(originalClass,originalSelector,swizzledIMP,swizzledType);
}
- (void)safe_setObject:(id)anObject forKey:(id<NSCopying>)aKey {
    if (anObject && aKey) {
        [self safe_setObject:anObject forKey:aKey];
    }
    else if (aKey) {
        [(NSMutableDictionary *)self removeObjectForKey:aKey];
    }
}
小结

Method Swizzling是runtime应用中最常见的黑魔法之一。它很强大,可以灵活的修改方法的实现,但它也很危险,我们需要更加仔细的理解runtime而不是简单的粘贴复制。

相关文章

Method Swizzling的各种姿势
黑魔法 - Method Swizzling
Objective-C Runtime 运行时之四:Method Swizzling

相关文章

网友评论

      本文标题:每日一问11——Method Swizzling

      本文链接:https://www.haomeiwen.com/subject/jwpcsxtx.html