Runtime 基础

参考文章:

1、Objctive-C Runtime
2、梧雨北辰
3、jackyshan
4、人仙儿a

就是想放张图.jpg

目录

  • Runtime介绍
  • Runtime消息传递
  • Runtime消息转发
  • Runtime之多继承的实现思路

Runtime介绍

因为Objc是一门动态语言,所以它总是想办法把一些决定工作从编译连接推迟到运行时。也就是说只有编译器是不够的,还需要一个运行时系统 (runtime system) 来执行编译后的代码。这就是 Objective-C Runtime 系统存在的意义,Runtime是一套底层纯C语言API。OC代码最终都会被编译器转化为运行时代码,通过消息机制决定函数调用方式,这也是OC作为动态语言使用的基础。

RunTime简称运行时。OC就是运行时机制,其中最主要的是消息机制。对于C语言,函数的调用在编译的时候会决定调用哪个函数。对于OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用

Runtime基本是用 C 和汇编写的,可见苹果为了动态系统的高效而作出的努力。你可以在这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的 runtime 版本,这两个版本之间都在努力的保持一致。

高级编程语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言,但是OC并不能直接编译为汇编语言,而是要先转写为纯C语言再进行编译和汇编的操作,从OC到C语言的过渡就是由runtime来实现的。然而我们使用OC进行面向对象开发,而C语言更多的是面向过程开发,这就需要将面向对象的类转变为面向过程的结构体。

Runtime消息传递

当一个对象调用一个方法的时候,类似 [objc customMethod],编译器会转换成消息发送objc_msgSend(objc, customMethod)。具体传递过程如下:

  • 首先,通过obj的isa指针找到它的 class ;
  • 在 class 的 method list 中查找 customMethod ;
  • 如果 class 中没到 customMethod,继续往它的 superclass 中找 ;
  • 一旦找到 customMethod 这个函数,就去执行它的实现IMP 。

objec_msgSend的方法定义如下:

OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)

消息传递过程中各种函数定义

1、class是一个指向objc_class结构体的指针,即在Runtime中:
typedef struct objc_class *Class; 

下面是Runtime中对objc_class结构体的具体定义:

//usr/include/objc/runtime.h
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
#if !OBJC2

    Class Nullable super_class                              OBJC2UNAVAILABLE;
    const char * Nonnull name                               OBJC2UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * Nullable ivars                  OBJC2UNAVAILABLE;
    struct objc_method_list * Nullable * _Nullable methodLists                    OBJC2UNAVAILABLE;
    struct objc_cache * Nonnull cache                       OBJC2UNAVAILABLE;
    struct objc_protocol_list * Nullable protocols          OBJC2UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

结构体参数说明如下:

  • isa指针:我们会发现objc_class和objc_object同样是结构体,而且都拥有一个isa指针。我们很容易理解objc_object的isa指针指向对象的定义,那么objc_class的指针是怎么回事呢?
    其实,在Runtime中Objc类本身同时也是一个对象。Runtime把类对象所属类型就叫做元类,用于描述类对象本身所具有的特征,最常见的类方法就被定义于此,所以objc_class中的isa指针指向的是元类,每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。
  • super_class指针:super_class指针指向objc_class类所继承的父类,但是如果当前类已经是最顶层的类(如NSProxy),则super_class指针为NULL
  • cache:为了优化性能,objc_class中的cache结构体用于记录每次使用类或者实例对象调用的方法。这样每次响应消息的时候,Runtime系统会优先在cache中寻找响应方法,相比直接在类的方法列表中遍历查找,效率更高。
  • ivars:ivars用于存放所有的成员变量和属性信息,属性的存取方法都存放在methodLists中。
  • methodLists:methodLists用于存放对象的所有成员方法。

通过这个结构体的命名我们不难发现,这个结构体保存的就是类包含的信息。这也说明了类对象其实就是一个结构体。这个结构体存放的数据称为元数据(metadata)。

2、实例(objc_object)
//对象
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;

我们都知道 id 在OC中是表示一个任意类型的类实例,从这里也可以看出,OC中的对象虽然没有明显的使用指针,但是在OC代码被编译转化为C之后,每个OC对象其实都是拥有一个isa的指针的。

类对象中的元数据存储的都是如何创建一个实例的相关信息,那么类对象和类方法应该从哪里创建呢?
就是从isa指针指向的结构体创建,类对象的isa指针指向的我们称之为元类(metaclass),
元类中保存了创建类对象以及类方法所需的所有信息,因此整个结构应该如下图所示:

301129-cc9c0a7ffb147fed.png

补充说明: objc_class 继承于 objc_object,也就是说一个 ObjC 类本身同时也是一个对象,为了处理类和对象的关系,runtime 库创建了一种叫做元类 (Meta Class) 的东西,类对象所属类型就叫做元类,它用来表述类对象本身所具备的元数据。类方法就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。当你发出一个类似 [NSObject alloc] 的消息时,你事实上是把这个消息发给了一个类对象 (Class Object) ,这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类 (root meta class) 的实例。所有的元类最终都指向根元类为其超类。所有的元类的方法列表都有能够响应消息的类方法。所以当 [NSObject alloc] 这条消息发给类对象的时候,objc_msgSend() 会去它的元类里面去查找能够响应消息的方法,如果找到了,然后对这个类对象执行方法调用。

