iOS 开发:『Runtime』详解(一)基础知识

本文首发于个人我的博客:『不羁阁』html

文章连接:bujige.net/blog/iOS-Ru…objective-c


本文用来介绍 iOS开发中 『Runtime』的基础知识。经过本文您将了解到:编程

  1. 什么是 Runtime?
  2. 消息机制的基本原理
  3. Runtime 中的概念解析
  4. Runtime 消息转发
  5. 消息发送以及转发机制总结

1. 什么是 Runtime?

咱们都知道,将源代码转换为可执行的程序,一般要通过三个步骤:编译连接运行。不一样的编译语言,在这三个步骤中所进行的操做又有些不一样。缓存

C 语言 做为一门静态类语言,在编译阶段就已经肯定了全部变量的数据类型,同时也肯定好了要调用的函数,以及函数的实现。bash

Objective-C 语言 是一门动态语言。在编译阶段并不知道变量的具体数据类型,也不知道所真正调用的哪一个函数。只有在运行时间才检查变量的数据类型,同时在运行时才会根据函数名查找要调用的具体函数。这样在程序没运行的时候,咱们并不知道调用一个方法具体会发生什么。数据结构

Objective-C 语言 把一些决定性的工做从编译阶段、连接阶段推迟到 运行时阶段 的机制,使得 Objective-C 变得更加灵活。咱们甚至能够在程序运行的时候,动态的去修改一个方法的实现,这也为大为流行的『热更新』提供了可能性。app

而实现 Objective-C 语言 运行时机制 的一切基础就是 Runtimeide

Runtime 其实是一个库,这个库使咱们能够在程序运行时动态的建立对象、检查对象,修改类和对象的方法。函数


2. 消息机制的基本原理

Objective-C 语言 中,对象方法调用都是相似 [receiver selector]; 的形式,其本质就是让对象在运行时发送消息的过程。post

咱们来看看方法调用 [receiver selector]; 在『编译阶段』和『运行阶段』分别作了什么?

  1. 编译阶段:[receiver selector]; 方法被编译器转换为:
    1. objc_msgSend(receiver,selector) (不带参数)
    2. objc_msgSend(recevier,selector,org1,org2,…)(带参数)
  2. 运行时阶段:消息接受者 recever 寻找对应的 selector
    1. 经过 recevierisa 指针 找到 recevierClass(类)
    2. Class(类)cache(方法缓存) 的散列表中寻找对应的 IMP(方法实现)
    3. 若是在 cache(方法缓存) 中没有找到对应的 IMP(方法实现) 的话,就继续在 Class(类)method list(方法列表) 中找对应的 selector,若是找到,填充到 cache(方法缓存) 中,并返回 selector
    4. 若是在 Class(类) 中没有找到这个 selector,就继续在它的 superClass(父类)中寻找;
    5. 一旦找到对应的 selector,直接执行 recever 对应 selector 方法实现的 IMP(方法实现)
    6. 若找不到对应的 selector,消息被转发或者临时向 recever 添加这个 selector 对应的实现方法,不然就会发生崩溃。

在上述过程当中涉及了好几个新的概念:objc_msgSendisa 指针Class(类)IMP(方法实现) 等,下面咱们来具体讲解一下各个概念的含义。


3. Runtime 中的概念解析

3.1 objc_msgSend

全部 Objective-C 方法调用在编译时都会转化为对 C 函数 objc_msgSend 的调用。objc_msgSend(receiver,selector);[receiver selector]; 对应的 C 函数。

3.2 Class(类)

objc/runtime.h 中,Class(类) 被定义为指向 objc_class 结构体 的指针,objc_class 结构体 的数据结构以下:

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

struct objc_class {
    Class _Nonnull isa;                                          // objc_class 结构体的实例指针

#if !__OBJC2__
    Class _Nullable super_class;                                 // 指向父类的指针
    const char * _Nonnull name;                                  // 类的名字
    long version;                                                // 类的版本信息,默认为 0
    long info;                                                   // 类的信息,供运行期使用的一些位标识
    long instance_size;                                          // 该类的实例变量大小;
    struct objc_ivar_list * _Nullable ivars;                     // 该类的实例变量列表
    struct objc_method_list * _Nullable * _Nullable methodLists; // 方法定义的列表
    struct objc_cache * _Nonnull cache;                          // 方法缓存
    struct objc_protocol_list * _Nullable protocols;             // 遵照的协议列表
#endif

};
复制代码

从中能够看出,objc_class 结构体 定义了不少变量:自身的全部实例变量(ivars)、全部方法定义(methodLists)、遵照的协议列表(protocols)等。objc_class 结构体 存放的数据称为 元数据(metadata)

