Method Swizzling

一、Method Swizzling 原理

我们知道 OC 是动态语言,我们执行一个函数的时候,其实是在发一条消息:[receiver message],这个过程就是根据 message 生成 selector,然后根据 selector 寻找指向函数具体实现的指针IMP,然后找到真正的函数执行逻辑。

首先,我们看下每一个类的结构体:

struct objc_class {
  struct objc_class * isa;
  const char *name;
  ….
  struct objc_method_list **methodLists; /*方法链表*/
};

其中 methodList 方法链表里存储的是 Method类型:

typedef struct objc_method *Method;
typedef struct objc_ method {
  SEL method_name;
  char *method_types;
  IMP method_imp;
};

Method 保存了一个方法的全部信息,包括 SEL 方法名,type各参数和返回值类型,IMP该方法具体实现的函数指针。

通过 Selector 调用方法时,会从methodList 链表里找到对应Method进行调用。

我们看下图解,就更加清晰啦 ~

Method Swizzling_第1张图片
图1.png

我们有几种方法可以进行修改:

  • 可以把某个Selector对应的函数指针IMP替换成新的
    class_replaceMethod

  • 也可以拿到已有的某个 Selector 对应的函数指针IMP,让另一个Selector 跟它对应
    method_exchangeImplementations
    method_setImplementation

归根结底,都是偷换了selector的IMP,如下图所示:

Method Swizzling_第2张图片
图2.png

二、Method Swizzling 实践

比如我们要给所有页面 添加页面统计。

我们先给UIViewController添加一个Category,然后在Category中的+(void)load方法中添加Method Swizzling方法,我们用来替换的方法也写在这个Category中。

由于load类方法是程序运行时这个类被加载到内存中就调用的一个方法,执行比较早,并且不需要我们手动调用。而且这个方法具有唯一性,也就是只会被调用一次,不用担心资源抢夺的问题。

定义Method Swizzling中我们自定义的方法时,需要注意尽量加前缀,以防止和其他地方命名冲突,Method Swizzling的替换方法命名一定要是唯一的,至少在被替换的类中必须是唯一的。

#import "UIViewController+swizzling.h"
#import @implementation UIViewController (swizzling)
 
+ (void)load {

    // 通过class_getInstanceMethod()函数从当前对象中的method list获取method结构体,如果是类方法就使用class_getClassMethod()函数获取。
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method fromMethod = class_getInstanceMethod([self class], @selector(viewDidLoad));
        Method toMethod = class_getInstanceMethod([self class], @selector(swizzlingViewDidLoad));
        /**
         *  我们在这里使用class_addMethod()函数对Method Swizzling做了一层验证,如果self没有实现被交换的方法,会导致失败。
         *  而且self没有交换的方法实现,但是父类有这个方法,这样就会调用父类的方法,结果就不是我们想要的结果了。
         *  所以我们在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO,我们就可以对其进行交换了。
         */
        if (!class_addMethod([self class], @selector(viewDidLoad), method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
            method_exchangeImplementations(fromMethod, toMethod);
        }
    });
}
 
// 我们自己实现的方法,也就是和self的viewDidLoad方法进行交换的方法。
- (void)swizzlingViewDidLoad {
    NSString *str = [NSString stringWithFormat:@"%@", self.class];
    // 我们在这里加一个判断,将系统的UIViewController的对象剔除掉
    if(![str containsString:@"UI"]){
        NSLog(@"统计打点 : %@", self.class);
    }
    [self swizzlingViewDidLoad];
}
@end

乍一看,这不递归了么?别忘记这是我们准备调换IMP的selector,[self swizzlingViewDidLoad] 将会执行真的 [self viewDidLoad] 。

