首页 > 代码库 > Runtime之方法

Runtime之方法

前两篇介绍了类与对象、成员变量&属性&关联对象的相关知识,本篇我们将开始讲解Runtime中最有意思的一部分内容:消息处理机制。我们从一个示例开始。

在OC中,我们使用下面这种方式来调用方法:

GofTest *test = [[GofTest alloc] init];
[test eat];

对上面的方法调用,我们用Runtime的消息发送机制改造一下:

id test = objc_msgSend(objc_getClass("GofTest"), sel_registerName("alloc"));
objc_msgSend(test, sel_registerName("init"));
objc_msgSend(test, sel_registerName("eat"));

对于上面的结果,我们来验证一下:

//cd到目录,执行如下指令
clang -rewrite-objc main.m

上面的Clang指令可以将 Objetive-C 的源码改写成 C 语言的,打开目录,可以看到多了一个main.cpp文件。打开main.cpp文件,这个文件有将近10W行代码,我们翻到最下面,看main函数:

#ifndef _REWRITER_typedef_GofTest
#define _REWRITER_typedef_GofTest
typedef struct objc_object GofTest;
typedef struct {} _objc_exc_GofTest;
#endif

struct GofTest_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
};


// - (void)eat;

/* @end */


int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        GofTest *test = ((GofTest *(*)(id, SEL))(void *)objc_msgSend)((id)((GofTest *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("GofTest"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)test, sel_registerName("eat"));
    }
    return 0;
}

去掉类型强制转换,我们再来分析一下:

int main(int argc, char * argv[]) {
   { __AtAutoreleasePool __autoreleasepool; 
        GofTest *test = objc_msgSend(objc_msgSend(objc_getClass("GofTest"), sel_registerName("alloc")), sel_registerName("init"));
        objc_msgSend(test, sel_registerName("eat"));
    }
    return 0;
}

通过查看C语言代码,我们得出一个初步结论:使用objc_msgSend函数,给objc_getClass函数实例化的对象发送sel_registerName获取到的方法的消息

在Runtime中,objc_msgSend这个方法是用汇编实现的,这是为了提升方法执行时的效率,因为几乎每个方法调用在内部都是通过objc_msgSend来完成的。为了进一步提升这个方法的效率,苹果还对每个类的方法做了缓存,当一个方法被调用过之后,它的IMP会被缓存在cache列表里,这样当再次调用该方法时,就可以很快地从缓存中取出,而不用再走一次完整的查找流程。

上面示例涉及到的SEL、objc_msgSend等内容,我们在下面会一一介绍。

【说明】:从Xcode 5.0开始,苹果不建议直接使用底层的消息发送机制,因此需要关掉下面的这个开关。

技术分享

看完上面的示例后,我们来了解几个基本类型。

1.基本类型

1.1SEL

SEL又叫选择器,是表示一个方法的selector的指针,其定义如下:

typedef struct objc_selector *SEL;

struct objc_selector
{
  void *sel_id;
  const char *sel_types;
};

方法的selector用于表示运行时方法的名字。Objective-C在编译时,会根据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的标识),这个标识就是SEL

SEL sel = @selector(alloc);
NSLog(@"sel : %p", sel);  //打印结果  sel : 0x103e719b2

两个类之间,只要方法名相同,那么方法的SEL就是一样的。每一个方法都对应着一个SEL,所以在Objective-C同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也不行。相同的方法只能对应一个SEL。这也就导致Objective-C在处理相同方法名且参数个数相同但类型不同的方法方面的能力很差。

当然,不同的类可以拥有相同的Selector,这个没有问题。不同的类的实例对象执行相同的Selector时,会在各自的方法列表中根据Selector去寻找自己对应的IMP。

工程中的所有SEL组成一个Set集合,如果我们想到这个方法集合中查找某个方法时,只需要找到这个方法对应的SEL就行了,SEL实际上就是根据方法名hash化了的一个字符串,而对于字符串的比较仅仅需要比较它们的地址就可以了。

本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。

我们可以通过下面三种方法来获取SEL:

//编译器提供
@selector()

//Runtime方法
OBJC_EXPORT SEL sel_registerName(const char *str)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);

