iOS 在VC生命周期中添加方法

/ 0评 / 0

之前遇到了一个问题,需要在VC的生命周期 viewDidLoad 之前添加一个方法,当时没有考虑,现在又需要用到了,简单说一下,其实之前考虑的是继承、Category(创建一个Category来覆盖系统方法,系统会优先调用Category中的代码,然后在调用原类中的代码),但是发现太麻烦了,于是就找到了另一种方法,也就是iOS中的黑魔法 Method Swizzling,其实本质上就是对IMP和SEL进行交换。
Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换,我们可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swilzzling代码执行完毕之后互换才起作用。而且Method Swizzling也是iOS中AOP(面相切面编程)的一种实现方式,我们可以利用苹果这一特性来实现AOP编程。来看图理解

上面图一中selector2原本对应着IMP2,但是为了更方便的实现特定业务需求,我们在图二中添加了selector3和IMP3,并且让selector2指向了IMP3,而selector3则指向了IMP2,这样就实现了“方法互换”。
在OC语言的runtime特性中,调用一个对象的方法就是给这个对象发送消息。是通过查找接收消息对象的方法列表,从方法列表中查找对应的SEL,这个SEL对应着一个IMP(一个IMP可以对应多个SEL),通过这个IMP找到对应的方法调用。
在每个类中都有一个Dispatch Table,这个Dispatch Table本质是将类中的SEL和IMP(可以理解为函数指针)进行对应。而我们的Method Swizzling就是对这个table进行了操作,让SEL对应另一个IMP。
在实现Method Swizzling时,核心代码主要就是一个runtime的C语言API:

OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2) 
 __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);

拿我们这里的需求来说,先给UIViewController添加一个Category,然后在Category中的+(void)load方法中添加Method Swizzling方法,我们用来替换的方法也写在这个Category中。由于load类方法是程序运行时这个类被加载到内存中就调用的一个方法,执行比较早,并且不需要我们手动调用。而且这个方法具有唯一性,也就是只会被调用一次,不用担心资源抢夺的问题。定义Method Swizzling中我们自定义的方法时,需要注意尽量加前缀,以防止和其他地方命名冲突,Method Swizzling的替换方法命名一定要是唯一的,至少在被替换的类中必须是唯一的。

#import "UIViewController+viewWillLoad.h"
#import 
@implementation UIViewController (viewWillLoad)

+ (void)load {
    Method fromMethod = class_getInstanceMethod([self class], @selector(viewDidLoad));
    Method toMethod = class_getInstanceMethod([self class], @selector(dc_viewWillLoad));
    if (!class_addMethod([self class], @selector(dc_viewWillLoad), method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
        method_exchangeImplementations(fromMethod, toMethod);
    }
}

// 我们自己实现的方法,也就是和self的viewDidLoad方法进行交换的方法。
- (void)dc_viewWillLoad {
   //将这个方法在头文件中声明

    [self dc_viewWillLoad];
}
@end

那为什么在我们的方法里又调用了一次自己呢?
还记得我们上面的图一和图二吗?Method Swizzling的实现原理可以理解为”方法互换“。假设我们将A和B两个方法进行互换,向A方法发送消息时执行的却是B方法,向B方法发送消息时执行的是A方法。
例如我们上面的代码,系统调用UIViewController的viewDidLoad方法时,实际上执行的是我们实现的dc_viewWillLoad方法。而我们在dc_viewWillLoad方法内部调用[self dc_viewWillLoad];时,执行的是UIViewController的viewDidLoad方法。
这样就OK了~

扩展

在项目开发过程中,经常因为NSArray数组越界或者NSDictionary的key或者value值为nil等问题导致的崩溃。由此,我们可以根据上面所学,对NSArray、NSMutableArray、NSDictionary、NSMutableDictionary等类进行Method Swizzling,但是实现方式就不能按照上面的例子来做了。

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

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

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

#import "NSArray+DCArray.h"
#import 
@implementation NSArray (DCArray)
+ (void)load {
    Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
    Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(dc_objectAtIndex:));
    method_exchangeImplementations(fromMethod, toMethod);
}

- (id)dc_objectAtIndex:(NSUInteger)index {
    if (self.count-1 < index) {
        // 这里做一下异常处理,不然都不知道出错了。
        @try {
            return [self dc_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 dc_objectAtIndex:index];
    }
}
@end

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

objc_getClass("__NSArrayI")

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

类 “真身”
NSArray __NSArrayI
NSMutableArray __NSArrayM
NSDictionary __NSDictionaryI
NSMutableDictionary __NSDictionaryM

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

注意:
不要在+ (void)load方法中调用[super load]方法,这会导致父类的Swizzling被重复执行两次,这样父类的Swizzling就会失效。
我们上面提到过Method Swizzling的实现原理就是对类的Dispatch Table进行操作,每进行一次Swizzling就交换一次SEL和IMP(可以理解为函数指针),如果Swizzling被执行了多次,就相当于SEL和IMP被交换了多次。这就会导致第一次执行成功交换了、第二次执行又换回去了、第三次执行.....这样换来换去的结果,能不能成功就看运气了,这也是好多人说Method Swizzling不好用的原因之一。

从这张图中我们也可以看出问题产生的原因了,就是Swizzling的代码被重复执行,为了避免这样的原因出现,我们可以通过GCD的dispatch_once函数来解决,利用dispatch_once函数内代码只会执行一次的特性。

在每个Method Swizzling的地方,加上dispatch_once函数保证代码只被执行一次。当然在实际使用中也可以对下面代码进行封装。

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(objectAtIndex:));
        Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(dc_objectAtIndexM:));
        method_exchangeImplementations(fromMethod, toMethod);
    });
}

我们之前说过IMP本质上就是函数指针,所以我们调试的时候可以通过打印函数指针的方式,查看SEL和IMP的交换流程。

Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(dc_objectAtIndex:));

NSLog(@"%p", method_getImplementation(fromMethod));
NSLog(@"%p", method_getImplementation(toMethod));
method_exchangeImplementations(fromMethod, toMethod);

NSLog(@"%p", method_getImplementation(fromMethod));
NSLog(@"%p", method_getImplementation(toMethod));
method_exchangeImplementations(fromMethod, toMethod);

NSLog(@"%p", method_getImplementation(fromMethod));
NSLog(@"%p", method_getImplementation(toMethod));
method_exchangeImplementations(fromMethod, toMethod);

NSLog(@"%p", method_getImplementation(fromMethod));
NSLog(@"%p", method_getImplementation(toMethod));

评论已关闭。