iOS底层探索 ---Runtime(一)--- 基础知识

image

本文摘抄自iOS 开发:『Runtime』详解(一)基础知识,不做任何商业用途。优秀的作品要大家一起欣赏

本文主要介绍「Runtime」的相关基础知识。主要有一下几点:
1、什么是Runtime?
2、消息机制的基本原理
3、Runtime中的概念解析(objc_msgSend/Class/Object/MetaClass/Method)
4、Runtime消息转发
5、消息发送以及转发机制总结


1、什么是Runtime?

我们都知道,将源代码转换为可执行程序,主要经过三个步骤:编译链接运行。不同的编译语言,在这三个步骤中所进行的操作又有一些不同。

C语言 作为一门静态语言,在编译阶段就已经确定了所有变量的数据类型,同时也确定好了调用的函数,以及函数的实现。

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

Objective-C 把一些决定性的工作从编译阶段链接阶段推迟到运行时阶段的机制,使得Objective-C变得更加灵活。我们甚至可以在程序运行的时候,动态去修改一个方法的实现,这也为「热更新」提供了可能性。

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

Runtime实际上是一个,这个库使我们可以在程序运行的时候,动态的创建对象、检查对象,修改类和对象的方法。


2、消息机制的基本原理

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

我们来看看方法调用[receiver selector];在「编译阶段」和「运行阶段」分别做了什么?

  1. 编译阶段:[receiver selector];方法被编译器转换为:

    1. objc_msgSend(receiver, selector) --- (不带参数)
    2. objc_msgSend(receiver, selector, org1, org2, ...) --- (带参数)
  2. 运行时阶段:消息接受者receiver寻找对应的selector.

    1. 通过receiverisa 指针找到receiverClass (类)
    2. Class (类)cache (方法缓存)的散列表中寻找对应的IMP (方法实现)
    3. 如果在cache (方法缓存)中没有找到对应的IMP (方法实现)的话,就继续在Class (类)method list (方法列表)中找对应的selector,如果找到,填充到cache (方法缓存)中,并返回selector
    4. 如果在Class (类)中没有找到这个selector,就继续在它的superClass (父类)中寻找;
    5. 一旦找到对应的selector,直接执行receiver对应的selecotr方法实现的IMP (方法实现)
    6. 若找不到对应的selector,消息被转发 或者 临时向receiver添加这个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 结构体的数据结构如下:

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

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;    // 指向父类的指针
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;    // 类的名称
    long version                                             OBJC2_UNAVAILABLE;    // 类的版本信息,默认为 ·0·
    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_class 结构体定义了很多变量:自身的所有实例变量(ivars)所有方法的定义(methodLists)遵守的协议列表(protocols)等。
objc_class 结构体存放的数据称为:元数据(metadata)

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

3.3、Object(对象)

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

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

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY; // 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 指针去找到对应的object_class 结构体,然后在object_class 结构体methodLists (方法列表)中找到我们调用的方法,然后执行。

3.4、Meta Class(元类)

从上面的内容我们看出,对象 (objc_object 结构体)isa 指针指向的是对应的类对象 (object_class 结构体)。那么类对象 (object_class 结构体)isa 指针又指向哪里呢?

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

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

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

在上面2、消息机制的基本原理中,我们探讨了对象方法的调用流程,我们通过对象的isa 指针找到对应的Class (类);然后在Class (类)methodLists (方法列表)中找到对应的selector

类方法的调用流程对象方法的调用流程是差不多的,流程如下:

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

下面看一个示例:

NSString *str = [NSString stringWithFormat:@"%d", 1];

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

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

上面,我们讲解了实例对象 (Object)类 (Class)Meta Class (元类)的基本概念,以及简单的指向关系。下面我们通过一张图来捋一下它们之间的关系。

image

我们先来看isa 指针

  1. 水平方向上,每一级中的实例对象isa 指针指向了对应的类对象,而类对象isa 指针指向了对应的元类。而所有元类isa 指针最终指向了NSObject 元类,因此NSObject 元类也被称为根元类
  2. 垂直方向上,元类isa 指针父类元类isa 指针都指向了根元类。而根元类isa 指针又指向了自己。