三、Method Swizzling 几个注意的问题

  • Method swizzling is not atomic
    我所见过的使用method swizzling实现的方法在并发使用时基本都是安全的。95%的情况里这都不会是个问题。通常你替换一个方法的实现,是希望它在整个程序的生命周期里有效的。也就是说,你会把 method swizzling 修改方法实现的操作放在一个加号方法 +(void)load里,并在应用程序的一开始就调用执行。你将不会碰到并发问题。假如你在 +(void)initialize初始化方法中进行swizzle,那么……rumtime可能死于一个诡异的状态。

  • The order of swizzles matters
    多个有继承关系的类的对象swizzle时,先从父对象开始。 这样才能保证子类方法拿到父类中的被swizzle的实现。在+(void)load中swizzle不会出错,就是因为load类方法会默认从父类开始调用。
    大家可以看看这篇文章详解:
    Method Swizzling

  • Possible naming conflicts
    命名冲突贯穿整个Cocoa的问题. 我们常常在类名和类别方法名前加上前缀。不幸的是,命名冲突仍是个折磨。但是swizzling其实也不必过多考虑这个问题。我们只需要在原始方法命名前做小小的改动来命名就好,比如通常我们这样命名:

@interface NSView : NSObject  

- (void)setFrame:(NSRect)frame;  
@end  

@implementation NSView (MyViewAdditions)  

- (void)my_setFrame:(NSRect)frame {  
    // do custom work  
    [self my_setFrame:frame];  
}  

+ (void)load {  
    [self swizzle:@selector(setFrame:) with:@selector(my_setFrame:)];  
}  
  
@end  

这段代码运行正确,但是如果my_setFrame: 在别处被定义了会发生什么呢?
这个问题不仅仅存在于swizzling,这里有一个替代的变通方法:

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

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

+ (void)load {
    [self swizzle:@selector(originalMethod:) with:(IMP)MethodSwizzle store:(IMP *)&MethodOriginal];
} 

看起来不那么Objectice-C了(用了函数指针),这样避免了selector的命名冲突。

四、Method Swizzling类簇

在我们项目开发过程中,经常因为NSArray数组越界或者NSDictionary的key或者value值为nil等问题导致的崩溃,对于这些问题苹果并不会报一个警告,而是直接崩溃,感觉苹果这样确实有点“太狠了”。

由此,我们可以根据上面所学,对NSArray、NSMutableArray、NSDictionary、NSMutableDictionary等类进行Method Swizzling,实现方式还是按照上面的例子来做。但是....你发现Method Swizzling根本就不起作用,代码也没写错啊,到底是什么鬼?

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

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

下面我们实现了防止NSArray因为调用objectAtIndex:方法,取下标时数组越界导致的崩溃:

#import "NSArray+LXZArray.h"
#import "objc/runtime.h"
@implementation NSArray (LXZArray)
+ (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  ----------\n", class_getName(self.class), __func__);
            NSLog(@"%@", [exception callStackSymbols]);
            return nil;
    }
        @finally {}
    } else {
        return [self lxz_objectAtIndex:index];
    }
}
@end

大家发现了吗,__NSArrayI才是NSArray真正的类,而NSMutableArray又不一样????。我们可以通过runtime函数获取真正的类:

 objc_getClass("__NSArrayI")

下面我们列举一些常用的类簇的“真身”:

Method Swizzling_第3张图片
图.png

五、Method Swizzling封装

在项目中我们肯定会在很多地方用到Method Swizzling,而且在使用这个特性时有很多需要注意的地方。我们可以将Method Swizzling封装起来,也可以使用一些比较成熟的第三方。

在这里我推荐Github上星最多的一个第三方-jrswizzle

里面核心就两个类,代码看起来非常清爽。

#import @interface NSObject (JRSwizzle)
+ (BOOL)jr_swizzleMethod:(SEL)origSel_ withMethod:(SEL)altSel_ error:(NSError**)error_;
+ (BOOL)jr_swizzleClassMethod:(SEL)origSel_ withClassMethod:(SEL)altSel_ error:(NSError**)error_;
@end
 
// MethodSwizzle类
#import BOOL ClassMethodSwizzle(Class klass, SEL origSel, SEL altSel);
BOOL MethodSwizzle(Class klass, SEL origSel, SEL altSel);

你可能感兴趣的:(Method Swizzling)