runtime 运行时--篇章二
运行时系统 – 篇章二
[TOC]
这一章讲,运行时系统的结构。runtime
开源代码可以点击这里下载,已经编译好了,下载即可直接运行。
运行时的组成部分
OC运行时系统由2个主要部分构成:编译器和运行时系统库
编译器
像C语言标准函数库为C语言程序提供标准API和实现代码一样。运行时系统库也会为OC面向对象的特性提供标准API和实现代码。这种库与所有OC程序链接。
编译器的作用是接受输入的源代码,生成使用了运行时系统库的代码,从而得到合法、可执行的OC程序。而不管是OC
还是Swift
,都是采用Clang作为编译器前端,LLVM(Low level vritual machine)
作为编译器后端。
编译器前端
编译器前端的任务是进行:语法分析,语义分析,生成中间代码(intermediate representation )。在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。编译器后端
编译器后端会进行机器无关的代码优化,生成机器语言,并且进行机器相关的代码优化。
且看编译器如何为OC的类和对象生成可执行的代码,以及如何实现对象消息:
生成对象消息传递代码
当编译器解析对象消息(发送消息的表达式)时,如:
1 | [接收器 消息] |
它会生成并调用运行时系统库中函数objc_msgSend()
的代码。该函数将接收器、选择器、消息传递的参数 作为输入参数。
编译器会将所有的消息传递表达式转换为运行时系统函数代码,并提供相应的参数。而每条消息都是以动态方式处理的,所以又回到了之前所讲的:接收器的类型和方法实现代码都是在运行时决定的。
1 | objc_msgSend(receiver, selector) |
- 首先它找到selector对应的方法实现。因为同一个方法可能在不同的类中有不同的实现,所以我们需要依赖于接收者的类来找到的确切的实现。
- 它调用方法实现,并将接收者对象及方法的所有参数传给它。
- 最后,它将实现返回的值作为它自己的返回值。
对于源代码中的类和对象来说,编译器创建了执行对象消息操作所需的数据结构。
生成类和对象代码
当编译器解析含有类定义和对象的OC源代码时,会生成相应的运行时数据结构。
OC的类与运行时系统库中的Class
数据结构对应。在objc.h
中,Class
是指向objc_class
类型的结构体指针。
1 | // An opaque type that represents an Objective-C class. |
不透明数据类型是一种接口定义不完整的C语言结构类型。提供了一种数据隐藏模式,其变量只能由专门为它们定义的函数访问。使用运行时系统库中的函数就可以访问Class
(即objc_class
)数据类型
同样,OC对象也拥有,编译器创建的运行时数据类型:
1 | struct objc_object { |
当我们创建对象时,系统会为objc_object
类型的结构体分配内存,这种数据由isa指针后面跟实例变量的数据构成。
objc_object
类型也有Class
类型的isa指针(也是就是说每个对象都有一个isa指针,指向类对象)。所有的OC对象和类都是以isa指针开头,isa指针是指向objc_class
结构体的指针。
让我们来看看id数据类型的定义:
1 | // 在objc.h中 |
显而易见,id
就是指向objc_object
结构体的指针。
查看运行时系统的数据结构
OC 对象运行时会转化为objc_object
类型的结构体,类会转化为objc_class
的结构体。下面我们通过OC代码来观察一下:
1 | @interface ANPerson : NSObject |
1 | // 对象 |
上述代码中,我们将
ANPerson
实例化对象person1
的内容打印了出来。person1
对象的第一个值竟然和类的首地址相同。这样正附和之前介绍的objc_object
结构体,objc_object
第一个成员就是Class
类型的isa
指针(isa指针是指向该类的内存地址)。而第二个值从打印值来看,正是该对象的实例变量。当编译器解析对象时,就会转化为objc_object
的结构体,这一下子是不是很明了了呢?对于类结构,从打印结果来看,是有2个值,第一个值和该类的元类地址一样,
objc_class
数据结构的第一个成员正是isa指针,是一个指向元类的指针。而另外一个值是该类的父类的首地址(也就是指向该类的父类指针)。你可能发现,打印isa指针的值(40869707 01000000)为啥和类的首地址(0x107978640)不一样?
原因:程序在模拟器上运行的,而MAC是以低字节序存储数据的。内存地址从左到右按照由低到高的顺序
低字节序:将低序字节存储在起始地址(地址低位存储值的低位,地址高位存储值的高位)
高字节序:将高序字节存储在起始地址(地址低位存储值的高位,地址高位存储值的低位)person1
一共占16个字节,isa
占8个字节,一个字节2个十六进制,反转过来,(07978640)刚刚好.对于整个类的层次结构,我们会更清晰:
运行时系统库(runtime)
类相关操作函数
对象的类定义 – objc_getClass
对象的元类 – objc_getMetaClass
类的父类 – class_getSuperclass
类的名字 – class_getName
类的版本信息 – class_getVersion
以字节为单位的类尺寸 – class_getInstanceSize
类的实例变量列表 – class_copyIvarList
类的方法列表 – class_copyMethodList
类的协议列表 – class_copyProtocolList
类的属性列表 – class_copyPropertyList
使用runtime创建类
1 | void testAddMethod(id self , IMP _cmd) |
关于Type Encodings的介绍,可以上官网查看。
运行时对象消息的传递
篇章一我们已经认识了SEL
、IMP
,现在再来认识Method
。
Method
用于表示类定义中的方法
1 | typedef struct objc_method *Method; struct |
method_name
是一个类型为SEL的变量,是方法的名称。method_types
方法参数的数据类型(具体类型可看Type Encodings)method_imp
是一个IMP的变量,在方法调用时提供方法的地址
该结构体中包含一个SEL
和IMP
,实际上相当于在SEL
和IMP
之间作了一个映射。一点找到了SEL
就找到了IMP
.
为了加速消息的处理,运行时系统不仅缓存(objc_cache
)使用过的selector及对应的方法的地址。而且还实现了分发表分派机制。
每个objc_class
(类实例) 都有一个objc_method_list
类型的二维指针。而每一个objc_method_list
(分发表)的结构体都含有一个objc_method
结构体类型的一维数组method_list
(指向objc_method
数据结构指针的数组),且只有一个元素。
objc_method_list
的定义如下图:
1 | struct objc_method_list { |
如下图,运行时会先根据selector
搜索类方法缓存查找方法IMP函数指针。如果没有找到,就会在其类的分发表中寻找selector
。并依此,一直沿着类的继承体系到达NSObject类,直到找到为止,它就会将会这个方法(Method
)存储到缓存中,方便下次使用。如果还没有找到就会走消息转发流程。
调用类方法
OC中的类也是对象,即我们常说的类对象,所以它也能接受消息。
1 | [NSObject alloc] |
runtime
是如何找到并调用类方法的呢?正如之前介绍的,对象的isa指针变量会指向描述该对象的类,类对象的isa指针变量是指向描述类(及其类方法)的元类。基类的元类的父类指针指向该基类本身。
runtime
是通过元类来实现的,元类是一个特殊的类对象。在元类中,每个类都有一个类方法列表。- 当源代码向对象发送消息时,
runtime
会通过想要的类实例方法分发表,获得合适的实例方法实现代码并跳转执行。 - 当源代码向类发送消息时,
runtime
会通过该类的元类类方法分发表,获得合适的类方法实现代码并跳转执行。
- 当源代码向对象发送消息时,
参考:
- Objective-C Runtime Programming Guide
- Objective-C Runtime 运行时之一:类与对象
- Objective-C Runtime 运行时之三:方法与消息
- 深入浅出Cocoa之消息