什么是Method Swizzling
Method Swizzling是改变一个已存在的selector的实现的技术。可以使用它来在利用iOS的runtime机制通过修改类的分发表中selector对应的函数,来修改selector的实现。也就是说它可以修改系统的方法,本质上就是对IMP和SEL进行交换。
SEL:系统在编译过程中,会根据方法的名字以及参数序列生成一个用来区分这个方法的唯一ID编号,这个 ID 就是 SEL 类型的。我们需要注意的是,只要方法的名字和参数序列完全相同,那么它们的 ID编号就是相同的。
IMP:即Implementation
,为指向函数实现的指针,如果我们能够获取到这个指针,则可以直接调用该方法,充分证实了它就是一个函数的指针。
额外说下Method:字面上一看就是方法的意思。Method
其实就是 objc_method
的结构体指针。
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class;
const char *name; long version; long info; long instance_size; struct objc_ivar_list *ivars; **struct objc_method_list **methodLists**; **struct objc_cache *cache**; struct objc_protocol_list *protocols; #endif }; struct objc_method_list { struct objc_method_list *obsolete; int method_count; #ifdef __LP64__ int space; #endif /* variable length structure */ struct objc_method method_list[1]; }; struct objc_method { SEL method_name; char *method_types; /* a string representing argument/return types */ IMP method_imp; };
objc_method_list实际上就是有着objc_method元素的可变数组,一个objc_method结构体里面有SEL也就是函数名,有表示函数类型的字符串,以及函数的实现IMP。
从这些定义中可以看出发送一条消息也就是objc_msgSend实际上做了什么事情:
1.首先通过obj的isa指针找到它的class。
2.然后在class的method_list列表中找我们要实现的函数。
3.如果没有找到,那就继续去它的superClass中找。
4.一旦找到我们要实现的函数,就去执行它的实现IMP。
Method Swizzling原理
Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换,我们可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swilzzling代码执行完毕之后互换才起作用。
在OC语言的runtime特性中,调用一个对象的方法就是给这个对象发送消息。是通过查找接收消息对象的方法列表,从方法列表中查找对应的SEL,这个SEL对应着一个IMP(一个IMP可以对应多个SEL),通过这个IMP找到对应的方法调用,归根到底是函数指针的调用,我们可以改变Method
结构体中IMP函数实现指针的指向:
以实例方法为例上两张原理图:
交换前:
交换后:
在每个类中都有一个Dispatch Table,这个Dispatch Table本质是将类中的SEL和IMP(可以理解为函数指针)进行对应。而我们的Method Swizzling就是对这个table进行了操作,让SEL对应另一个IMP。
Method Swizzling使用
1.页面添加统计功能:
因为Method Swizzling的实现模式比较固定,所以将其抽象为一个分类,可以直接方便调用。
#import <Foundation/Foundation.h> @interface NSObject (Swizzling) +(void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector; @end #import "NSObject+Swizzling.h" #import <objc/runtime.h> @implementation NSObject (Swizzling) +(void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector { Class class = [self class]; Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzleMethod = class_getInstanceMethod(class, swizzledSelector); BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod)); if (didAddMethod) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else{ method_exchangeImplementations(originalMethod, swizzleMethod); } } @end
- class_addMethod:实现会覆盖父类的方法实现。但不会取代本类中已存在的实现,如果本类中包含一个同名的实现,则函数返回NO。这里为源SEL添加IMP是为了避免源SEL没有实现IMP的情况。若添加成功则说明源SEL没有实现IMP,将源SEL的IMP替换到交换SEL的IMP;若添加失败则说明源SEL已经有IMP了,直接将两个SEL的IMP交换就可以了。
- class_replaceMethod:该函数的行为可以分为两种:如果类中不存在name指定的方法,则类似于clss_addMethod函数一样会添加方法;如果类中已存在name指定的方法,则类似于method_setImplementation一样代替原方法的实现。
统计页面展示次数,我们可以先给UIViewController添加一个Category,然后再Category中的+(void)load方法中添加Method Swizzling方法。我们用来天的方法也可以写在这个分类中。
#import <UIKit/UIKit.h> @interface UIViewController (Swizzling) @end #import "UIViewController+Swizzling.h" #import "NSObject+Swizzling.h" @implementation UIViewController (Swizzling) +(void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [UIViewController methodSwizzlingWithOriginalSelector:@selector(viewDidLoad) bySwizzledSelector:@selector(my_ViewDidLoad)]; }); } -(void) my_ViewDidLoad { [self my_ViewDidLoad]; NSString *str = [NSString stringWithFormat:@"%@", self.class]; // 我们在这里加一个判断,将系统的UIViewController的对象剔除掉 if(![str containsString:@"UI"]){ NSLog(@"统计打点 : %@", self.class); } } @end
有几点需要注意下:
1.Swizzling应该总在+load中执行
Runtime会在类被加载到内存时调用+load方法,在类第一次被调用时实现+initialize方法。由于Method Swizzling会影响到类的全局状态,所以要尽量避免在并发处理中出现竞争情况。+load方法能保证在类的初始化过程中被加载,并保证这种改变应用级的行为的一致性。
2.要使用dispatch_once执行方法交换
方法交换要求线程安全,而且保证任何情况下只交换一次。
3.可能会有人有疑问,在my_ViewDidLoad中又调用了一次my_ViewDidLoad不会引起递归调用吗
实际上Method Swizzling实现原理可以理解为“方法互换”,当调用viewDidLoad的时候实际上调用的是my_ViewDidLoad,而在my_ViewDidLoad中再次调用my_ViewDidLoad的时候实际上调用的是viewDidLoad。
- (void)viewDidAppear:(BOOL)animated;
以及
- (void)viewDidDisappear:(BOOL)animated;具体实现和上面类同。
Method Swizzling类簇
当你交换NSArray
等类簇的方法的时候,因为这些类簇类,其实是一种抽象工厂的设计模式。抽象工厂内部有很多其它继承自当前类的子类,抽象工厂类会根据不同情况,创建不同的抽象对象来进行调用。所以对NSArray、NSMutableArray、NSDictionary、NSMutableDictionary等类进行Method Swizzling,实现方式还是按照上面的例子来做。但是....你发现Method Swizzling根本就不起作用。所以也就是我们对NSArray类进行操作其实只是对父类进行了操作,在NSArray内部会创建其他子类来执行操作,真正执行操作的并不是NSArray自身,所以我们应该对其“真身”进行操作。
下面我们实现了防止NSArray因为调用objectAtIndex:方法,取下标时数组越界导致的崩溃:
#import "NSArray+SwizzArray.h" #import "objc/runtime.h" @implementation NSArray (SwizzArray) + (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真正的类。
关于Method Swizzling类簇这块,可以直接使用AvoidCrash -- 远离常见的崩溃,但是往往快刀都是一把双刃剑,具体使用还要看情况。
参考文章:
Runtime奇技淫巧之method_exchangeImplementations