Runtime

193 阅读13分钟

1.Runtime 概念

OC是一门动态语言,之所以称为动态语言是因为程序在运行时才根据方法名找到对应的方法/函数来调用,而不是在编译时就决定了调用哪个函数。

OC语言是C语言的超集,C语言的基础上加上 Runtime ,使得C语言有了面向对象的特性,变成了OC。

Runtime 就是一个库,里面包含了用C语言和汇编语言写了一套API,利用这套API,使 C 语言拥有了面向对象的开发能力。

2.Runtime 库中的类和实例

objc.h 文件中可以看到 Class 的定义:

// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

可以看到 Class 是一个objc_class结构体类型的指针。我们接着来 runtime.h 看 objc_class 结构体的定义:

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

objc.h 文件中可以看到实例对象的定义:

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

objc_object 结构体中可以看到,这个结构体只包含一个 isa 指针,这个指针指向实例对象所属的类。

类的实例对象的 isa 指向它的类;类的 isa 指向该类的 metaclass,metaclass 的 isa 指向根 metaclass,如果该 metaclass 是根 metaclass则指向自身;

isa 是什么?

OC中的类和实例均可以看做是一个结构体“对象”。

那么,isa 是一个指向 Class 的指针。

实例对象的 isa 是指向这个对象的类的指针。

类对象的 isa 指针指向其所属的类,即元类(meta-Class)。元类中存储着类对象的类方法,当访问某个类的类方法时会通过该 isa 指针从元类中寻找方法对应的函数指针。实例的类中存储着实例方法,当访问某个实例方法时会通过 isa 指针从这个实例的类中寻找实例方法对应的函数指针。

id 是什么?

/// A pointer to an instance of a class.
typedef struct objc_object *id;
#endif

它是一个 objc_object 结构类型的指针。该类型的对象可以转换为任何一种对象。

3. 好好看看 objc_class 这个结构体

在上面的类的机构体中,可以看到其中包含了一个 isa(指向 Class 的isa 指针)、super_class(超类)、nameversioninfoinstance_sizeobjc_ivar_list(成员变量列表)、objc_method_list(方法列表)、cacheprotocols(协议列表)。

3.1 父类 super_class

一个类的父类。 想要获取一个类的父类,使用 API 为:

///Returns the superclass of a class.
Class _Nullable class_getSuperclass(Class _Nullable cls) 

类的 super_class 指向其父类,如果该类为根类则值为 NULL,metaclass 的 super_class 指向父 metaclass,如果该 metaclass 是根 metaclass则指向该 metaclass 对应的类。

3.2 类名 name

想要获取类名,使用 API 为:

///Returns the name of a class.
const char * _Nonnull class_getName(Class _Nullable cls) 

3.3 version

我们可以使用这个字段来提供类的版本信息。这对于对象的序列化非常有用,它可是让我们识别出不同类定义版本中实例变量布局的改变。

3.4 cache

用于缓存最近使用的方法。

一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。

cache的使用可以在我们发送消息时,不用在 methodLists 中遍历一遍所有的method。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候 runtime 就会优先去 cache 中查找,如果cache没有,才去methodLists 中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率。

3.5 实例对象的内存占用大小 instance_size

想要获取实例对象的内存占用大小,使用 API 为:

///Returns the size of instances of a class.

size_t class_getInstanceSize(Class _Nullable cls) 

...

例:
size_t size = class_getInstanceSize([Person class]);

字节对齐/内存对齐

3.6 成员变量列表 objc_ivar_list

objc_class中,所有的成员变量、属性的信息是放在链表ivars中的。ivars是一个数组,数组中每个元素是指向Ivar(变量信息)的指针。

获取成员变量列表的方法为:

- (void) getAllIvars {
    unsigned int count = 0;
        Ivar * ivars = class_copyIvarList([self class], &count);
        for (unsigned int i = 0; i < count; i ++) {
            Ivar ivar = ivars[i];
            const char * name = ivar_getName(ivar);
            const char * type = ivar_getTypeEncoding(ivar);
            NSLog(@"ivar --->类型为%s,名字为 %s ",type, name);
        }
        free(ivars);
}

输出为:

2021-03-24 17:42:29.189344+0800 TestData[8855:305253] ivar --->类型为@"NSString",名字为 city 
2021-03-24 17:42:29.189518+0800 TestData[8855:305253] ivar --->类型为@"NSString",名字为 _name 
2021-03-24 17:42:29.189664+0800 TestData[8855:305253] ivar --->类型为q,名字为 _age 