3、元类(Meta Class)

元类(Meta Class)是一个类对象的类。

通过上图我们可以看出整个体系构成了一个自闭环,struct objc_object结构体实例它的isa指针指向类对象,类对象的isa指针指向了元类,super_class指针指向了父类的类对象,而元类的super_class指针指向了父类的元类,那元类的isa指针又指向了自己。

在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。

为了调用类方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念,元类中保存了创建类对象以及类方法所需的所有信息。

任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。

4、Method(objc_method)

Method表示某个方法的类型,即在Runtime中:

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

我们可以在objct_class定义中看到methodLists,其中的元素就是Method,下面是Runtime中objc_method结构体的具体定义:

struct objc_method {
    SEL Nonnull method_name                                 OBJC2UNAVAILABLE;
    char * Nullable method_types                            OBJC2UNAVAILABLE;
    IMP Nonnull method_imp                                  OBJC2UNAVAILABLE;
}

参数说明:

  • method_name:方法名类型SEL
  • method_types: 一个char指针,指向存储方法的参数类型和返回值类型
  • method_imp:本质上是一个指针,指向方法的实现

在这个结构体重,我们已经看到了SELIMP,说明SELIMP其实都是Method的属性。

5、Ivar

Ivar代表类中实例变量的类型,是一个指向ojbcet_ivar的结构体的指针,即在Runtime中:

/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar;

下面是Runtime中对objc_ivar结构体的具体定义:

struct objc_ivar {
    char * Nullable ivar_name                               OBJC2UNAVAILABLE;
    char * Nullable ivar_type                               OBJC2UNAVAILABLE;
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef LP64
    int space                                                OBJC2_UNAVAILABLE;
#endif
}

关于Ivar的深度解释,请看这篇文章

6、SEL

SEL是一个指向objc_selector结构体的指针,即在Runtime中:

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

objc_msgSend函数第二个参数类型为SEL,它是selector在Objective-C中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL:

@property SEL selector;

可以看到selector是SEL的一个实例。

A method selector is a C string that has been registered (or “mapped“) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded.

其实selector就是个映射到方法的C字符串,你可以用 Objective-C 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个 SEL 类型的方法选择器。

selector既然是一个string,我觉得应该是类似className+method的组合,命名规则有两条:

  • 同一个类,selector不能重复
  • 不同的类,selector可以重复

这也带来了一个弊端,我们在写C代码的时候,经常会用到函数重载,就是函数名相同,参数不同,但是这在Objective-C中是行不通的,因为selector只记了method的name,没有参数,所以没法区分不同的method。

注意:

  1. 不同类中相同名字的方法对应的方法选择器是相同的。
  2. 即使是同一个类中,方法名相同而变量类型不同也会导致它们具有相同的方法选择器。

通常我们获取SEL有三种方法:

  1. OC中,使用@selector(“方法名字符串”)
  2. OC中,使用NSSelectorFromString(“方法名字符串”)
  3. Runtime方法,使用sel_registerName(“方法名字符串”)
7、IMP

IMP是一个函数指针,它在Runtime中的定义如下:

/// A pointer to the function of a method implementation.
typedef void (IMP)(void / id, SEL, ... */ ); 

IMP就是指向最终实现程序的内存地址的指针。

当OC发起消息后,最终执行的代码是由IMP指针决定的。利用这个特性,我们可以对代码进行优化:当需要大量重复调用方法的时候,我们可以绕开消息绑定而直接利用IMP指针调起方法,这样的执行将会更加高效,

void (*setter)(id, SEL, BOOL);
int i;
setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
    setter(targetList[i], @selector(setFilled:), YES);

注意:这里需要注意的就是函数指针的前两个参数必须是id和SEL。

8、Category(objc_category)

Category是表示一个指向分类的结构体的指针,其定义如下:

struct category_t { 
    const char *name; 
    classref_t cls; 
    struct method_list_t *instanceMethods; 
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
};

参数说明

  • name:是指 class_name 而不是 category_name。
  • cls:要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对 应到对应的类对象。
  • instanceMethods:category中所有给类添加的实例方法的列表。
  • classMethods:category中所有添加的类方法的列表。
  • protocols:category实现的所有协议的列表。
  • instanceProperties:表示Category里所有的properties,这就是我们可以通过objc_setAssociatedObject和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的。

消息转发

通过前文的理解,我们知道OC的方法被编译之后对应的函数如下:

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

这是我们常见的形式,其实还有如下几种:

  • objc_msgSend_stret
  • objc_msgSendSuper
  • objc_msgSendSuper_stret

如果消息传递给超类就使用带有super的方法,如果返回值是结构体而不是简单值就使用带有stret的值。

