iOS Runtime 详解 基础篇

阅读本文将了解下面几个问题:

  1. 什么是 Runtime?
  2. 消息机制的基本原理
  3. Runtime中的数据结构
  4. Runtime消息发送原理
  5. 消息发送流程总结

1 什么是 Runtime?

C/C++ 是静态语言的代表,它们在编译阶段就已经确定好了要调用的函数,以及函数的实现,如果函数未实现就会编译报错。

Objective-C 是一个动态语言,在编译阶段并不知道真正调用的是哪个函数。只有在运行时才知道具体会发生什么。

Objective-C 的动态性,决定了它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。这个运行时系统就是Runtime,它基本上是用C和汇编写的一套API,这个库使C语言有了面向对象的能力。

2 消息机制的基本原理

OC的方法调用都是类似[receiver selector]的形式,其实每次都是一个运行时消息发送过程。

  1. 编译阶段:[receiver selector]方法被编译器转化:
    1.objc_msgSend(receiver,selector)(不带参数)
    2.objc_msgSend(recevier,selector,org1,org2,…)(带参数)

  2. 运行时阶段:消息接收者recever寻找对应的selector:
    1.recever能找到对应的selector,直接执行接收receiver对象的selector方法。
    2.recever找不到对应的selector,消息被转发或者临时向recever添加这个selector对应的实现内容,否则崩溃。

OC调用方法[receiver selector],编译阶段确定了要向哪个接收者发送message消息,但是接收者如何响应决定于运行时的判断。

Objective-C代码在编译和运行阶段会被转化为Runtime方式运行,Objective-C类、对象和方法等都对应了C中的结构体,我们来看一下它们是如何定义的。

3 Runtime中的数据结构

Runtime代码如何查看呢,我们可以通过下面的方式:

1.导入下面的头文件,然后Command +鼠标右键点击,即可进入Runtime的源码文件。

#import <objc/runtime.h>

2.我们也可以通过组合键 [Cmd + Shift + O ] ,搜索相应的文件进入。

下面我们就分析一下Objective-C代码在C中对应的结构。

3.1 objc_msgSend

上面也提到过,Objective-C方法调用在编译时都会转化为对应C函数的调用:objc_msgSend(receiver,selector)

3.2 Object(实例)

objc/objc.h中,我们来看一下Object(对象),是如何定义的:

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

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

我们知道id是一种通用的对象类型,它可以指向属于任何类的对象。在这里id被定义为一个指向 objc_object结构体的指针。

objc_object结构体只包含一个Class类型的isa 指针,也就是说,一个Object(对象)唯一保存的就是它所属Class(类) 的地址。下面我们看一下 Class是如何定义的。

3.3 Class(类)

objc/objc.h中,可以看到Class是一个指向objc_class结构体的指针:

typedef struct objc_class *Class;

objc/runtime.h中,是objc_class结构体的具体定义:

struct objc_class {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

    #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

    } OBJC2_UNAVAILABLE;

我们可以看到objc_class结构体中定义了很多的成员变量:指向父类的指针、类的名字、版本、实例大小、实例变量列表、方法列表、缓存、遵守的协议列表等。这个结构体存放的数据称为元数据(metadata)。

我们还能注意到objc_class结构体中也有一个isa指针。这就说明了Class本身其实也是一个对象,因此我们称之为类对象,类对象在编译期产生,用于创建实例对象,是单例。

3.4 元类(Meta Class):

我们可以发现实例对象和类对象结构体中都拥有一个isa指针,实例对象的isa指针指向他所属的类(Class),那么类对象的isa指针指向哪儿里呢?

类对象的isa指针指向了元类,元类(Meta Class)是一个类对象的类。在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。
为了调用类方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念,元类中保存了创建类对象以及类方法所需的所有信息。

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

上面,我们已经了解了 实例对象(Object)、类(Class)、Meta Class(元类) 的基本概念。

下面,我们通过一张图,来清晰的了解下它们之间的继承关系,以及isa的指向关系:

isa指针指向:

  1. 实例对象的isa指针指向了对应的类对象,而类对象的isa指针指向了对应的元类。
  2. 所有元类的isa指针最终指向了NSObject元类,因此NSObject元类也被称为根元类。根元类的isa指针又指向了自己。

super_class指针指向:

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

3.6 Method(方法)

object_classmethodLists(方法列表)存放的元素就是Method(方法)。

objc/runtime.h中,看下定义:

/// 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                     // 方法实现;
}

下面,我们来了解下它的三个成员变量。

1. SEL(方法名)

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

SEL是一个指向objc_selector结构体的指针,然而我们并不能在Runtime中找到它的结构体的详细定义。Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL

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

3. char * method_types (方法类型)

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

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

3.7 类缓存(objc_cache)

objc_cache用于缓存最近使用的方法。一个类只有一部分方法是常用的,每次调用一个方法之后,这个方法就被缓存到objc_cache中,下次调用时runtime会先在objc_cache中查找,如果objc_cache中没有,才会去methodList中查找。相比直接在类的方法列表中遍历查找,效率更高。

4 深入理解Rutime消息发送

我们在分析了OC语言对应的底层C结构之后,现在可以进一步理解运行时的消息发送机制。先前讲到,OC调用方法被编译转化为如下的形式:

id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)

最后一步中我们提到:若找不到对应的selector,消息被转发或者临时向recevier添加这个selector应的实现方法,否则就会发生崩溃。

当一个方法找不到的时候,Runtime提供了 消息动态解析消息接受者重定向消息重定向 三种方法来处理,这三种方法的调用关系如下图:

4.1 动态方法解析(Dynamic Method Resolution)

