[转载]Runtime详解

 
Runtime的特性主要是消息(方法)传递,若是消息(方法)在对象中找不到,就进行转发,具体怎么实现的呢。咱们从下面几个方面探寻Runtime的实现机制。
  • Runtime介绍
  • Runtime消息传递
  • Runtime消息转发
  • Runtime应用

Runtime介绍

Objective-C 扩展了 C 语言,并加入了面向对象特性和 Smalltalk 式的消息传递机制。而这个扩展的核心是一个用 C 和 编译语言 写的 Runtime 库。它是 Objective-C 面向对象和动态机制的基石。
Objective-C 是一个动态语言,这意味着它不只须要一个编译器,也须要一个运行时系统来动态得建立类和对象、进行消息传递和转发。理解 Objective-C 的 Runtime 机制能够帮咱们更好的了解这个语言,适当的时候还能对语言进行扩展,从系统层面解决项目中的一些设计或技术问题。了解 Runtime ,要先了解它的核心 - 消息传递 (Messaging)。
  Runtime其实有两个版本: “modern” 和 “legacy”。咱们如今用的 Objective-C 2.0采用的是现行 (Modern) 版的 Runtime 系统,只能运行在 iOS 和 macOS 10.5 以后的 64 位程序中。而 macOS 较老的32位程序仍采用 Objective-C 1 中的(早期)Legacy 版本的 Runtime 系统。这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你须要从新编译它的子类,而现行版就不须要。
  Runtime 基本是用 C 和汇编写的,可见苹果为了动态系统的高效而做出的努力。你能够在 这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的  runtime 版本,这两个版本之间都在努力的保持一致。
  平时的业务中主要是使用 官方Api,解决咱们框架性的需求。
  高级编程语言想要成为可执行文件须要先编译为汇编语言再汇编为机器语言,机器语言也是计算机可以识别的惟一语言,可是OC并不能直接编译为汇编语言,而是要先转写为纯C语言再进行编译和汇编的操做,从OC到C语言的过渡就是由runtime来实现的。然而咱们使用OC进行面向对象开发,而C语言更多的是面向过程开发,这就须要将面向对象的类转变为面向过程的结构体。

Runtime消息传递

一个对象的方法像这样[obj foo],编译器转成消息发送objc_msgSend(obj, foo),Runtime时执行的流程是这样的:
  • 首先,经过obj的isa指针找到它的 class ;
  • 在 class 的 method list 找 foo ;
  • 若是 class 中没到 foo,继续往它的 superclass 中找 ;
  • 一旦找到 foo 这个函数,就去执行它的实现IMP 。
  但这种实现有个问题,效率低。但一个class 每每只有 20% 的函数会被常常调用,可能占总调用次数的 80% 。每一个消息都须要遍历一次objc_method_list 并不合理。若是把常常被调用的函数缓存下来,那能够大大提升函数查询的效率。这也就是objc_class 中另外一个重要成员objc_cache 作的事情 - 再找到foo 以后,把foo 的method_name 做为key ,method_imp做为value 给存起来。当再次收到foo 消息的时候,能够直接在cache 里找到,避免去遍历objc_method_list。从前面的源代码能够看到objc_cache是存在objc_class 结构体中的。
objec_msgSend的方法定义以下:
 
  那消息传递是怎么实现的呢?咱们接下来看看对象(object),类(class),方法(method)这几个的结构体:
//
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;
  1. 系统首先找到消息的接收对象,而后经过对象的isa找到它的类。
  2. 在它的类中查找method_list,是否有selector方法。
  3. 没有则查找父类的method_list。
  4. 找到对应的method,执行它的IMP。
  5. 转发IMP的return值。
下面讲讲消息传递用到的一些概念:
  • 类对象(objc_class)
  • 实例(objc_object)
  • 元类(Meta Class)
  • Method(objc_method)
  • SEL(objc_selector)
  • IMP
  • 类缓存(objc_cache)
  • Category(objc_category)

类对象(objc_class)

Objective-C类是由Class类型来表示的,它其实是一个指向objc_class结构体的指针。
 
查看objc/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;

 struct objc_class结构体定义了不少变量,经过命名不难发现,html

