OC Runtime指导文档阅读

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天,点击查看活动详情

OC底层原理探索文档汇总

OC Runtime指导文档阅读

主要内容:

  1. 消息发送
  2. 动态方法解析
  3. 消息转发
  4. 类型编码
  5. 属性声明

并没有完全逐字逐句翻译,而是理解性阅读,因为觉得逐字逐句这种方式翻译还不如直接看英文原文

官方文档:Objective-C Runtime Programming Guide

1、介绍

OC语言将尽可能多的决策从编译和链接时推迟到运行时,只要有可能,OC将总是采用动态的方式来解决问题,因此OC不仅需要一个编译器,还需要一个运行时系统来执行编译好的代码。

这个文档介绍了1)NSObject类以及OC程序如何与运行时系统交互,2)特别是着重说明了在运行时加载新类和将消息转发到其他对象的过程,3)它还提供了有关如何在程序运行时查找有关对象的信息

在早期版本中,如果要改变类中的实例变量的布局,就需要重新编译该类的所有子类。 在现行版本,如果改变类中的实例变量的布局,不需要重新编译该类的任何子类。

2、和运行时系统的交互

有三种途径可以和运行时系统交互

2.1 通过OC源代码

大部分情况下,运行时系统在后台自动运行,我们只需要编写和编译OC源代码即可. 当编译OC类和方法时,编译期将会自动创建一些C/C++数据结构和函数来与系统进行交互,这些数据结构和函数就可以实现语言动态特性,这些数据结构包含类和协议、分类、对象等定义的信息,函数用来处理消息。

2.2 通过Foundation框架中NSObject类的方法

  • NSObject类的某些方法会提供查询运行时系统的信息,也就是说NSObject类只是简单的从运行时系统获取信息,可以用来对对象进行一定的自我检查。
  • 程序中绝大部分类都是NSObject类的子类,所以大部分都继承了NSObject类的方法,因而继承了NSObject的行为(NSProxy是个例外)
  • 在一些情况下,NSObject类指定以了一个模板,用于说明应该完成什么事,但是没有具体实现,也就是没有说应该怎么完成。
  • 通过调用NSObject方法,会间接的调用runtime

常见方法

方法.png

2.3 通过直接调用运行时系统的函数

  • 运行时系统是一个有公开接口的动态库,由一些数据结构和函数的集合组成
  • 这些数据结构和函数的声明头文件在/usr/include/objc中
  • 这些函数使用纯C的函数实现OC的功能
  • 这些函数使得访问运行时系统接口和提供开发工具成为可能

直接调用运行时C函数

运行时函数.png

3、消息发送

本章介绍如何将消息表达式转换为 objc_msgSend函数调⽤,以及如何按名称引⽤⽅法。然后解释如何利⽤objc_msgSend,以及如果需要,如何绕过动态绑定。

3.1 objc_msgSend函数

在Objective-C中,消息直到运⾏时才绑定到⽅法实现。编译器转换消息表达式为 [receiver message]

调⽤消息传递函数 objc_msgSend。此函数将接收⽅和消息中提到的⽅法的名称(即⽅法选择器)作为其两个主要参数。 objc_msgSend(receiver, selector)

消息中传递的任何参数也将传递给objc_msgSend:

objc_msgSend(receiver, selector, arg1, arg2, ...)

动态绑定

消息传递函数为动态绑定:

  • 先绑定查找选择器引⽤的过程(⽅法实现)。由于同⼀⽅法可以由不同的类实现,因此它找到的精确过程取决于接受者。
  • 它然后调⽤过程,将接收对象(指向其数据的指针)以及为⽅法。
  • 最后,它传递过程的返回值作为⾃⼰的回报值。

3.1 获取函数指针imp

消息传递的关键在于编译器为每个类和对象构建的结构。每个类结构都包含以下两个基本元素:

  • 指向超类的指针。
  • ⼀个类调度表。此表中的条⽬将⽅法选择器与其标识的⽅法的类特定地址相关联。setOrigin::⽅法的选择器与setOrigin::(实现的过程)的地址相关联,display⽅法的选择器与display的地址关联,依此类推。