objc_class 结构体 的第一个成员变量是 isa 指针isa 指针 保存的是所属类的结构体的实例的指针,这里保存的就是 objc_class 结构体的实例指针,而实例换个名字就是 对象。换句话说,Class(类) 的本质其实就是一个对象,咱们称之为 类对象

3.3 Object(对象)

接下来,咱们再来看看 objc/objc.h 中关于 Object(对象) 的定义。 Object(对象)被定义为 objc_object 结构体,其数据结构以下:

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa;       // objc_object 结构体的实例指针
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;
复制代码

这里的 id 被定义为一个指向 objc_object 结构体 的指针。从中能够看出 objc_object 结构体 只包含一个 Class 类型的 isa 指针

换句话说,一个 Object(对象)惟一保存的就是它所属 Class(类) 的地址。当咱们对一个对象,进行方法调用时,好比 [receiver selector];,它会经过 objc_object 结构体isa 指针 去找对应的 objc_class 结构体,而后在 objc_class 结构体methodLists(方法列表) 中找到咱们调用的方法,而后执行。

3.4 Meta Class(元类)

从上边咱们看出,对象(objc_object 结构体)isa 指针 指向的是对应的 类对象(objc_class 结构体)。那么 类对象(objc_class 结构体)的 isa 指针 又指向什么呢?

objc_class 结构体isa 指针 实际上指向的的是 类对象 自身的 Meta Class(元类)

那么什么是 Meta Class(元类)

Meta Class(元类) 就是一个类对象所属的 。一个对象所属的类叫作 类对象,而一个类对象所属的类就叫作 元类

Runtime 中把类对象所属类型就叫作 Meta Class(元类),用于描述类对象自己所具备的特征,而在元类的 methodLists 中,保存了类的方法链表,即所谓的「类方法」。而且类对象中的 isa 指针 指向的就是元类。每一个类对象有且仅有一个与之相关的元类。

2. 消息机制的基本原理 中咱们讲解了 对象方法的调用过程,咱们是经过对象的 isa 指针 找到 对应的 Class(类);而后在 Class(类)method list(方法列表) 中找对应的 selector

类方法的调用过程 和对象方法调用差很少,流程以下:

  1. 经过类对象 isa 指针 找到所属的 Meta Class(元类)
  2. Meta Class(元类)method list(方法列表) 中找到对应的 selector;
  3. 执行对应的 selector

下面看一个示例:

NSString *testString = [NSString stringWithFormat:@"%d,%s",3, "test"];
复制代码

上边的示例中,stringWithFormat: 被发送给了 NSString 类NSString 类 经过 isa 指针 找到 NSString 元类,而后在该元类的方法列表中找到对应的 stringWithFormat: 方法,而后执行该方法。

3.5 实例对象、类、元类之间的关系

上面,咱们讲解了 实例对象(Object)类(Class)Meta Class(元类) 的基本概念,以及简单的指向关系。下面咱们经过一张图来清晰地表示出这种关系。

咱们先来看 isa 指针

  1. 水平方向上,每一级中的 实例对象isa 指针 指向了对应的 类对象,而 类对象isa 指针 指向了对应的 元类。而全部元类的 isa 指针 最终指向了 NSObject 元类,所以 NSObject 元类 也被称为 根源类
  2. 垂直方向上, 元类isa 指针父类元类isa 指针 都指向了 根元类。而 根源类isa 指针 又指向了本身。

咱们再来看 父类指针

  1. 类对象父类指针 指向了 父类的类对象父类的类对象 又指向了 根类的类对象根类的类对象 最终指向了 nil。
  2. 元类父类指针 指向了 父类对象的元类父类对象的元类父类指针指向了 根类对象的元类,也就是 根元类。而 根元类父亲指针 指向了 根类对象,最终指向了 nil。

3.6 方法(Method)

objc_class 结构体methodLists(方法列表)中存放的元素就是 方法(Method)

先来看下 objc/runtime.h 中,表示 方法(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;                    // 方法名
    char * _Nullable method_types;               // 方法类型
    IMP _Nonnull method_imp;                     // 方法实现
};
复制代码

能够看到,objc_method 结构体 中包含了 方法名(method_name)方法类型(method_types)方法实现(method_imp)。下面,咱们来了解下这三个变量。

  1. SEL method_name; // 方法名
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
复制代码

SEL 是一个指向 objc_selector 结构体 的指针,可是在 runtime 相关头文件中并无找到明确的定义。不过,经过测试咱们能够得出: SEL 只是一个保存方法名的字符串。

SEL sel = @selector(viewDidLoad);
NSLog(@"%s", sel);              // 输出:viewDidLoad
SEL sel1 = @selector(test);
NSLog(@"%s", sel1);             // 输出:test
复制代码
  1. IMP method_imp; // 方法实现
/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif
复制代码

IMP 的实质是一个函数指针,所指向的就是方法的实现。IMP用来找到函数地址,而后执行函数。

  1. char * method_types; // 方法类型