//OC方法
FOUNDATION_EXPORT SEL NSSelectorFromString(NSString *aSelectorName);

示例如下:

        SEL sel = @selector(alloc);
        NSLog(@"sel : %p", sel);  //打印结果  sel : 0x103e719b2
        id test = objc_msgSend(objc_getClass("GofTest"), sel_registerName("alloc"));
        objc_msgSend(test, sel_registerName("init"));
        objc_msgSend(test, NSSelectorFromString(@"eat"));

1.2IMP

IMP实际上是一个函数指针,指向方法实现的首地址。其定义如下:

#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id (*IMP)(id, SEL, ...); 
#endif

这个函数的第一个参数是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),第二个参数是方法选择器(selector),接下来是方法的实际参数列表。

前面介绍过的SEL就是为了查找方法的最终实现IMP的。由于每一个方法对应唯一的SEL,因此我们可以通过SEL方便快速准确地获得它所对应的IMP,查找过程将在下面讨论。取得IMP后,我们就获得了执行这个方法代码的入口点,此时,我们就可以像调用普通的C语言函数一样来使用这个函数指针了。

通过取得IMP,我们可以跳过Runtime的消息传递机制,直接执行IMP指向的函数实现,这样省去了Runtime消息传递过程中所做的一系列查找操作,会比直接向对象发送消息高效一些。

1.3Method

Method用于表示类定义的方法,定义如下:

typedef struct objc_method *Method;

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;  //方法名
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;  //方法实现
}  

从定义可以看到,结构体中包含一个SEL和IMP,实际上相当于在SEL和IMP之间作了一个映射。有个SEL,我们便可以找到对应的IMP,从而调用方法的实现代码。

2.相关操作函数

Runtime提供了一系列的方法来处理与方法相关的操作。包括方法本身及SEL。

//调用指定方法的实现
OBJC_EXPORT id method_invoke(id receiver, Method m, ...) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
//调用返回一个数据结构的方法的实现
OBJC_EXPORT void method_invoke_stret(id receiver, Method m, ...) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0)
    OBJC_ARM64_UNAVAILABLE;
//获取方法名
OBJC_EXPORT SEL method_getName(Method m) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
//返回方法的实现
OBJC_EXPORT IMP method_getImplementation(Method m) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
//获取描述方法参数和返回值类型的字符串
OBJC_EXPORT const char *method_getTypeEncoding(Method m) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
//获取方法的返回值类型的字符串
char * method_copyReturnType ( Method m );
//获取方法的指定位置参数的类型字符串
OBJC_EXPORT char *method_copyArgumentType(Method m, unsigned int index) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
//通过引用返回方法的返回值类型字符串
OBJC_EXPORT void method_getReturnType(Method m, char *dst, size_t dst_len) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
//返回方法的参数的个数
OBJC_EXPORT unsigned int method_getNumberOfArguments(Method m)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);
//通过引用返回方法指定位置参数的类型字符串
OBJC_EXPORT void method_getArgumentType(Method m, unsigned int index, 
                                        char *dst, size_t dst_len) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
//返回指定方法的方法描述结构体
OBJC_EXPORT struct objc_method_description *method_getDescription(Method m) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
//设置方法的实现
OBJC_EXPORT IMP method_setImplementation(Method m, IMP imp) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
//交换两个方法的实现
OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
//返回给定选择器指定的方法的名称
OBJC_EXPORT const char *sel_getName(SEL sel)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);
//在Objective-C Runtime系统中注册一个方法,将方法名映射到一个选择器,并返回这个选择器。
//【说明】:所谓的注册,就是把一个methodName映射到一个selector并返回selecor; 如果已经注册则直接返回selector
OBJC_EXPORT SEL sel_registerName(const char *str) OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0); //在Objective-C Runtime系统中注册一个方法(objc.h) OBJC_EXPORT SEL sel_getUid(const char *str) OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0); //比较两个选择器 OBJC_EXPORT BOOL sel_isEqual(SEL lhs, SEL rhs) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