创建新对象时,将为其分配内存,并初始化其实例变量。对象变量中的第⼀个变量是指向其类结构的指针。这个名为isa的指针让对象访问其类,并通过该类访问它继承的所有类。

示意图:

消息发送.png

说明:

  1. 当消息发送到对象时,消息传递函数跟随对象的isa指针指向类结构,在该类结构中查找调度表中的⽅法选择器。
  2. 如果在那⾥找不到选择器,objc_msgSend会跟随指向超类的指针并尝试在其调度表中查找选择器。
  3. 连续的失败导致objc_msgSend爬升类层次结构,直到到达NSObject类。
  4. ⼀旦找到选择器,函数就会调⽤表中输⼊的⽅法,并将接收对象的数据结构传递给它。

快速查找流程

为了加快消息传递过程,运⾏时系统在使⽤⽅法时缓存选择器和地址。每个类都有⼀个单独的缓存,它可以包含继承⽅法的选择器以及类中定义的⽅法的选择器。在搜索调度表之前, 消息传递例程⾸先检查接收对象类的缓存(理论上,曾经使⽤过⼀次的⽅法可能会再次使 ⽤)。如果⽅法选择器在缓存中,则消息传递只⽐函数调⽤稍慢。⼀旦⼀个程序运⾏⾜够长的时间来“预热”它的缓存,它发送的⼏乎所有消息都会找到⼀个缓存⽅法。缓存动态增长以适应程序运⾏时的新消息

3.3 使用隐藏函数

当 objc_msgSend找到实现⽅法的过程时,它调⽤该过程并将消息中的所有参数传递给它。它还向过程传递两个隐藏参数:

  • 接收对象
  • 方法选择器

这些参数为每个⽅法实现提供关于调⽤它的消息表达式的两部分的显式信息。它们被称为 “隐藏”,因为它们没有在定义⽅法的源代码中声明。它们在代码编译时被插⼊到实现中。

虽然这些参数没有显式声明,但是源代码仍然可以引⽤它们(就像它可以引⽤接收对象的实例变量⼀样)。⽅法将接收对象引⽤为self,并将其⾃⼰的选择器引⽤为_cmd。在下⾯的⽰例中,_cmd表⽰异常⽅法的选择器,self指向接收到异常消息的对象。

代码:

- strange
{
//_cmd:表示异常的选择器
//self:指向接收到异常消息的对象
id	target = getTheReceiver(); SEL method = getTheMethod();

if ( target == self || method == _cmd ) return nil;
return [target performSelector:method];
}
复制代码

获取方法地址

规避动态绑定的唯⼀⽅法是获取⽅法的地址,然后像函数⼀样直接调⽤它。当⼀个特定的⽅法将被连续执⾏很多次,并且您希望避免每次执⾏该⽅法时消息传递的开销,这种情况可能 ⽐较合适。 使⽤NSObject类中定义的⽅法methodForSelector:,可以请求指向实现⽅法的过程的指针, 然后使⽤该指针调⽤该过程。methodForSelector:返回的指针必须谨慎地转换为正确的函数类型。类型转换中应包括返回类型和参数类型。

代码:

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);
复制代码

说明:

  • 传递给过程的前两个参数是接收对象(self)和⽅法选择器(_cmd)。这些参数隐藏在⽅法语法中,但必须在⽅法作为函数调⽤时显式。
  • 使⽤methodForSelector:绕过动态绑定可以节省消息传递所需的⼤部分时间。但是,只有在特定的消息被重复多次时,节省的空间才会显著,如上⾯所⽰的for循环中。

4、动态方法解析

本章描述如何动态地提供⽅法的实现。

可以实现 resolveInstanceMethod:和 resolveClassMethod: ⽅法,分别为实例和类⽅法的给定选择器动态提供实现。 Objective-C⽅法只是⼀个C函数,它⾄少有两个参数self和_cmd。可以使⽤函数class_ addMethod将函数作为⽅法添加到类中。因此,考虑到以下功能:

void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
复制代码

可以使⽤resolveInstanceMethod将其作为⽅法(称为ResolveThisMethodDynamic)动态添加到类中,如下所⽰:

@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end
复制代码