方法类型 method_types 是个字符串,用来存储方法的参数类型和返回值类型。

到这里, Method 的结构就已经很清楚了,MethodSEL(方法名)IMP(函数指针) 关联起来,当对一个对象发送消息时,会经过给出的 SEL(方法名) 去找到 IMP(函数指针) ,而后执行。


4. Runtime 消息转发

2. 消息机制的基本原理 最后一步中咱们提到:若找不到对应的 selector,消息被转发或者临时向 recever 添加这个 selector 对应的实现方法,不然就会发生崩溃。

当一个方法找不到的时候,Runtime 提供了 消息动态解析消息接受者重定向消息重定向 等三步处理消息,具体流程以下图所示:

4.1 消息动态解析

Objective-C 运行时会调用 +resolveInstanceMethod: 或者 +resolveClassMethod:,让你有机会提供一个函数实现。咱们能够经过重写这两个方法,添加其余函数实现,并返回 YES, 那运行时系统就会从新启动一次消息发送的过程。

主要用的的方法以下:

// 类方法未找到时调起,能够在此添加类方法实现
+ (BOOL)resolveClassMethod:(SEL)sel;
// 对象方法未找到时调起,能够在此对象方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel;

/** * class_addMethod 向具备给定名称和实现的类中添加新方法 * @param cls 被添加方法的类 * @param name selector 方法名 * @param imp 实现方法的函数指针 * @param types imp 指向函数的返回值与参数类型 * @return 若是添加方法成功返回 YES,不然返回 NO */
BOOL class_addMethod(Class cls, SEL name, IMP imp, 
                const char * _Nullable types);
复制代码

举个例子:

#import "ViewController.h"
#include "objc/runtime.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 执行 fun 函数
    [self performSelector:@selector(fun)];
}

// 重写 resolveInstanceMethod: 添加对象方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(fun)) { // 若是是执行 fun 函数,就动态解析,指定新的 IMP
        class_addMethod([self class], sel, (IMP)funMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void funMethod(id obj, SEL _cmd) {
    NSLog(@"funMethod"); //新的 fun 函数
}

@end
复制代码

打印结果: 2019-06-12 10:25:39.848260+0800 runtime[14884:7977579] funMethod

从上边的例子中,咱们能够看出,虽然咱们没有实现 fun 方法,可是经过重写 resolveInstanceMethod: ,利用 class_addMethod 方法添加对象方法实现 funMethod 方法,并执行。从打印结果来看,成功调起了funMethod 方法。

咱们注意到 class_addMethod 方法中的特殊参数 v@:,具体可参考官方文档中关于 Type Encodings 的说明:传送门

4.2 消息接受者重定向

若是上一步中 +resolveInstanceMethod: 或者 +resolveClassMethod: 没有添加其余函数实现,运行时就会进行下一步:消息接受者重定向。

若是当前对象实现了 -forwardingTargetForSelector:,Runtime 就会调用这个方法,容许咱们将消息的接受者转发给其余对象。

用到的方法:

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

注意:这里+resolveInstanceMethod: 或者 +resolveClassMethod:不管是返回 YES,仍是返回 NO,只要其中没有添加其余函数实现,运行时都会进行下一步。

举个例子:

#import "ViewController.h"
#include "objc/runtime.h"

@interface Person : NSObject

- (void)fun;

@end

@implementation Person

- (void)fun {
    NSLog(@"fun");
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 执行 fun 方法
    [self performSelector:@selector(fun)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES; // 为了进行下一步 消息接受者重定向
}

// 消息接受者重定向
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(fun)) {
        return [[Person alloc] init];
        // 返回 Person 对象,让 Person 对象接收这个消息
    }
    
    return [super forwardingTargetForSelector:aSelector];
}
复制代码

打印结果: 2019-06-12 17:34:05.027800+0800 runtime[19495:8232512] fun

能够看到,虽然当前 ViewController 没有实现 fun 方法,+resolveInstanceMethod: 也没有添加其余函数实现。可是咱们经过 forwardingTargetForSelector 把当前 ViewController 的方法转发给了 Person 对象去执行了。打印结果也证实咱们成功实现了转发。

咱们经过 forwardingTargetForSelector 能够修改消息的接收者,该方法返回参数是一个对象,若是这个对象是否是 nil,也不是 self,系统会将运行的消息转发给这个对象执行。不然,继续进行下一步:消息重定向流程。

4.3 消息重定向

若是通过消息动态解析、消息接受者重定向,Runtime 系统仍是找不到相应的方法实现而没法响应消息,Runtime 系统会利用 -methodSignatureForSelector: 方法获取函数的参数和返回值类型。

  • 若是 -methodSignatureForSelector: 返回了一个 NSMethodSignature 对象(函数签名),Runtime 系统就会建立一个 NSInvocation 对象,并经过 -forwardInvocation: 消息通知当前对象,给予这次消息发送最后一次寻找 IMP 的机会。
  • 若是 -methodSignatureForSelector: 返回 nil。则 Runtime 系统会发出 -doesNotRecognizeSelector: 消息,程序也就崩溃了。

