您的位置:首页 > 移动开发 > IOS开发

苹果的“黑魔法”Method Swizzling

2016-02-19 16:44 387 查看
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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息