总结: 转发⽅法(如消息转发中所述)和动态⽅法解析在很⼤程度上是正交的。在转发机制⽣效之 前,类有机会动态解析⽅法。如果调⽤了respondsToSelector:或instanceRespondToSelector:, 则动态⽅法解析器将有机会⾸先为选择器提供IMP。如果实现resolveInstanceMethod:但希望 通过转发机制实际转发特定的选择器,则为这些选择器返回NO。

动态加载

Objective-C程序可以在运⾏时加载和链接新的类和类别。新代码被合并到程序中,并与开始时加载的类和类别相同。 动态加载可以⽤来做很多不同的事情。例如,系统⾸选项应⽤程序中的各个模块是动态加载的。 在Cocoa环境中,通常使⽤动态加载来定制应⽤程序。其他⼈可以编写程序在运⾏时加载的模块,就像Interface Builder加载⾃定义调⾊板和OSX系统⾸选项应⽤程序加载⾃定义⾸选项模块⼀样。可加载模块扩展了应⽤程序的功能。他们以你所允许的⽅式为之做出贡献,但却 ⽆法预料或定义你⾃⼰。您提供框架,但其他⼈提供代码

5、消息转发

向不处理该消息的对象发送消息是错误的。但是,在宣布错误之前,运⾏时系统会给接收对象第⼆次处理消息的机会。

转发

如果将消息发送到不处理该消息的对象,则在宣布错误之前,运⾏时会向该对象发送⼀个forwardInvocation:message,其中NSInvocation对象作为其唯⼀参数,NSInvocation对象将封装原始消息及其传递的参数。 您可以实现forwardInvocation:⽅法来给消息提供默认响应,或者以其他⽅式避免错误。顾名思义,forwardInvocation:通常⽤于将消息转发到另⼀个对象。 要了解转发的范围和意图,请设想以下场景:⾸先,假设您正在设计⼀个可以响应名为negotiate的消息的对象,并且希望其响应包含另⼀种对象的响应。通过将协商消息传递给所实现的协商⽅法主体中的其他对象,可以很容易地完成此操作。 更进⼀步,假设您希望对象对negotiate 消息的响应与在另⼀个类中实现的响应完全相同。实现这⼀点的⼀种⽅法是让您的类从另⼀个类继承该⽅法。然⽽,这样安排事情可能是不可能的。您的类和实现negotiate的类位于继承层次结构的不同分⽀中可能有很好的原因。 即使您的类不能继承协商⽅法,您仍然可以通过实现该⽅法的⼀个版本来“借⽤”该⽅法,该 ⽅法只需将消息传递给另⼀个类的实例:

- (id)negotiate
{
if ( [someOtherObject respondsTo:@selector(negotiate)] ) return [someOtherObject negotiate];
return self;
}
复制代码

这种⽅式可能会有点⿇烦,特别是如果有很多消息需要对象传递给另⼀个对象。你必须实现 ⼀个⽅法来覆盖你想从另⼀个类借⽤的每个⽅法。⽽且,你可能不想知道你在哪⾥写了完整的代码。该集合可能依赖于运⾏时的事件,并且在将来实现新⽅法和类时可能会发⽣变化。

forwardInvocation提供的第⼆个机会是:message为这个问题提供了⼀个不那么特别的解决 ⽅案,⽽且是动态的,⽽不是静态的。它的⼯作原理是这样的:当⼀个对象因为没有与消息中的选择器匹配的⽅法⽽⽆法响应消息时,运⾏时系统通过发送forwardInvocation:message 通知对象。每个对象都从NSObject类继承⼀个forwardInvocation:⽅法。但是,NSObject的 ⽅法版本只是调⽤doesNotRecognizeSelector:。通过重写NSObject的版本并实现⾃⼰的版本,您可以利⽤forwardInvocation:message提供的机会将消息转发到其他对象。

若要转发消息,forwardInvocation:⽅法只需:

  • 确定消息的位置
  • 并将位置与原始消息⼀起发送到那⾥

可以使⽤invokeWithTarget:⽅法发送消息:

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector: [anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}
复制代码

所转发的消息的返回值返回给原始发送⽅。所有类型的返回值都可以传递给发送⽅,包括 id、结构和双精度浮点数。

