Method Swizzling

学习博客:《iOS黑魔法-Method Swizzling》 (这个作者太牛了,写了我一直想知道的类簇的swizz方法)

前言:

使用另一份实现来替换原有的方法实现,这道工序叫做“方法调配”,开发者常用此技术向原有实现中添加新功能。

一、 一般的swizz

先给要替换的方法的类添加一个Category,然后在Category中的+(void)load方法中添加Method Swizzling方法,我们用来替换的方法也写在这个Category中。由于load类方法是程序运行时这个类被加载到内存中就调用的一个方法,执行比较早,并且不需要我们手动调用。而且这个方法具有唯一性,也就是只会被调用一次,不用担心资源抢夺的问题。

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
   
        SEL originalSelector = @selector(showSplash:);
        SEL swizzledSelector = @selector(swizzledShowSplash:);
       // 通过class_getInstanceMethod()函数从当前对象中的method list获取method结构体,如果是类方法就使用class_getClassMethod()函数获取。
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
     /**
     *  我们在这里使用class_addMethod()函数对Method Swizzling做了一层验证,如果self没有实现被交换的方法,会导致失败。
     *  而且self没有交换的方法实现,但是父类有这个方法,这样就会调用父类的方法,结果就不是我们想要的结果了。
     *  所以我们在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO,我们就可以对其进行交换了。
     */

        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);
        }
    });
}
- (void)swizzledShowSplash:(NSString *)message { if(message && message.length > 0){ [self swizzledShowSplash]; // 此时swizzledShowSplash已经和showSplash交换了实现,所以这里实际是在调用showSplash } }

 二、Method Swizzling类簇

按照上面的例子对NSArray、NSMutableArray、NSDictionary、NSMutableDictionary等类进行Method Swizzling来做swizz。发现Method Swizzling根本就不起作用。

这是因为Method Swizzling对NSArray这些的类簇是不起作用的。因为这些类簇类,其实是一种抽象工厂的设计模式。抽象工厂内部有很多其它继承自当前类的子类,抽象工厂类会根据不同情况,创建不同的抽象对象来进行使用。例如我们调用NSArray的objectAtIndex:方法,这个类会在方法内部判断,内部创建不同抽象类进行操作。

所以也就是我们对NSArray类进行操作其实只是对父类进行了操作,在NSArray内部会创建其他子类来执行操作,真正执行操作的并不是NSArray自身,所以我们应该对其“真身”进行操作。

“真身”
NSArray

__NSArrayI

NSMutableArray

__NSArrayM

NSDictionary

__NSDictionaryI

NSMutableArray

__NSDictionaryM

例如:防止NSArray因为调用objectAtIndex:方法,取下标时数组越界导致的崩溃的swizz方法:

#import "NSArray+CRMArray.h"

@implementation NSArray (CRMArray)
+ (void)load {
    [super load];
    Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
    Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(lxz_objectAtIndex:));
    method_exchangeImplementations(fromMethod, toMethod);
}

- (id)lxz_objectAtIndex:(NSUInteger)index {
    if (self.count-1 < index) {
        // 这里做一下异常处理,不然都不知道出错了。
        @try {
            return [self lxz_objectAtIndex:index];
        }
        @catch (NSException *exception) {
            // 在崩溃后会打印崩溃信息,方便我们调试。
            NSLog(@"---------- %s Crash Because Method %s  ----------
", class_getName(self.class), __func__);
            NSLog(@"%@", [exception callStackSymbols]);
            return nil;
        }
        @finally {}
    } else {
        return [self lxz_objectAtIndex:index];
    }
}
@end

 三、使用场景

  通过此方案,开发者可以为那些“完全不知道其具体实现的”黑盒方法添加日志记录功能,这非常有助于调试程序。然而,此做法只在调试程序时用,很少有人在调试程序之外的场合用上述“方法调配技术”来永久改动某个类的功能。

原文地址:https://www.cnblogs.com/Xylophone/p/5983749.html