因此咱们能够在 -forwardInvocation: 方法中对消息进行转发。

用到的方法:

// 获取函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

// 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation;
复制代码

举个例子:

#import "ViewController.h"
#include "objc/runtime.h"

@interface Person : NSObject

- (void)fun;

@end

@implementation Person

- (void)fun {
    NSLog(@"fun");
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 执行 fun 函数
    [self performSelector:@selector(fun)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES; // 为了进行下一步 消息接受者重定向
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return nil; // 为了进行下一步 消息重定向
}

// 获取函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"fun"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    
    return [super methodSignatureForSelector:aSelector];
}

// 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;   // 从 anInvocation 中获取消息
    
    Person *p = [[Person alloc] init];

    if([p respondsToSelector:sel]) {   // 判断 Person 对象方法是否能够响应 sel
        [anInvocation invokeWithTarget:p];  // 若能够响应,则将消息转发给其余对象处理
    } else {
        [self doesNotRecognizeSelector:sel];  // 若仍然没法响应,则报错:找不到响应方法
    }
}
@end
复制代码

打印结果: 2019-06-13 13:23:06.935624+0800 runtime[30032:8724248] fun

能够看到,咱们在 -forwardInvocation: 方法里面让 Person 对象去执行了 fun 函数。

既然 -forwardingTargetForSelector:-forwardInvocation: 均可以将消息转发给其余对象处理,那么二者的区别在哪?

区别就在于 -forwardingTargetForSelector: 只能将消息转发给一个对象。而 -forwardInvocation: 能够将消息转发给多个对象。

以上就是 Runtime 消息转发的整个流程。

结合以前讲的 2. 消息机制的基本原理,就构成了整个消息发送以及转发的流程。下面咱们来总结一下整个流程。


5. 消息发送以及转发机制总结

调用 [receiver selector]; 后,进行的流程:

  1. 编译阶段:[receiver selector]; 方法被编译器转换为:
    1. objc_msgSend(receiver,selector) (不带参数)
    2. objc_msgSend(recevier,selector,org1,org2,…)(带参数)
  2. 运行时阶段:消息接受者 recever 寻找对应的 selector
    1. 经过 recevierisa 指针 找到 recevierclass(类)
    2. Class(类)cache(方法缓存) 的散列表中寻找对应的 IMP(方法实现)
    3. 若是在 cache(方法缓存) 中没有找到对应的 IMP(方法实现) 的话,就继续在 Class(类)method list(方法列表) 中找对应的 selector,若是找到,填充到 cache(方法缓存) 中,并返回 selector
    4. 若是在 class(类) 中没有找到这个 selector,就继续在它的 superclass(父类)中寻找;
    5. 一旦找到对应的 selector,直接执行 recever 对应 selector 方法实现的 IMP(方法实现)
    6. 若找不到对应的 selector,Runtime 系统进入消息转发机制。
  3. 运行时消息转发阶段:
    1. 动态解析:经过重写 +resolveInstanceMethod: 或者 +resolveClassMethod:方法,利用 class_addMethod方法添加其余函数实现;
    2. 消息接受者重定向:若是上一步添加其余函数实现,可在当前对象中利用 -forwardingTargetForSelector: 方法将消息的接受者转发给其余对象;
    3. 消息重定向:若是上一步没有返回值为 nil,则利用 -methodSignatureForSelector:方法获取函数的参数和返回值类型。
      1. 若是 -methodSignatureForSelector: 返回了一个 NSMethodSignature 对象(函数签名),Runtime 系统就会建立一个 NSInvocation 对象,并经过 -forwardInvocation: 消息通知当前对象,给予这次消息发送最后一次寻找 IMP 的机会。
      2. 若是 -methodSignatureForSelector: 返回 nil。则 Runtime 系统会发出 -doesNotRecognizeSelector: 消息,程序也就崩溃了。

参考资料


以上就是 iOS 开发:『Runtime』详解(一):基础知识 的全部内容了。 整篇文章主要就讲了一件事:消息发送以及转发机制的原理和流程。这也是 Runtime 系统的工做原理。

下一篇笔者准备讲一下『Runtime』的黑魔法 Method Swizzling


iOS 开发:『Runtime』详解 系列文章:

还没有完成:

  • iOS 开发:『Runtime』详解(五)Crash 防御系统
  • iOS 开发:『Runtime』详解(六)Objective-C 2.0 结构解析
  • iOS 开发:『Runtime』详解(七)KVO 底层实现
相关文章
相关标签/搜索