forwardInvocation:⽅法可以充当未识别消息的分发中⼼,将它们分发给不同的接收⽅。或者它可以是⼀个中转站,将所有消息发送到同⼀个⽬的地。它可以将⼀条消息转换成另⼀条消息,或者简单地“吞下”⼀些消息,这样就不会有响应,也不会出错。forwardInvocation: ⽅法还可以将多个消息合并到单个响应中。forwardInvocation:做什么取决于实现者。然 ⽽,它为链接转发链中的对象提供了机会,为程序设计提供了可能性。

注意:forwardInvocation:⽅法只有在消息没有调⽤名义接收⽅中的现有⽅法时才能处理它们。例如,如果您希望您的对象将negotiate消息转发到另⼀个对象,则它不能有⾃⼰的negotiate⽅法。如果是这样,消息就永远不会到达forwardInvocation:.。

转发和多重继承

可以用消息转发来模拟多重继承,一个对象通过转发来响应消息,看起来就像是继承了其他类而使用其方法

消息转发和多继承.png

说明: Warrior通过消息转发调用了Diplomat的negotiate方法,看上去像是继承自Diplomat一样

代理对象

消息转发可以以一个轻量级的对象(消息代理对象)代表更多的对象进行消息处理。

同时也存在着其它类型的消息代理对象。例如,假设您有个对象需要操作大量的数据——它可能需要创建 一个复杂的图片或者需要从磁盘上读一个文件的内容。创建一个这样的对象是很费时的,您可能希望能推 迟它的创建时间——直到它真正需要时,或者系统资源空闲时。同时,您又希望至少有一个预留的对象和 程序中其它对象交互。 在这种情况下,你可以为该对象创建一个轻量的代理对象。该代理对象可以有一些自己的功能,例如响应 数据查询消息,但是它主要的功能是代表某个对象,当时间到来时,将消息转发给被代表的对象。当代理 对象的 forwardInvocation:方法收到需要转发给被代表的对象的消息时,代理对象会保证所代表的 对象已经存在,否则就创建它。所有发到被代表的对象的消息都要经过代理对象,对程序来说,代理对象 和被代表的对象是一样的。

转发和继承

虽然转发模仿继承,但NSObject类从不混淆两者。例如在 NSObject 类中,方法 respondsToSelector:和 isKindOfClass:只会出现在继承链中,而不是消息转发链中,此时我们如果想要使得这些方法对外透明,也就是消息转发的方法也返回YES,就需要重写这些方法,判断后返回YES。

转发

6、类型编码

为了和运行时系统协作,编译器将方法的返回类型和参数类型都编码成一个字符串,并且和方法选标关联在一起。

编码的获取:使用@encode(类型名)

常见编码格式:

类型编码格式.png

7、属性声明

当编译器遇到一个属性(Property)声明时,编译器将产生一些描述性的元数据,并且属性所在的类、分类或者协议相关联。 这些描述性的元数据就是关于Property的attribute。 您可以通过函数访问元数据,这些函数存在于一些类或协议中,比如通过@encode获得属性的类型编码,将属性的特征(Attribute)作为C字符串的数组返回等。 为了区分property和attribute,我称呼property为属性,attribute为特征

我这里只写了认识和使用,详细的属性特征的描述范例可以直接看原文。

7.1 属性类型和相关函数

属性(Property)类型定义了对描述属性的结构体 objc_property 的不透明的句柄。

结构体:

typedef struct objc_property *Property;
复制代码

API:

API.png

具体实现:

函数实现.png

7.2 属性类型编码

property_getAttributes 函数将返回属性(Property)的名字,@encode 编码,以及其它特征(Attribute)。

具体实现:

@property (nonatomic ,assign ,readonly) int age;
@property (nonatomic ,copy,readwrite) NSString *name;
复制代码

结果:

结果.png

说明:

  1. 以字母 T 开始,接着是@encode 编码和逗号
  2. 如果属性有 readonly 修饰,则字符串中含有 R 和逗号
  3. 如果属性有 copy 或者 retain 修饰,则字符串分别含有 C 或者&,然后是逗号。
  4. 如果属性定义有定制的 getter 和 setter 方法,则字符串中有 G 或者 S 跟着相应的方法名以及逗号
  5. 字符串以 V 然后是属性的名字结束。

猜你喜欢

转载自juejin.im/post/7086262492254437407
今日推荐