3.7 方法列表 objc_method_list

一个类的实例方法,都会存在 Class 中的 objc_method_list 里,获取方法列表的方式如下:

- (void)getAllMethods {
    unsigned int count = 0;
    Method *methodList = class_copyMethodList([self class], &count);
    for (unsigned int i = 0 ; i < count; i++) {
        Method method = methodList[i];
        NSLog(@"method--->%@",NSStringFromSelector(method_getName(method)));
    }
}

控制台输出为:

2021-03-24 17:42:29.189837+0800 TestData[8855:305253] method--->getAllIvars
2021-03-24 17:42:29.189977+0800 TestData[8855:305253] method--->getAllMethods
2021-03-24 17:42:29.190092+0800 TestData[8855:305253] method--->name
2021-03-24 17:42:29.190211+0800 TestData[8855:305253] method--->.cxx_destruct
2021-03-24 17:42:29.190320+0800 TestData[8855:305253] method--->setName:
2021-03-24 17:42:29.190484+0800 TestData[8855:305253] method--->viewDidLoad
2021-03-24 17:42:29.190812+0800 TestData[8855:305253] method--->setAge:
2021-03-24 17:42:29.191131+0800 TestData[8855:305253] method--->age

3.7.1 Method 是什么?

先看看Method 这个 objc_method 结构体:

/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;

struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}   

其中:

name 就是这个方法的方法名,方法名类型为 SEL

方法类型 method_types 是个 char 指针,存储方法的参数类型返回值类型;

method_imp 指向了方法的实现,本质是一个函数指针。

// 添加方法
BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types ); //和成员变量不同的是可以为类动态添加方法。如果有同名会返回NO
//修改方法实现
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp)
// 获取实例方法
Method class_getInstanceMethod ( Class cls, SEL name );
// 获取类方法
Method class_getClassMethod ( Class cls, SEL name );
// 获取所有方法的数组
Method * class_copyMethodList ( Class cls, unsigned int *outCount );
// 获取方法的返回值类型的字符串
char * method_copyReturnType ( Method m );
// 交换两个方法的实现
void method_exchangeImplementations ( Method m1, Method m2 );

3.7.2 SEL 是什么?

SEL 代表方法/函数名,一般叫做选择器,可以把 SEL 看做是方法名字符串

两个类之间,无论它们是父子关系,还是没有关系,只要方法名相同,那么方法的 SEL 就相同,每一个方法都对应着一个SEL,所以在 Objective-C 同一个类中,不能存在同名的方法,即使参数类型不同也不行。

不同的类可以拥有相同的 SEL,不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector寻找对应的 IMP。SEL 就是为了查找方法的最终实现IMP。

关于 SEL 的一些操作函数:

// 返回给定选择器指定的方法的名称
const char * sel_getName ( SEL sel );
// 在Objective-C Runtime系统中注册一个方法,将方法名映射到一个选择器,并返回这个选择器
SEL sel_registerName ( const char *str );
// 比较两个选择器
BOOL sel_isEqual ( SEL lhs, SEL rhs );
// 类实例是否响应指定的selector
BOOL class_respondsToSelector ( Class cls, SEL sel );

3.7.3 IMP 是什么?

IMP 代表函数的具体实现,存储的内容是函数地址。也就是说当找到 IMP 的时候就可以找到函数实现,进而对函数进行调用。

每个方法对应唯一的 SEL,通过 SEL 快速准确地获得对应的 IMP,取得IMP后,就获得了执行这个方法代码了。

关于 IMP 的一些操作函数:

// 获取方法的实现
IMP method_getImplementation ( Method m );
// 设置方法的实现
IMP method_setImplementation ( Method m, IMP imp );
// 替换方法的实现
IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types );
//在block 设置 IMP的具体实现
IMP _Nonnull imp_implementationWithBlock(id _Nonnull block)

3.8 协议列表 protocols

协议列表用来存储 遵守的正式协议

获取一个 Class 中 遵循的协议(不是声明的):

- (void)getAllProtocals {
    unsigned int count = 0;
    Protocol *__unsafe_unretained  * protocalLists = class_copyProtocolList([self class], &count);
    for (unsigned int i = 0; i < count ; i++) {
        Protocol *protocal = protocalLists[i];
        NSLog(@"protocal--->%@",NSStringFromProtocol(protocal));
    }
    free(protocalLists);
}