我们再来看父类指针

  1. 类对象父类指针指向了父类的类对象父类的类对象又指向了根类的类对象跟类的类对象最终指向了nil
  2. 元类父类指针指向了父类对象的元类父类对象的元类父类指针指向了根类对象的元类,也就是根元类;而根元类父类指针指向了根类对象,最终指向了nil
3.6、Method(方法)

object_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        OBJC2_UNAVAILABLE; // 方法名
    char * _Nullable method_types   OBJC2_UNAVAILABLE; // 方法类型
    IMP _Nonnull method_imp         OBJC2_UNAVAILABLE; // 方法实现
} 

可以看到,objc_method 结构体中包含了method_name (方法名)method_types (方法类型)method_imp (方法实现)。下面我们来详细探索一下这三个变量。

  • SEL method_name; --- 方法名
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

SEL是一个指向objc_selector 结构体的指针,但是Runtime相关头文件中并没有找到明确的定义。不过,通过测试我们可以得出:SEL只是一个保存方法名的字符串。

image

  • 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用来找到函数地址,然后执行函数。

  • char * method_types; --- 方法类型

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

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


4、Runtime 消息转发

在上面消息发送流程中我们提到过:若找不到对应的selector,消息被转发或者临时向receiver添加selector对应的方法实现,否则就会发生崩溃。

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

image

4.1、消息动态解析

Objective-C运行时会调用+resolveInstanceMethod: 或者 +resolveClassMethod:,让你有机会提供一个函数实现。前者对象方法未找到时调用,后者类方法未找到时调用。我们可以通过重写这两个方法,添加其他函数实现,并返回YES,那运行时系统就会重新启动一次消息发送的过程。

主要使用的方法如下:

/** 
 * Adds a new method to a class with a given name and implementation.
 * 
 * @param cls The class to which to add a method.
 * @param name A selector that specifies the name of the method being added.
 * @param imp A function which is the implementation of the new method. The function must take at least two arguments—self and _cmd.
 * @param types An array of characters that describe the types of the arguments to the method. 
 * 
 * @return YES if the method was added successfully, otherwise NO 
 *  (for example, the class already contains a method implementation with that name).
 *
 * @note class_addMethod will add an override of a superclass's implementation, 
 *  but will not replace an existing implementation in this class. 
 *  To change an existing implementation, use method_setImplementation.
 */

/**
 * class_addMethod      向具有给定 名称和实现 的类中添加新方法
 * @param cls           被添加方法的类
 * @param name          selector 方法名
 * @param imp           实现方法的函数指针
 * @param types         描述方法参数类型的字符数组
 * @return
*/
OBJC_EXPORT BOOL
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                const char * _Nullable types) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