运行时阶段的消息发送的详细步骤如下:

  1. 检测selector 是不是需要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会retain,release 这些函数了。
  2. 检测target 是不是nil 对象。ObjC 的特性是允许对一个 nil对象执行任何一个方法不会 Crash,因为会被忽略掉。
  3. 如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,若可以找得到就跳到对应的函数去执行。
  4. 如果在cache里找不到就找一下方法列表methodLists。
  5. 如果methodLists找不到,就到超类的方法列表里寻找,一直找,直到找到NSObject类为止。
  6. 如果还找不到,Runtime就提供了如下三种方法来处理:动态方法解析、消息接受者重定向、消息重定向,这三种方法的调用关系如下图:
image.png

动态方法解析(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)

使用实例解析:

@interface RuntimeTestManager : NSObject

+ (void)test01;
- (void)test02;

@end

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

@implementation RuntimeTestManager

+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(test01)) {
        class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(test01_st)), "v@");
        return true; ///如果返回false,则会走消息接收者重定向,详情见下面的消息接收者重定向
    }
    return [class_getSuperclass(self) resolveClassMethod:sel];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(test02)) {
        class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(test02_st)), "v@");
        return true; ///如果返回false,则会走消息接收者重定向,详情见下面的消息接收者重定向
    }
    return [super resolveInstanceMethod:sel];
}

+ (void)test01_st {
    NSLog(@"+++++++++++");
}

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

@end

调用以及执行结果

///调用
RuntimeTestManager *runtime = [[RuntimeTestManager alloc] init];
[RuntimeTestManager test01];
[runtime test02];
///执行结果
2018-11-27 14:31:43.340386+0800 Runtime[7423:2700369] +++++++++++
2018-11-27 14:31:43.340422+0800 Runtime[7423:2700369] -----------

注意

  • 我们注意到class_addMethod方法中的特殊参数“v@”,具体可参考这里
  • 成功使用动态方法解析还有个前提,那就是我们必须存在可以处理消息的方法,比如上述代码中的test01_sttest02_st

消息接收者重定向

如果上文中的动态方法解析的两个方法resolveInstanceMethod :resolveClassMethod:返回false,消息发送机制就进入了消息转发(Forwarding)的阶段。我们可以使用Runtime通过下面的方法替换消息接收者的为其他对象,从而保证程序的继续执行。

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

实例介绍:

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

@interface ViewController ()

@property(nonatomic, strong) RuntimeTestManager *runtimeManager;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [ViewController performSelector:@selector(test01) withObject:nil];
    RuntimeTestManager *runtime = [[RuntimeTestManager alloc] init];
    self.runtimeManager = runtime;
    [self performSelector:@selector(test02) withObject:nil];
}
///重定向类方法的消息接收者,返回一个类
+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(test01)) {
        return [RuntimeTestManager class];
    }
    return [super forwardingTargetForSelector:aSelector];
}
///重定向实例方法的消息接受者,返回一个实例对象
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(test02)) {
        return self.runtimeManager;
    }
    return [super forwardingTargetForSelector:aSelector];
}

@end

注意

  • RuntimeTestManager就是一个普通的类,声明并实现一个实例方法和类方法。
  • 动态方法解析阶段返回NO时,我们可以通过forwardingTargetForSelector可以修改消息的接收者,该方法返回参数是一个对象,如果这个对象是非nil,非self,系统会将运行的消息转发给这个对象执行。否则,继续查找其他流程。

执行结果

2018-11-27 14:58:47.122145+0800 Runtime[7427:2708367] +++++++++++
2018-11-27 14:58:47.122187+0800 Runtime[7427:2708367] -----------

消息重定向

如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。
首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil ,Runtime则会发出 -doesNotRecognizeSelector:消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation 对象并发送-forwardInvocation:消息给目标对象。

实例介绍:

/////需要从这个方法中获取的信息来创建NSInvocation对象,因此我们必须重写这个方法,为给定的selector提供一个合适的方法签名。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(test01)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;
    
    RuntimeTestManager *runtimeManager = [[RuntimeTestManager alloc] init];
    if ([runtimeManager respondsToSelector:sel]) {
        ///若可以响应,则将消息转发给其他对象处理
        [anInvocation invokeWithTarget:runtimeManager];
    
    } else {
        ///若仍然无法响应,则报错:找不到响应方法
        [self doesNotRecognizeSelector:sel];
    }
}

调用方法以及执行顺序

///调用方法
[self performSelector:@selector(test01) withObject:nil];
///执行结果
2018-11-27 15:30:21.859604+0800 Runtime[7465:2722857] +++++++++++

总结:

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,643评论 0 9
  • 主要参考链接: http://yulingtianxia.com/blog/2014/11/05/objectiv...
    Kevin_Junbaozi阅读 3,228评论 0 11
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,473评论 33 467
  • 参考链接: http://www.cnblogs.com/ioshe/p/5489086.html 简介 Runt...
    乐乐的简书阅读 2,097评论 0 9
  • 在五一的三天一夜的徒步旅行。我突然明白了一些道理,以前不明白的,现在明白了,以前想不通的,现在想通了,我觉得是好事...
    黑风铃阅读 132评论 0 0