输出为:

2021-03-24 17:58:52.811344+0800 TestData[9068:318233] protocal--->UIScrollViewDelegate

4.Runtime 的其他用法/API

4.1 增加方法 class_addMethod()

增加方法的 API 为:

BOOL class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)

返回值为 BOOL 类型,意味着添加方法的结果是成功添加还是添加失败。

创建一个 Person 类,其中有实例方法 eatFood :

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject
- (void)eatFood;
@end

......

@implementation Person
- (void)eatFood {
    NSLog(@"Person responds to eatFood function");
}

在 ViewController中有个方法为:

- (void)needAddedToPerson {
    NSLog(@"ACTION ---- needAddedToPerson");
}

假如想要把这个 needAddedToPerson 添加到 Person 中的时候,具体操作应该是:

- (void)personAddMethod {
    //获取 Person 的方法列表
    NSLog(@"方法添加前");
    unsigned int count = 0;
    Method *methodList = class_copyMethodList([Person class], &count);
    for (unsigned int i = 0; i < count; i ++) {
        Method method = methodList[i];
        NSLog(@"Person 中的方法--%@",NSStringFromSelector(method_getName(method)));
    }
    
    Method method = class_getInstanceMethod([self class], @selector(needAddedToPerson));
    IMP methodIMP = method_getImplementation(method);
    const char *type = method_getTypeEncoding(method);
    class_addMethod([Person class], @selector(needAddedToPerson), methodIMP, type);
    
    NSLog(@"方法添加后");
    //获取 Person 的方法列表
    unsigned int count1 = 0;
    Method *methodList1 = class_copyMethodList([Person class], &count1);
    for (unsigned int i = 0; i < count1; i ++) {
        Method method = methodList1[i];
        NSLog(@"Person 中的方法--%@",NSStringFromSelector(method_getName(method)));
    }
}

控制台输出为:

2021-03-27 17:21:09.663939+0800 TestData[1115:24583] 方法添加前
2021-03-27 17:21:09.664293+0800 TestData[1115:24583] Person 中的方法--eatFood
2021-03-27 17:21:09.664812+0800 TestData[1115:24583] 方法添加后
2021-03-27 17:21:09.664979+0800 TestData[1115:24583] Person 中的方法--needAddedToPerson
2021-03-27 17:21:09.665096+0800 TestData[1115:24583] Person 中的方法--eatFood

由上结果可知通过class_addMethod() 成功往 Person 类中添加了 needAddedToPerson 方法。

4.2 方法替换 class_replaceMethod()

增加方法的 API 为:

IMP _Nullable class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)

还是 4.1 的类文件代码,现在需要将 needAddedToPerson 的实现替换掉 Person 的 eatFood 方法:

//用 needAddedToPerson 的实现替换掉 person的 eaFood 方法
    Method method = class_getInstanceMethod([self class], @selector(needAddedToPerson));
    IMP methodIMP = method_getImplementation(method);
    class_replaceMethod([Person class], @selector(eatFood), methodIMP, method_getTypeEncoding(method));
    Person *person = [[Person alloc] init];
    [person eatFood];
    
    //也可以直接用写在 block 中代码实现替换掉 方法实现。
    method_setImplementation(class_getInstanceMethod([Person class], @selector(eatFood)), imp_implementationWithBlock(^(id self, SEL _cmd){
        //block 里面的是方法体的具体实现
        NSLog(@"将needAddedToPerson改为空实现");
    }));
    Person *person1 = [[Person alloc] init];
    [person1 eatFood];

控制台输出为:

2021-03-27 23:07:59.884726+0800 TestData[2054:72385] ACTION ---- needAddedToPerson // [person eatFood];
2021-03-27 23:07:59.885079+0800 TestData[2054:72385] 将needAddedToPerson改为空实现 //[person1 eatFood];

4.3 方法交换 method_exchangeImplementations()

基于上述的例子,再创建一个 Students 类,这个类里面存在一个 - (void)study 方法,我们现在交换一下 needAddedToPerson 方法 和 Studentsstudy 方法。

- (void)exchangeTwoMethods1 {
    Method studyMethod = class_getInstanceMethod([Students class], @selector(study));
    Method needAddedMethod = class_getInstanceMethod([self class], @selector(needAddedToPerson));
    method_exchangeImplementations(studyMethod, needAddedMethod);
    [[Students new] study];
    [self needAddedToPerson];
}