结构体里保存了指向父类的指针、类的名字、版本、实例大小、实例变量列表、方法列表、缓存、遵照的协议列表等,
 一个类包含的信息也不就正是这些吗?没错,类对象就是一个结构体struct objc_class,这个结构体存放的数据称为元数据(metadata),
 该结构体的第一个成员变量也是isa指针,这就说明了Class自己其实也是一个对象,所以咱们称之为类对象,类对象在编译期产生用于建立实例对象,是单例。

实例(objc_object)

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

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

类对象中的元数据存储的都是如何建立一个实例的相关信息,那么类对象和类方法应该从哪里建立呢?git

就是从isa指针指向的结构体建立,类对象的isa指针指向的咱们称之为元类(metaclass),github

 元类中保存了建立类对象以及类方法所需的全部信息,所以整个结构应该以下图所示:

元类(Meta Class)

经过上图咱们能够看出整个体系构成了一个自闭环,struct objc_object结构体实例它的isa指针指向类对象,编程

 
类对象的isa指针指向了元类,super_class指针指向了父类的类对象,
 
而元类的super_class指针指向了父类的元类,那元类的isa指针又指向了本身。

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

在上面咱们提到,全部的类自身也是一个对象,咱们能够向这个对象发送消息(即调用类方法)。
为了调用类方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念,元类中保存了建立类对象以及类方法所需的全部信息。
 
任何NSObject继承体系下的meta-class都使用NSObject的meta-class做为本身的所属类,而基类的meta-class的isa指针是指向它本身。

Method(objc_method)

先看下定义
struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

Method和咱们平时理解的函数是一致的,就是表示可以独立完成一个功能的一段代码,好比:这段代码,就是一个函数。数据结构

咱们来看下objc_method这个结构体的内容:
  • SEL method_name 方法名
  • char *method_types 方法类型
  • IMP method_imp 方法实现
在这个结构体重,咱们已经看到了SEL和IMP,说明SEL和IMP其实都是Method的属性。
咱们接着来看SEL。

SEL(objc_selector)

先看下定义
Objc.h
/// 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:并发

 能够看到selector是SEL的一个实例。
 其实selector就是个映射到方法的C字符串,你能够用 Objective-C 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来得到一个 SEL 类型的方法选择器。
selector既然是一个string,我以为应该是相似className+method的组合,命名规则有两条:
  • 同一个类,selector不能重复
  • 不一样的类,selector能够重复
这也带来了一个弊端,咱们在写C代码的时候,常常会用到函数重载,就是函数名相同,参数不一样,可是这在Objective-C中是行不通的,由于selector只记了method的name,没有参数,因此无法区分不一样的method。
好比:
 
是会报错的。
咱们只能经过命名来区别:
 
在不一样类中相同名字的方法所对应的方法选择器是相同的,即便方法名字相同而变量类型不一样也会致使它们具备相同的方法选择器。

IMP

看下IMP的定义
/// A pointer to the function of a method implementation.  指向一个方法实现的指针
typedef id (*IMP)(id, SEL, ...); 
#endif

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

在iOS的Runtime中,Method经过selector和IMP两个属性,实现了快速查询方法及实现,相对提升了性能,又保持了灵活性。

类缓存(objc_cache)

当Objective-C运行时经过跟踪它的isa指针检查对象时,它能够找到一个实现许多方法的对象。然而,你可能只调用它们的一小部分,而且每次查找时,搜索全部选择器的类分派表没有意义。因此类实现一个缓存,每当你搜索一个类分派表,并找到相应的选择器,它把它放入它的缓存。因此当objc_msgSend查找一个类的选择器,它首先搜索类缓存。这是基于这样的理论:若是你在类上调用一个消息,你可能之后再次调用该消息。
为了加速消息分发, 系统会对方法和对应的地址进行缓存,就放在上述的objc_cache,因此在实际运行中,大部分经常使用的方法都是会被缓存起来的,Runtime系统实际上很是快,接近直接执行内存地址的程序速度。

Category(objc_category)

Category是表示一个指向分类的结构体的指针,其定义以下:
 
   
structcategory_t{constchar*name;
   classref_tcls; 
   structmethod_list_t*instanceMethods;
   structmethod_list_t*classMethods;
   structprotocol_list_t*protocols;
   structproperty_list_t*instanceProperties;
};
name:是指 class_name 而不是 category_name。
cls:要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段经过name对 应到对应的类对象。
instanceMethods:category中全部给类添加的实例方法的列表。
classMethods:category中全部添加的类方法的列表。
protocols:category实现的全部协议的列表。
instanceProperties:表示Category里全部的properties,这就是咱们能够经过objc_setAssociatedObject和objc_getAssociatedObject增长实例变量的缘由,不过这个和通常的实例变量是不同的。

