天天看点

苹果的“黑魔法”Method Swizzling

Method Swizzling原理

Method Swizzling是发生在运行时的,主要用于在运行时将两个Method进行交换,可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swizzling代码执行完毕之后互换才起作用。Method Swizzling是iOS中AOP(面向切面编程)的一种实现方式,我们可以利用苹果这一特性来实现AOP编程。

首先,让我们通过下面的两张图片来了解一下Method Swizzling的实现原理

NSString类可以响应lowercaseString、uppercaseString、capitalizedString等选择子。这张映射表中的每个选择子都映射到了不同的IMP之上,如下图所示:

苹果的“黑魔法”Method Swizzling

图 1 NSString类的选择子映射表

Objective-C运行期系统提供了几个方法都能够用来操作这张表。开发者可以向其中新增选择子,也可以改变某选择子所对应的方法实现,还可以交换两个选择子所映射到的指针。经过几次操作之后,类的方法表就会变成图2

苹果的“黑魔法”Method Swizzling

图 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")           

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

苹果的“黑魔法”Method Swizzling

参考文献:

1.Effective Objective-C 2.0 Matt Galloway 著 爱飞翔 译

2. iOS黑魔法-Method Swizzling  http://www.jianshu.com/p/ff19c04b34d0

继续阅读