控制台输出为:

2021-03-27 23:27:41.143720+0800 TestData[2266:83218] ACTION ---- needAddedToPerson
2021-03-27 23:27:41.143927+0800 TestData[2266:83218] Students responds to study function

可以看出,两个方法都成功交换。

那么如果用一个方法去交换另一个类中存在于父类中的方法(子类未复写的情况)会怎样呢?

比如,用 needAddedToPerson 去交换 Students 的 eatFood 方法(Students 的 eatFood 由父类 Person 类继承而来)。

- (void)exchangeTwoMethods2s {
    Method studyMethod = class_getInstanceMethod([Students class], @selector(eatFood));
    Method needAddedMethod = class_getInstanceMethod([self class], @selector(needAddedToPerson));
    method_exchangeImplementations(studyMethod, needAddedMethod);
    [[Person new] eatFood];
    [[Students new] eatFood];
    [self needAddedToPerson];
}

控制台输出:

2021-03-27 23:34:10.672759+0800 TestData[2346:87145] ACTION ---- needAddedToPerson  // [[Person new] eatFood];
2021-03-27 23:34:10.672921+0800 TestData[2346:87145] ACTION ---- needAddedToPerson [[Students new] eatFood];
2021-03-27 23:34:10.673040+0800 TestData[2346:87145] Person responds to eatFood function //[self needAddedToPerson];

可以看出来,如果去交换一个未在当前类中复写的、继承自父类的方法的时候,会直接把父类的方法给替换掉,因此也就会导致一个隐患就是也会影响到系统内其他继承自这个父类的对象文件,在方法调用时就会出现问题,比如如果交换了一个 UIView 的某个方法,可能就会影响到 UILabel、UIImageView 的正常使用。

为了避免上面的情况,我们再次优化代码,我们需要在这个类中并没有这个方法的时候,添加 一个方法,并且用空实现来做,然后再去交换方法。

优化代码如下:

- (void)exchangeTwoMethods3 {
    [self exchangeImplementationsInOriClass:[Students class] oriSEL:@selector(study) newClass:[self class] newSEL:@selector(needAddedToPerson)];
    [[Students new] performSelector:@selector(study)];
    [self needAddedToPerson];
}

......

- (BOOL)exchangeImplementationsInOriClass:(Class)oriCls oriSEL:(SEL)oriSel newClass:(Class)newCls newSEL:(SEL)newSel {
    //传入的类不能为空
    if (!oriCls || !newCls) {
        return NO;
    }
    
    Method oriMethod = class_getInstanceMethod(oriCls, oriSel);
    Method newMethod = class_getInstanceMethod(newCls, newSel);
    //传入的需要替换的方法要有意义
    if (!newMethod) {
        return NO;
    }
    
    //往 oriCls 类中添加 oriSel 方法
    BOOL isAddedMethodSuccess = class_addMethod(oriCls, oriSel, method_getImplementation(newMethod), method_getTypeEncoding(newMethod));
    if (isAddedMethodSuccess) {
        //1.如果YES 说明在原先的 oriCls 是没有 oriSel 方法的,而且此时已经添加成功,方法的实现是用的newSel 的实现。
        //2.既然要交换方法,而且原先 oriCls 中并没有 oriSel,所以需要用一个空方法来替代 这个 newCls 中的 newSel
        
        //为 newCls 中的 newSel替换方法的实现
        IMP oriMethodIMP = method_getImplementation(oriMethod) ?: imp_implementationWithBlock(^(id selfObject) {
            NSLog(@"这是一个空方法,我只负责输出一下");
        });//一个空的实现
        
        const char *oriMethodTypeEncoding = method_getTypeEncoding(oriMethod) ?: "v@:";
        class_replaceMethod(newCls, newSel, oriMethodIMP, oriMethodTypeEncoding);//用这个空实现来替换进去方法中
        
    } else {
        //如果添加不成功说明原先的 oriCls 本来确实存在 oriSel,可以直接替换
        method_exchangeImplementations(oriMethod, newMethod);
    }
    return YES;
}

要点:

1.Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换,只有在这段 Method Swilzzling 代码执行完毕之后互换才起作用。

2.先给要替换的方法的类添加一个 Category ,然后在 Category 中的 +(void)load 方法中添加 Method Swizzling 方法,我们用来替换的方法也写在这个Category中。