所谓动态解析,我们可以理解为通过cache和方法列表没有找到方法时,Runtime为我们提供一次动态添加方法实现的机会,主要用到的方法如下:

//OC方法:
//类方法未找到时调起,可于此添加类方法实现
+ (BOOL)resolveClassMethod:(SEL)sel
//实例方法未找到时调起,可于此添加实例方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel

//Runtime方法:
/**
 运行时方法:向指定类中添加特定方法实现的操作
 @param cls 被添加方法的类
 @param name selector方法名
 @param imp 指向实现方法的函数指针
 @param types imp函数实现的返回值与参数类型
 @return 添加方法是否成功
 */
BOOL class_addMethod(Class _Nullable cls,
                     SEL _Nonnull name,
                     IMP _Nonnull imp,
                     const char * _Nullable types)

下面使用一个示例来说明动态解析:ViewController类中声明方法却未添加实现,我们通过Runtime动态方法解析的操作为其添加方法实现,具体代码如下:

#import "ViewController.h"
#import <objc/runtime.h>

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [[self class] performSelector:@selector(run)];
    [self performSelector:@selector(walk)];
}

//重写父类方法:处理类方法
+ (BOOL)resolveClassMethod:(SEL)sel {
    if(sel == @selector(run)){
        class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(ssl_run)), "v@");
        return YES;   //添加函数实现,返回YES
    }
    return [class_getSuperclass(self) resolveClassMethod:sel];
}

//重写父类方法:处理实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if(sel == @selector(walk)){
        class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(ssl_walk)), "v@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

+ (void)ssl_run {
    NSLog(@"%s ",__func__);
}

- (void)ssl_walk {
    NSLog(@"%s ",__func__);
}

运行程序,代码没有崩溃,并打印如下结果:

+[ViewController ssl_run]
-[ViewController ssl_walk]

class_addMethod 方法中的特殊参数“v@”,具体可参考官方文档中关于 Type Encodings 的说明:点击查看

这里+resolveInstanceMethod:或者 +resolveClassMethod:无论是返回YES,还是返回NO,只要其中没有添加其他函数实现,Runtime都会进行下一步:消息接受者重定向。

4.2 消息接收者重定向

这一步会调用下面两个方法:

//重定向类方法的消息接收者,返回一个类
+ (id)forwardingTargetForSelector:(SEL)aSelector

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

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

下面使用一个示例来说明消息接收者的重定向:

#import "ViewController.h"
#import <objc/runtime.h>

@interface Person : NSObject

@end

@implementation Person

+ (void)run {
    NSLog(@"%s ",__func__);
}

- (void)walk {
    NSLog(@"%s ",__func__);
}

@end

@interface ViewController ()

@property (nonatomic, strong) Person *person;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [[self class] performSelector:@selector(run)];
    [self performSelector:@selector(walk)];
}

//重定向类方法:返回一个类对象
+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(run)) {
         
        return [Person class];
    }
    return [super forwardingTargetForSelector:aSelector];
}

//重定向实例方法:返回类的实例
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(walk)) {
        return self.person;
    }
    return [super forwardingTargetForSelector:aSelector];
}

- (Person *)person {
    if (!_person) {
        _person = [Person new];
    }
    return _person;
}

@end

代码没有崩溃,并打印如下结果:

+[ViewController ssl_run]
-[ViewController ssl_walk]

动态方法解析阶段无效时,我们可以通过forwardingTargetForSelector修改消息的接收者,该方法返回参数是一个对象,如果这个对象是非nil,非self,系统会将运行的消息转发给这个对象执行。否则,进行下一步:消息重定向。

4.3 消息重定向

这一步中首先会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nilRuntime则会发出-doesNotRecognizeSelector: 消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送 -forwardInvocation:消息给目标对象。

看下方法的定义:

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

// 类方法消息重定向
+ (void)forwardInvocation:(NSInvocation *)anInvocation;

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

// 对象方法消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation;

举个例子:

#import "ViewController.h"
#import <objc/runtime.h>

@interface Person : NSObject

@end

@implementation Person

- (void)walk {
    NSLog(@"%s ",__func__);
}

@end

@interface ViewController ()

@end

@implementation ViewController

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

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES;// 返回YES,进入下一步转发
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return nil;// 返回nil,进入下一步转发
}

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

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

    Person *p = [Person new];
    if([p respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:p];// 将消息转发给其他对象处理
    }
    else {
        [self doesNotRecognizeSelector:sel];// 报错,代码崩溃
    }
}

@end

代码没有崩溃,并打印如下结果:

-[Person walk]

这一步中,通过签名,Runtime生成了一个对象anInvocation,发送给了forwardInvocation,我们在forwardInvocation方法里面让Person对象去执行了walk函数。

以上就是Runtime的三次转发流程。

5 消息发送流程总结

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

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

    1. objc_msgSend(receiver,selector)(不带参数)。
    2. objc_msgSend(recevier,selector,org1,org2,…)(带参数)。
  2. 运行时阶段:recevier寻找对应的selector

    1. 通过recevierisa指针找到recevierclass(类)。
    2. Class(类)的cache(方法缓存)中寻找对应的selector
    3. 如果在cache(方法缓存)中没有找到对应的 selector ,就继续在Class(类)的methodList(方法列表)中查找,如果找到,缓存到cache 中,并返回selector
    4. 如果在class(类)中没有找到这个selector,就继续在它的superclass(父类)中寻找。
    5. 一旦找到selector,直接执行相关联的IMP(方法实现)。
    6. 若找不到对应的selectorRuntime系统进入消息转发阶段。
  3. 消息转发阶段:

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

参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335