eg:

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

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    [self performSelector:@selector(func)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(func)) {
        class_addMethod([self class], sel, (IMP)funcMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void funcMethod(id objc, SEL _cmd) {
    NSLog(@"new funcMethod");
}

@end

输出结果:
2021-05-25 10:16:58.373018+0800 test[22870:647107] new funcMethod

从上面的例子中,我们可以看出,虽然我们没有实现func方法,但是通过重写resolveInstanceMethod:,利用class_addMethod方法添加对象方法实现funcMethod方法,并执行。从打印的结果来看,我们成功调起了funcMethod方法。

大家也注意到了class_addMethodtypes这个参数的传入比较特殊。这里大家可以参考官方文档中关于Type Encodings的说明。「官方文档」

4.2、消息接受者重定向

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

如果当前对象实现了-forwardingTargetForSelector: 或者 +forwardingTargetForSelector:方法,·Runtime·就会调用这个方法,允许我们将消息的·接受者·转发给其他对象。

其中用到的方法如下:

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

注意:
1、类方法 和 实例方法,所对应的·消息接受接重定向·,使用的是两个不同的方法,一个是+,一个是-
2、这里+resolveInstanceMethod: 或者 +resolveClassMethod:无论返回值是YES还是NO,只要其中没有添加其他函数的实现,运行时都会进行下一步。

eg:

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

@interface Person : NSObject

- (void)func;

@end

@implementation Person

- (void)func {
    NSLog(@"Person ---- func");
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    [self performSelector:@selector(func)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES;
}

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

@end

打印结果:
2021-05-25 10:51:27.007218+0800 test[23042:684421] Person ---- func

可以看到,虽然当前ViewController没有实现func方法,+resolveInstanceMethod:也没有添加其他函数实现。但是我们通过forwardingTargetSelector把当前ViewController的方法转发给了Person对象去执行了。打印结果也证明了我们成功实现了转发。

我们通过forwardingTargetSelector可以修改消息的接受者,该方法返回参数是一个对象,如果这个对象不是nil,也不是self,系统会将运行的消息转发给这个对象执行。否则,继续进行下一步:消息重定向

4.3、消息重定向

如果经过消息动态解析消息接受者重定向Runtime系统还是找不到相应的方法实现,从而无法相应消息,Runtime系统会利用-methodSignatureForSelector: 或者 +methodSignatureForSelector:方法获取函数的参数和返回值类型。

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

所以我们可以在forwardInvocation方法中对消息进行转发。

注意:类方法 和 对象方法 消息转发第三步调用的方法同样不一样。

类方法的调用:
1、+ methodSignatureForSelector:
2、+ forwardInvocation:
3、+ doesNotRecognizeSelector:

对象方法的调用:
1、- methodSignatureForSelector:
2、- forwardInvocation:
3、- doesNotRecognizeSelector:

用到的方法:

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

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

eg:

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

@interface Person : NSObject

- (void)func;

@end

@implementation Person

- (void)func {
    NSLog(@"Person ---- func");
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    [self performSelector:@selector(func)];
}

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

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

///获取函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"func"]) {
        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

打印结果:
2021-05-25 13:36:53.710918+0800 test[23878:778445] Person ---- func

可以看到,我们在-forwardInvocation:方法里面让Person对象执行了func函数。

既然-forwardingTargetForSelector:-forwardInvocation:都可以将消息转发给其他对象处理,那么两者之间的区别是什么?
区别在于-forwardingTargetForSelector:只能将消息转发给一个对象;而-forwardInvocation:可以将消息转发给多个对象。

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


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

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

  1. 编译阶段[receiver selector];方法被编译器转换为:

    1. Objc_msgSend(receiver, selector) --- 不带参数
    2. Objc_msdSend(receiver, selector, org1, org2, ...) --- 带参数
  2. 运行时阶段:消息接受者receiver寻找对应的selector

    1. 通过receiverisa 指针找到receiverClass (类)
    2. Class (类)cache (方法缓存)的散列表中寻找对应的IMP (方法实现)
    3. 如果在cache (方法缓存)中没有找到对应的IMP (方法实现)的话,就继续在Class (类)method list (方法列表)中找对应的selector,如果找到,填充到cache (方法缓存)中,并返回selector
    4. 如果在class (类)中没有找到这个selector,就继续在它的superclass (父类)中寻找;
    5. 一旦找到对应的selector,直接执行receiver对应的selector方法实现的IMP (方法实现)
    6. 若找不到对应的selectorRuntime系统进入消息转发机制。
  3. 运行时消息转发阶段

    1. 动态解析:通过重写+resolveInstanceMethod: 或者 +resolveClassMethod:方法,利用class_addMethod方法添加其他函数实现;
    2. 消息接受者重定向:如果上一步没有添加其他函数实现,可在当前对象中利用forwardingTargetForSelector:方法将消息的接受者转发给其他对象;
    3. 消息重定向:如果上一步返回值是nil,则利用methodSignatureForSelector:方法获取函数的参数和返回值类型。
      1. 如果methodSignatureForSelector:返回了一个NSMethodSignature对象(函数签名),Runtime系统就会创建一个NSInvocation对象,并通过forwardInvocation:消息通知当前对象,给予此次消息发送最后一次寻找IMP的机会。
      2. 如果methodSignatureForSelector:返回nil。则Runtime系统会发出doesNotRecognizeSelector:消息,程序也就崩溃了。

比如说上面的第3步,我们可以制造奔溃,然后通过函数调用栈来查看一下:


image

你可能感兴趣的:(iOS底层探索 ---Runtime(一)--- 基础知识)