由于 load 类方法是程序运行时这个类被加载到内存中就调用的一个方法,执行比较早,并且不需要我们手动调用。

3.Swizzling 应该总在 +load 中执行。Swizzling 在 +load 中执行时,不要调用[super load]。如果多次调用了[super load],可能会出现“Swizzle无效”的假象。

4.Swizzling 应该总是在 dispatch_once 中执行, 为了避免 Swizzling 的代码被重复执行,我们可以通过 GCD 的 dispatch_once函数来解决,利用dispatch_once函数内代码只会执行一次的特性。

4.4 分类 category 增加属性

众所周知分类中是无法添加属性的,因为在分类中添加的属性,是无法创建成员变量的,如果在分类的声明中写 @property 只能为其生成 get 和 set 方法的声明。

那么我们如果使用一个全局的变量 比如使用 NSString * _name 呢?


- (NSString *)name {
    return _name;
}

- (void)setName:(NSString *)name {
    _name = name;
}

这样简单一看没有什么问题,但是由于 全局变量在程序执行过程中,只会开辟一次内存,当我们创建多个对象修改其属性值都会修改同一个变量,这样就无法保证像属性一样每个对象都拥有其自己的属性值。这时我们就需要借助 runtime 为分类增加属性的功能了。

使用 Runtime 为 分类添加属性的步骤是:

第一步,创建一个分类,比如给任何一个对象都添加一个name属性。

第二步,在 .h 中用 @property 声明出 name属性,之后 set 和 get 方法的声明会自动生成,

#import "Students.h"

NS_ASSUME_NONNULL_BEGIN

@interface Students (AddProperty)
@property(nonatomic, strong) NSString *name;
@end

第三步,在.m 中重写 -(void)setName 和 -(NSString *)name方法。

#import "Students+AddProperty.h"
#import <objc/runtime.h>

@implementation Students (AddProperty)
static char * const nameKey = "nameKey";
- (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name {
    return  objc_getAssociatedObject(self, &nameKey);
}

这样我们就可以正常的使用分类中添加的属性了。

Students *students = [[Students alloc] init];
students.name = @"张明";
NSLog(@"%@",students.name); //输出 `张明`

5.消息转发机制

在 Objective-C 中,方法的调用其实是通过 objc_msgSend(id self, SEL op, ...) 函数来进行消息转发,其中 self 为消息的接收者, op: 消息的方法名,C 字符串... :参数列表,可以为多个参数。

比如我们调用 [people eatFood] 其实是向 people 发送消息,通过函数 objc_msgSend 来实现方法的调用

objc_msgSend(people,@selector(eatFood))

5.1 方法调用流程/消息机制

当我们对 people 调用 eatFood 方法的时候,具体流程为:

第一步,根据实例对象 people 的 isa 指针,找到它所属的类,也就是 People 类。(如果是调用类方法,是根据类的 isa 指针找到它的元类,毕竟类方法存在于元类的方法列表中)。

第二步,在 People 类的 cache 中(里面存储的平时常用的方法),寻找方法名为 eatFood 的方法。如果有找到,就去方法列表 objc_method_list 中寻找这个方法。(类方法同理)

第三步,如果再 cache 中找到了这个方法,就进行调用;如果在 cache 中没找到这个方法,但是在方法列表中 objc_method_list 找到了,就进行调用方法后,将这个方法添加到 cache 中。

第四步,如果在 cache 中找到,objc_method_list中也没找到,就在其父类中按第一步重新开始这个流程。

第五步,如果在父类中找到了这个方法,就去执行,如果一层一层寻找上去后发现最终没有找到这个方法,就抛出异常 [NSObject(NSObject) doesNotRecognizeSelector:]

为什么是在 NSObject 中执行了doesNotRecognizeSelector ,因为 NSObject 作为基类,是所有类的最顶级的父类。

上述五步可以归纳一个流程图为:

截屏2021-03-29 下午6.46.08.png

5.2 消息转发

在 Objective-C 中进行方法调用的时候,系统会查看这个对象能否接收该消息,如果不能,或者说当这个方法找不到的时候,为了避免应用程序的crash,Runtime 提供了 消息动态解析消息接受者重定向消息重定向 等三步处理消息,这三步也叫做 ”消息转发“

消息动态解析

用到的重写的方法为:

+ (BOOL)resolveInstanceMethod:(SEL)sel {} (针对实例方法)
+ (BOOL)resolveClassMethod:(SEL)sel {}  (针对类方法)

举例:我们在VC中调用一个不存在的 doWork 方法。

VC:
[self performSelector:@selector(doWork)];

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(doWork)) {
        class_addMethod([self class], sel, method_getImplementation(class_getInstanceMethod([self class], @selector(addedMethod))), "v@:");
        return YES;
    } else {
        return [super resolveInstanceMethod:sel];
    }
}