使用示例如下所示:

    GofPerson *person = [[GofPerson alloc] init];
    Class objectClsObj = object_getClass(person);
    
    //1.注册方法(这里的注册,就是把一个methodName映射到一个selector并返回selecor; 如果已经注册则直接返回selector)
    //留意一下,如果方法名称相同,则返回的SEL也是一样的
    SEL sel_register = sel_registerName("registerNewMethod");
    SEL sel_register2 = sel_getUid("registerNewMethod");
    
    //2.比较两个SEL
    if (sel_isEqual(sel_register, sel_register2)) {
        NSLog(@"相同的SEL");
    }
    else
    {
        NSLog(@"不同的SEL");
    }
    
    //3.获取实例方法
    Method instanceMethod = class_getInstanceMethod([GofPerson class], @selector(doWork:));  //如果指定的SEL不存在,那么返回Nil
    //objc_msgSend、objc_msgSendSuper、method_invoke(有返回值)都用于消息发送
    //objc_msgSend_stret、objc_msgSend_fpret函数是这三者的变体
    //当返回的是结构体、浮点数时,会调用类似_stret、_fpret的方法
    NSString *strRet = method_invoke(person, instanceMethod, @"看书");  //调用方法
    NSLog(@"method_invoke 执行方法结果:%@", strRet);
    SEL sel_instanceMethod = method_getName(instanceMethod);  //获取SEL
    NSLog(@"实例方法名称:%s", sel_getName(sel_instanceMethod));  //获取方法名称
    
    //4.获取类方法
    Method classMethod = class_getClassMethod(objectClsObj, @selector(createPersonWithName:age:));
    SEL sel_classMethod = method_getName(classMethod);  //获取SEL
    NSLog(@"类方法名称:%s", sel_getName(sel_classMethod));  //获取方法名称
    
    //5.获取实例方法列表
    unsigned int count_instanceMethod = 0;
    Method *instanceMethodList = class_copyMethodList([GofPerson class], &count_instanceMethod);
    for (int i = 0; i < count_instanceMethod; i++)
    {
        Method method = instanceMethodList[i];
        SEL sel_Method = method_getName(method);
        const char *returnType = method_copyReturnType(method);  //方法返回值类型
        NSLog(@"实例方法%d:%@ 返回值类型:%s", i, NSStringFromSelector(sel_Method), returnType);
    }
    free(instanceMethodList);
    
    //6.获取类方法列表
    unsigned int count_classMethod = 0;
    Method *classMethodList = class_copyMethodList(object_getClass([GofPerson class]), &count_classMethod);
    for (int i = 0; i < count_classMethod; i++)
    {
        Method method = classMethodList[i];
        SEL sel_Method = method_getName(method);
        const char *methodType = method_getTypeEncoding(method);// 获取方法参数类型和返回类型
        NSLog(@"类方法%d:%@  方法类型:%s", i, NSStringFromSelector(sel_Method), methodType);
    }
    free(classMethodList);
    
    //7.获取方法相关信息
    Method methodTest = class_getClassMethod(objectClsObj, @selector(createPersonWithName:age:));
    //获取方法返回值类型
    const char* method_ReturnType = method_copyReturnType(methodTest);
    NSLog(@"方法返回值类型:%@",[NSString stringWithUTF8String:method_ReturnType]);
    //获取方法参数个数
    unsigned int numberOfArguments = method_getNumberOfArguments(methodTest);
    NSLog(@"方法参数个数:%d",numberOfArguments);
    char argName[512] = {};
    for ( int i = 0; i < numberOfArguments ; i ++)
    {
        //获取方法参数类型方式1
        method_getArgumentType(methodTest, i, argName, 512);
        NSLog(@"方式1统计参数%d类型:%s", i, argName);
        memset(argName, \0, strlen(argName));
        //获取方法参数类型方式2
        const char *type = method_copyArgumentType(methodTest, i);
        NSLog(@"方式2统计参数%d类型:%@", i, [NSString stringWithUTF8String:type]);
    }
    //获取方法描述
    struct objc_method_description *method_descriptions = method_getDescription(methodTest);
    struct objc_method_description method_description = method_descriptions[0];
    SEL sel_des = method_description.name;
    const char *type_des = method_description.types;
    NSLog(@"方法描述 名称:%@  类型:%@",NSStringFromSelector(sel_des),[NSString stringWithUTF8String:type_des]);
    
    //8.交换方法实现
    SEL originalSelector = @selector(doWork:);
    SEL swizzledSelector = @selector(sayHello:);
    Method originalMethod = class_getInstanceMethod(objectClsObj, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(objectClsObj, swizzledSelector);
    method_exchangeImplementations(originalMethod, swizzledMethod);
//    if (!originalMethod || !swizzledMethod) {
//        return;
//    }
//    
//    IMP originalIMP = method_getImplementation(originalMethod);
//    IMP swizzledIMP = method_getImplementation(swizzledMethod);
//    const char *originalType = method_getTypeEncoding(originalMethod);
//    const char *swizzledType = method_getTypeEncoding(swizzledMethod);
    
    // 这儿的先后顺序是有讲究的,如果先执行后一句,那么在执行完瞬间方法被调用容易引发死循环
//    class_replaceMethod(objectClsObj, swizzledSelector, originalIMP, originalType);
//    class_replaceMethod(objectClsObj, originalSelector, swizzledIMP, swizzledType);
    [person doWork:@"吃饭"];
    [person sayHello:@"Gof"];

3.方法调用流程

在本篇开始,我们知道在Runtime中,编译器会把OC的消息表达式[receiver message]转化为一个消息函数的调用,即objc_msgSend,这个函数将消息接收者和方法名作为其基础参数。

这个函数完成了动态绑定的所有事情:

  1. 首先它找到selector对应的方法实现。因为同一个方法可能在不同的类中有不同的实现,所以需要依赖接收者的类来找到确切的实现。
  2. 它调用方法实现,并将接收者对象及方法的所有参数传给它。
  3. 最后,它将实现返回的值作为它自己的返回值。

当我们创建一个新对象时,先为其分配内存,并初始化其成员变量。其中isa指针也会被初始化,让对象可以访问类及类的继承体系。

技术分享

当消息发送给一个对象时,objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法分发表里面查找方法的selector。如果没有找到selector,则通过objc_msgSend结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的selector。依此,会一直沿用类的继承体系到达NSObject类。一旦定位到selector,函数就获取到了实现的入口点,并传入相应的参数来执行方法的具体实现。如果最后没有定位到selector,则会走消息转发流程,这个后面会介绍。为了加速消息的处理,运行时系统缓存使用过的selector及对应的方法的地址。

3.1隐藏参数

objc_msgSend有两个隐藏参数:

  1. 消息接收对象
  2. 方法的selector

这两个参数为方法的实现提供了调用者的信息。之所以说是隐藏的,是因为它们在定义方法的源代码中没有声明。它们是在编译器被插入实现代码的。

虽然这些参数没有显示声明,但在代码中仍然可以引用它们。我们可以使用self来引用接收者对象,使用_cmd来引用选择器。例如:

- (NSString *)sayHello:(NSString *)name {
    NSLog(@"Hello %s", sel_getName(_cmd));
    return name;
}

在实际开发中,我们用的比较多的是self,_cmd用的比较少。

3.2获取方法地址

Runtime中方法的动态绑定让我们写代码时更具灵活性。如我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等。不过灵活性的提升也带来了性能上的一些损耗。毕竟我们需要去查找方法的实现,而不像函数调用来的那么直接。

NSObject类提供了methodForSelector:方法,让我们可以获取到方法的指针,然后通过这个指针来调用实现代码。 使用示例如下:

    GofPerson *person = [[GofPerson alloc] init];
    
    IMP methodImplement = [person methodForSelector:@selector(sayHi)];
    for (int i = 0; i < 10000; i++) {
        methodImplement(self, @selector(sayHi));  
    }

【注意】:sayHi即使是私有方法(在.h文件中没有暴露接口),也可以调用成功。

4.消息转发

当一个对象能响应一个消息时,就会走正常的方法调用流程。但如果一个对象无法响应指定的消息时,会发生什么事呢?

默认情况下,如果是以[object message]的方式调用方法,如果object无法响应message消息时,编译器会报错。但如果是以perfrom...的形式来调用,则需要等到运行时才能确定object是否能响应消息。如果不能,则程序崩溃。 

通常,当我们不能确定一个对象是否能接收某个消息时,会先调用respondsToSelector:来判断一下。使用示例如下:

    GofPerson *person = [[GofPerson alloc] init];
    [person performSelector:@selector(test)];

本节我们不讨论使用respondsToSelector判断的情况,来谈一个新的机制:消息转发机制

当一个对象无法接收某一消息时,就会启动所谓消息转发机制。通过这一机制,我们可以告诉对象如何处理未知的消息。默认情况下,对象接收到未知的消息,会导致程序崩溃,通过控制台,我们可以看到以下异常信息:

技术分享

这段异常信息实际上是由NSObject的doesNotRecognizeSelector方法抛出来的。不过,我们可以采取一些措施,让我们的程序执行特定的逻辑,而避免程序的崩溃。

消息转发机制基本上分为三个步骤:

  1. 动态方法解析
  2. 备用接收者
  3. 完整转发

4.1动态方法解析

对象在接收到未知的消息时,首先会调用所属类的类方法+resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法),这两个方法是NSObject根类提供的类方法,调用时机为当被调用的方法实现部分没有找到,而消息转发机制启动之前的这个中间时刻。在这个方法中,我们有机会为该未知消息新增一个“处理方法”。不过使用该方法的前提是我们已经实现了该“处理方法”,只需要在运行时通过class_addMethod函数动态添加到类里面就可以了。示例代码如下:

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selectorString = NSStringFromSelector(sel);
    if ([selectorString isEqualToString:@"test"]) {
        class_addMethod(self.class, @selector(test), (IMP)testImplement, "@:");
    }
    return [super resolveInstanceMethod:sel];
}