从上面的category_t的结构体中能够看出,分类中能够添加实例方法,类方法,甚至能够实现协议,添加属性,不能够添加成员变量。框架

Runtime消息转发

前文介绍了进行一次发送消息会在相关的类对象中搜索方法列表,若是找不到则会沿着继承树向上一直搜索知道继承树根部(一般为NSObject),若是仍是找不到而且消息转发都失败了就回执行doesNotRecognizeSelector:方法报unrecognized selector错。那么消息转发究竟是什么呢?接下来将会逐一介绍最后的三次机会。
  • 动态方法解析
  • 备用接收者
  • 完整消息转发

动态方法解析

首先,Objective-C运行时会调用 +resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。若是你添加了函数并返回YES, 那运行时系统就会从新启动一次消息发送的过程。
实现一个动态方法解析的例子以下:
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //执行foo函数
    [self performSelector:@selector(foo:)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(foo:)) {//若是是执行foo函数,就动态解析,指定新的IMP
        class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void fooMethod(id obj, SEL _cmd) {
    NSLog(@"Doing foo");//新的foo函数
}

 

打印结果:
 
2018-04-01 12:23:35.952670+0800 ocram[87546:23235469] Doing foo
能够看到虽然没有实现foo:这个函数,可是咱们经过class_addMethod动态添加fooMethod函数,并执行fooMethod这个函数的IMP。从打印结果看,成功实现了。
若是resolve方法返回 NO ,运行时就会移到下一步:forwardingTargetForSelector。

备用接收者

若是目标对象实现了-forwardingTargetForSelector:,Runtime 这时就会调用这个方法,给你把这个消息转发给其余对象的机会。
实现一个备用接收者的例子以下:
#import "ViewController.h"
#import "objc/runtime.h"

@interface Person: NSObject

@end

@implementation Person

- (void)foo {
    NSLog(@"Doing foo");//Person的foo函数
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //执行foo函数
    [self performSelector:@selector(foo)];
}

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

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(foo)) {
        return [Person new];//返回Person对象,让Person对象接收这个消息
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

@end

 

打印结果:
 
2018-04-01 12:45:04.757929+0800 ocram[88023:23260346] Doing foo
能够看到咱们经过forwardingTargetForSelector把当前ViewController的方法转发给了Person去执行了。打印结果也证实咱们成功实现了转发。

完整消息转发

若是在上一步还不能处理未知消息,则惟一能作的就是启用完整的消息转发机制了。
 
首先它会发送-methodSignatureForSelector:消息得到函数的参数和返回值类型。若是-methodSignatureForSelector:返回nil ,Runtime则会发出 -doesNotRecognizeSelector: 消息,程序这时也就挂掉了。若是返回了一个函数签名,Runtime就会建立一个NSInvocation 对象并发送 -forwardInvocation:消息给目标对象。
实现一个完整转发的例子以下:
 
#import "ViewController.h"
#import "objc/runtime.h"

@interface Person: NSObject

@end

@implementation Person

- (void)foo {
    NSLog(@"Doing foo");//Person的foo函数
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //执行foo函数
    [self performSelector:@selector(foo)];
}

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

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

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"foo"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];//签名,进入forwardInvocation
    }
    
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;

    Person *p = [Person new];
    if([p respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:p];
    }
    else {
        [self doesNotRecognizeSelector:sel];
    }

}

@end

 

 

打印结果:
 
2018-04-01 13:00:45.423385+0800 ocram[88353:23279961] Doing foo
从打印结果来看,咱们实现了完整的转发。经过签名,Runtime生成了一个对象anInvocation,发送给了forwardInvocation,咱们在forwardInvocation方法里面让Person对象去执行了foo函数。签名参数v@:怎么解释呢,这里苹果文档 Type Encodings有详细的解释。
以上就是Runtime的三次转发流程。
 
做者:jackyshan 连接:https://www.jianshu.com/p/6ebda3cd8052 來源:简书
相关文章
相关标签/搜索