......

- (void)addedMethod {
    NSLog(@"被添加的方法");
}

通过上面的方式,我们在 resolveInstanceMethod: 方法中,利用 class_addMethod 方法,将未实现的 doWork 绑定到 addedMethod 上就能完成转发,最后返回 YES。

注:Type Encodings如何使用请参看 Type Encodings

如果方法动态添加,则消息得到处理,消息转发结束;如果我们这一步的时候,什么也没做,或者说我们需要转发给别的消息接受者,我们就用到了第二步:消息接收者重定向。

注: 这里 +resolveInstanceMethod: 或者 +resolveClassMethod: 无论是返回 YES,还是返回 NO,只要其中没有添加其他函数实现,运行时都会进行下面一步。例如我们在上面的例子中,我们把 class_addMethod() 注释掉,无论返回YES 或者NO,都会执行到下面的消息接受者重定向步骤,也就是调用 - (id)forwardingTargetForSelector:(SEL)aSelector 方法,相反如果这里有了添加方法这个步骤,则下面的消息接受者重定向不会被调用。

消息接受者重定向

用到的重写的方法为:

// 重定向方法的消息接收者,返回一个类或实例对象
- (id)forwardingTargetForSelector:(SEL)aSelector;
// 重定向类方法的消息接收者,返回一个类或实例对象
+ (id)forwardingTargetForSelector:(SEL)aSelector;

还是继续上面的例子,我们注销掉 class_addMethod() 这个步骤,并且创建一个类为 Person 类,其中这个类中实现了 doWork 方法,我们现在需要通过消息接受者重定向,将消息接受者重定向为 Person,也就是让 Person 实例去执行 doWork 方法。

应该怎么做呢?

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(doWork)) {
        return [[Person alloc] init];
        //return nil;//如果返回nil ,会执行第三步
    }
    return [super forwardingTargetForSelector:aSelector];
}

当实现了 - (id)forwardingTargetForSelector:(SEL)aSelector 这个方法并且返回了替代执行的对象,那么就会在 Person 类中去查找 doWork 方法并调用。

这一步是替消息找备用接收者,确定能不能把这条消息转给其他接收者处理,如果返回一个非self的对象,则会把消息发送给该对象,消息转发结束;如果这一步返回的是 nil,那么就要开启消息转发的第三步--消息重定向

消息重定向

消息重定向用到的方法为:

// 获取对象方法函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
// 对象方法消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation;
// 获取类方法函数的参数和返回值类型,返回签名
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
// 类方法消息重定向
+ (void)forwardInvocation:(NSInvocation *)anInvocation;

具体使用如下:

#pragma mark - 返回一个方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(doWork)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

//方法转发具体的实现。与 methodSignatureForSelector 二者相互依赖,只有返回了正确的方法签名,才会执行第二个方法。
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    Person *person = [[Person alloc] init];
    Machine *machine = [[Machine alloc] init];//新创建一个Machine类
    if ([person respondsToSelector:@selector(doWork)]) {
        [anInvocation invokeWithTarget:person ];
    }
    if ([machine respondsToSelector:@selector(doWork)]) {
        [anInvocation invokeWithTarget:machine];
    }
}

一个要求返回一个 方法签名,第二个方法转发具体的实现。二者相互依赖,只有返回了正确的方法签名,才会执行第二个方法。

这次的转发作用和第二次的比较类似,都是将 A 类的某个方法,转发到 B 类的实现中去。不同的是,第三次的转发相对于第二次更加灵活, forwardingTargetForSelector: 只能固定的转发到一个对象; forwardInvocation: 可以让我们转发到多个对象中去。

在方法签名方法中,如果签名为 nil,则消息无法处理,抛出异常;否则,调用 forwardInvocation: 方法,调用成功则消息转发结束,调用失败则消息无法处理,抛出异常。

如果到了第三步仍然没找到对应的实现,就会 crash

至此上面三个流程,就是消息转发的整个流程。

可归纳总结流程图为:

未命名文件.png