void testImplement() {
    NSLog(@"TEST");
}

4.2备用接收者

如果在上一步无法处理消息,则Runtime会继续调用方法forwardingTargetForSelector 。如果一个对象实现了这个方法,并返回一个非nil得结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是self,否则会出现无限循环。当然,如果我们没有指定相应的对象来处理消息,则应该调用父类的实现来返回结果。示例代码如下:

//GofPerson.m文件
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"forwardingTargetForSelector");
    NSString *selectorString = NSStringFromSelector(aSelector);
    // 将消息转发给GofPersonReceiver来处理
    if ([selectorString isEqualToString:@"test"]) {
        return [[GofPersonReceiver alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

//新添加GofPersonReceiver类的.m文件
@implementation GofPersonReceiver

- (void)test {
    NSLog(@"GofPersonReceiver TEST");
}

@end

这里实际上是对消息指定了一个新的发送对象,将"test"转发给了这个GofPersonReceiver实例由它来相应这个消息。这里的返回值就是需要相应这个消息的对象。

【注意】:这一步适合于我们只想将消息转发到另一个能处理该消息的对象上;但这一步无法对消息进行处理,如操作消息的参数和返回值。

4.3完整消息转发

如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。此时会调用这个方法: forwardInvocation

运行时系统会在这一步给消息接收者最后一次机会将消息转发给其他对象。对象会创建一个表示消息的NSInvocation对象,把与尚未处理的消息有关的全部细节都封装在anInvocation中,包括selector,目标(target)和参数。我们可以通过实现forwardInvocation:方法来对不能处理的消息做一些默认的处理,以避免程序崩溃,这个函数主要是用来将消息转发给其它对象。每一个对象都从NSObject类继承了forwardInvocation:方法,但在NSObject中,该方法只是简单的调用doesNotRecognizeSelector:,通过重写该方法我们就可以利用forwardInvocation:将消息转发给其它对象。

forwardInvocation方法的实现有两个任务:

  1. 定位可以响应封装在anInvocation中的消息对象。这个对象不需要能处理所有未知消息。
  2. 使用anInvocation作为参数,将消息发送给选中的对象。anInvocation将会保留调用结果,运行时系统会提取这一结果并将其发送到消息的原始发送者。

不过,在这个方法中我们可以实现一些更复杂的功能,我们可以对消息的内容进行修改,比如追回一个参数等,然后再去出发消息。另外,若发现某个消息不应由本类处理,则应调用父类的同名方法,以便继承体系中的每个类都有机会处理此调用请求。 

还有一个很重要的问题,我们必须重新写以下方法: 

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE("");

消息转发机制使用从这个方法中获取的信息来创建NSInvocation对象。因此我们必须重写这个方法,为给定的selector提供一个合适的签名方法。通过重写methodSignatureForSelector:方法可以事先判断出另一个对象是不是真的能够响应将被转发的消息。如果返回NO,那么就不用执行forwardInvocation:方法了

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (!signature) {
        if ([GofPersonReceiver instancesRespondToSelector:aSelector]) {
            signature = [GofPersonReceiver instanceMethodSignatureForSelector:aSelector];
        }
    }
    return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if ([GofPersonReceiver instancesRespondToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:[[GofPersonReceiver alloc] init]];
    }
}

从某种意义上来讲,forwardInvocation:就像一个未知消息的分发中心,将这些未知的消息转发给其他对象。或者也可以像一个运输站一样将所有未知消息都发送给同一个接收对象。这取决于具体的实现。

4.4消息转发小结 

技术分享

5.方法交换(Method Swizzling)

方法交换是一项异常强大的技术,它可以允许我们动态地替换方法的实现,实现 Hook 功能,是一种比子类化更加灵活的“重写”方法的方式。

我们从一个页面统计的示例开始:

#import "UIViewController+GofUMAnalytics.h"
#import <objc/message.h>

@implementation UIViewController (GofUMAnalytics)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(gof_viewWillAppear:);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (success) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

#pragma mark - Method Swizzling

- (void)gof_viewWillAppear:(BOOL)animated {
    [self gof_viewWillAppear:animated];
    //加友盟统计代码
    NSLog(@"页面进入:%@", NSStringFromClass([self class]));
}

@end

看到上面的代码,大家可能存在这几个问题:

1.为什么是在 +load 方法中实现 Method Swizzling 的逻辑,而不是其他的什么方法?

+load 和 +initialize 是 Objective-C runtime 会自动调用的两个类方法。但是它们被调用的时机却是有差别的,+load 方法是在类被加载的时候调用的,而 +initialize 方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。也就是说 +initialize 方法是以懒加载的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的 +initialize 方法是永远不会被调用的。此外 +load 方法还有一个非常重要的特性,那就是子类、父类和分类中的 +load 方法的实现是被区别对待的。换句话说在 Objective-C runtime 自动调用 +load 方法时,分类中的 +load 方法并不会对主类中的 +load 方法造成覆盖。

2.为什么 Method Swizzling 的逻辑需要用 dispatch_once 来进行调度?

因为swizzling会改变全局状态,所以我们需要在运行时采取一些预防措施。原子性就是这样一种措施,它确保代码只被执行一次,不管有多少个线程。GCD的dispatch_once可以确保这种行为,我们应该将其作为method swizzling的最佳实践。

3.为什么需要调用 class_addMethod 方法,并且以它的结果为依据分别处理两种不同的情况 ?

我们使用 Method Swizzling 的目的通常都是为了给程序增加功能,而不是完全地替换某个功能,所以我们一般都需要在自定义的实现中调用原始的实现。

  • 情况一:主类本身有实现需要替换的方法,也就是 class_addMethod 方法返回 NO 。这种情况的处理比较简单,直接使用method_exchangeImplementations交换两个方法的实现就可以了。
  • 情况二:主类本身没有实现需要替换的方法,而是继承了父类的实现,即 class_addMethod 方法返回 YES 。这时使用 class_getInstanceMethod 函数获取到的 originalSelector 指向的就是父类的方法,我们再通过执行 class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); 将父类的实现替换到我们自定义的 mrc_viewWillAppear 方法中。这样就达到了在 mrc_viewWillAppear 方法的实现中调用父类实现的目的。

【注意】:

Swizzling通常被称作是一种黑魔法,容易产生不可预知的行为和无法预见的后果。因此在使用时需要注意如下地方:

  1. 总是调用方法的原始实现(除非有更好的理由不这么做):API提供了一个输入与输出约定,但其内部实现是一个黑盒。Swizzle一个方法而不调用原始实现可能会打破私有状态底层操作,从而影响到程序的其它部分。
  2. 避免冲突:给自定义的分类方法加前缀,从而使其与所依赖的代码库不会存在命名冲突。
  3. 明白是怎么回事:简单地拷贝粘贴swizzle代码而不理解它是如何工作的,不仅危险,而且会浪费学习Objective-C运行时的机会。

 

Runtime之方法