Method Swizzling原理
Method Swizzling是发生在运行时的,主要用于在运行时将两个Method进行交换,可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swizzling代码执行完毕之后互换才起作用。Method Swizzling是iOS中AOP(面向切面编程)的一种实现方式,我们可以利用苹果这一特性来实现AOP编程。
首先,让我们通过下面的两张图片来了解一下Method Swizzling的实现原理
NSString类可以响应lowercaseString、uppercaseString、capitalizedString等选择子。这张映射表中的每个选择子都映射到了不同的IMP之上,如下图所示:
图 1 NSString类的选择子映射表
Objective-C运行期系统提供了几个方法都能够用来操作这张表。开发者可以向其中新增选择子,也可以改变某选择子所对应的方法实现,还可以交换两个选择子所映射到的指针。经过几次操作之后,类的方法表就会变成图2
图 2 经过数次操作之后的NSString选择子映射表
在新的映射表中,多了一个名为newSelector的选择子,capitalizedString的实现也变了,而lowercaseString与uppercaseString的实现则互换了。上述修改均无须编写子类,只要修改了“方法表”的布局,就会反映到程序中所有的NSString实例之上。
Method Swizzling使用
在实现Method Swizzling时,核心代码主要是一个runtime的C语言API:
<span style="font-size:14px;"> void method_exchangeImplementations(Method fromMethod, Method toMethod) </span>此函数的两个参数表示待交换的两个方法实现,而方法实现则可通过下列函数获得:
<span style="font-size:14px;"> Method fromMethod = class_getInstanceMethod(objc_getClass("_NSArrayI"), @selector(objectAtIndex:));</span>
此函数根据给定的选择从类中取出与之相关的方法。执行下列代码,即可交换前面的low而caseString与uppercaseString方法实现:
<span style="font-size:14px;">Method fromMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString)); Method toMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString)); method_exchangeImplementations(fromMethod, toMethod);</span>如果在NSString实例上调用lowercaseString,那么执行的将是uppercaseString的原有实现,反之亦然:
<span style="font-size:14px;"> NSString *string = @"ThIs iS tHe String"; NSString *lowercaseString = [string lowercaseString]; NSLog(@"lowercaseString = %@",lowercaseString); //Output: lowercaseString = THIS IS THE STRING</span>刚才向大家演示了如何交换两个方法实现,然而在实际应用中,像这样直接交换两个方法实现的,意义并不大。因为lowercaseString与uppercaseString这两个方法已经各自实现得很好了,没必要再交换了。但是,可以通过这一手段来为既有的方法实现增添新功能。比方说,在我们项目开发过程中,经常因为NSArray数组越界或者NSDictionary的key或者value值为nil等问题导致的崩溃,对于这些问题苹果并不会报一个警告,而是直接崩溃。
由此,我们可以根据上面所学,对NSArray、NSMutableArray、NSDictionary、NSMutableDictionary等类进行Method Swizzling,实现方式还是按照上面的例子来做。但是,你发现Method Swizzling根本不起作用,代码也没写错。是什么原因导致这个问题?
这是因为Method Swizzling对NSArray这些的类簇是不起作用的。因为这些类簇类,其实是一种抽象工厂的设计模式。抽象工厂内部有很多其它继承自当前类的子类,抽象工厂类会根据不同情况,创建不同的抽象对象来进行使用。例如我们调用NSArray的objectAtIndex:方法,这个类会在方法内部判断,内部创建不同抽象类进行操作。
所以也就是我们对NSArray类进行操作其实只是对父类进行了操作,在NSArray内部会创建其他子类来执行操作,真正执行操作的并不是NSArray自身,所以我们应该对其“真身”进行操作。
下面我们实现了防止NSArray因为调用objectAtIndex:取下标时数组越界导致的崩溃:
新方法可以添加至NSArray的一个“分类”(category)中:
#import <Foundation/Foundation.h> @interface NSArray (JWLArray) @end新方法的实现如下:
// // NSArray+JWLArray.m // CAE_Hycloud // // Created by bcc_cae on 16/1/25. // Copyright © 2016年 bcc_cae. All rights reserved. // #import "NSArray+JWLArray.h" #import "objc/runtime.h" @implementation NSArray (JWLArray) + (void)load { [super load]; // 通过class_getInstanceMethod()函数从当前对象中的method_list获取method结构体,如果是类方法就使用class_getClassMethod()函数获取 Method fromMethod = class_getInstanceMethod(objc_getClass("_NSArrayI"), @selector(objectAtIndex:)); Method toMethod = class_getInstanceMethod(objc_getClass("_NSArrayI"), @selector(jwl_objectAtIndex:)); method_exchangeImplementations(fromMethod, toMethod); } -(id)jwl_objectAtIndex:(NSUInteger)index { if (self.count - 1 < index) { //异常处理 @try { return [self jwl_objectAtIndex:index]; } @catch (NSException *exception) { //在崩溃后会打印崩溃信息 HYLog(@"----- %s Crash Because Method %s -----\n",class_getName(self.class),__func__); HYLog(@"%@",[exception callStackSymbols]); return nil; } @finally {} } else { return [self jwl_objectAtIndex:index]; } } @end大家可以发现,_NSArray才是NSArray真正的类,我们可以通过runtime函数获取真正的类:
objc_getClass("_NSArrayI")下面我们列举一些常用的类簇的“真身”:
1.Effective Objective-C 2.0 Matt Galloway 著 爱飞翔 译
2. iOS黑魔法-Method Swizzling http://www.jianshu.com/p/ff19c04b34d0