Objective-C的runtime语言使它具备了动态语言的特性,也就是平时所说的“运行时”。在runtime的基础上,可以做很多平时难以想到事,或者化简原先 较为繁杂的解决方案。
相对于静态语言,比如C以下程序
1 |
|
执行clang -c
进行编译后,获取符号表nm run.o
,可以得到全局唯一的符号_run
,对函数run的调用直接参考链接后_run符号在代码段的地址
1 | 0000000000000010 T _main |
对比Objective-C的以下函数
1 | @implementation Dog : NSObject |
执行clang -rewrite-objc main.m
将其转换成底层C++文件后可以得到
1 | int main(int argc, const char * argv[]) { |
可以看到,对Objective-C编译前期,会将内部的方法调用,转换成调用objc_msgSend
。也就是说,编译完成后,方法地址是不能确定的,需要在运行时,通过Selector进行查找,而这正是runtime的关键,也就是发送消息机制。
runtime的基本要素
如上面例子所示,在编译后[dog run]
被编译器转化成了
1 | ((void (*)(id, SEL))(void *)objc_msgSend)((id)dog, sel_registerName("run")); |
将上面的情况抽取成统一的说法就是,在编译器编译后[receiver message]
会被转化成以下形式
1 | objc_msgSend(receiver, selector) |
objc_msgSend
是一个消息发送函数,它以消息接收者和方法名作为基础参数。
在有参数的情况下,则会被转换为
1 | objc_msgSend(receiver, selector, arg1, arg2, ...) |
消息的接收者receiver在接受到消息后,查找对应selector的实现,根据查找的结果可以进行若干种种不同的处理。
更深层的了解,需要了解下对应的数据结构
id
上文中objc_msgSend
的第一个参数有个强转类型,即id。id是可以指向对象的万能指针,查看runtime源码,得知其定义如下:
1 | typedef struct objc_object *id; |
根据union
联合的存储空间以大成员的存储空间计算性质,可以猜测isa_t
的作用只是真不同位数处理器的优化,我们可以直接这样表示:
1 | struct objc_object { |
可以看出,id
是一个指向objc_object
结构体的指针(注意,在runtime中对象可以用结构体进行表示)。objc_object
结构体包含了Class isa
成员,而isa
就是我们常说的创建一个对象时,用来指向所属类的指针
。因此根据isa
就可以获取对应的类。
- 注:C++中结构的作用被拓宽了,也表示定义一个类的类型,struct和class的区别就在默认类型上一个是public,一个是private,这里就直接描述为结构体了
Class
上文中,isa
为Class
类型,而Class
则是objc_class
指针类型的别名:
1 | typedef struct objc_class *Class; |
而objc_class
具体的定义如下:
1 | struct objc_class : objc_object { |
在上文中已经介绍过objc_object
结构体,objc_class
继承自结构体objc_object
。可以看出objc_object
的isa
为private
类型成员变量,objc_class
继承后无法访问,所以objc_object
提供了以下两个成员函数:
1 | Class ISA(); |
所以,对objc_class
重要的成员变量进行下解释:
isa
为指向对象对应类的指针(这里注意一点,由于类也是一个对象(单例),所以这个单例中也有一个isa
指针指向类对象所属的类->metaClass
,即元类)superclass
为指向父类的指针cache
用于对调用方法的缓存,类似CPU先访问L1、L2、L3缓存的目的相似,它也是推断最近调用的方法极有可能被二次调用
,并将其存入cache
,在二次调用时先在cache
查找方法,而不是直接在类的方法列表中查找properties
为属性列表protocols
为协议列表method_lists
/method_list
为方法列表ivars
为成员变量列表class_ro_t
结构体中存储的都是类基本的东西,比如获取'load'
方法时,是从baseMethods
获取相应的IMP函数实现的:
1 | IMP objc_class::getLoadMethod() |
其中先了解下ivar_list_t
、method_list_t
、cache_t
的结构定义:
ivar_list_t
的结构为:
ivar_t
就是对应的成员变量
1 | struct ivar_list_t { |
method_list_t
为:
- 其中
method_iterator
为结构体自己构造的一个迭代器,用来访问方法,可以看到,构造的迭代器结构体中包含了method
成员变量
1 | struct method_list_t { |
cache_t
为:
- 可以看出
bucket_t
包含了一个IMP
类型的私有成员,供查找后调用实现 _occupied
和_mask
分别表示实际占用
的缓存_buckets总数和分配
的缓存_buckets总数
1 | struct cache_t { |
上文还涉及到了一个概念metaClass
元类,元类为类对象所属的类,以实例解释:
当我们调用类方法时,消息的接收者即为类,如文中一开始的代码:
1 | Dog *dog = [[Dog alloc] init]; |
这里的alloc
消息即发送给了Dog
类,编译转换后的代码为:
1 | Dog *dog = ((Dog *(*)(id, SEL))(void *)objc_msgSend)((id)((Dog *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Dog"), sel_registerName("alloc")), sel_registerName("init")); |
我们只需要关注这一行:
- 这里获取到的是类对象,只要再获取一次就得到了元类
1 | // objc_getClass表示根据对象名获取对应的类 |
关于元类,苹果提供了这么一张表:
图中的实线是superclass
指针,虚线是isa
指针。可以看到,根元类的超类NSObject
(Root class)并没有对应的超类,并且,它的isa
指针指向了自己。
总结一下:
- 每个实例对象的
isa
都指向了所属的类
- 每个类对象的
isa
都指向了所属的类,即元类
,其superclass
指针指向继承的父类
- 每个元类的
isa
都指向了超类
,即NSObject
Ivar
Ivar
,我把它理解成instance variable
,也就是实例变量,可以观察它的定义:
1 | typedef struct ivar_t *Ivar; |
Ivar
其实是指向ivar_t
结构体的指针,它包含了实例变量名(name)、类型(type)、相对对象地址偏移(offset)以及内存数据对齐等信息。
跟多关于实例变量的剖析可以查看Objective-C类成员变量深度剖析
Method
从以下定义的结构体可以看出,Method
主要住用为关联了方法名SEL
和方法的实现IMP
,当遍通过Method
自己的定义的迭代器查找方法名SEL
时,就可以找到对应的方法实现IMP
,从而调用方法的实现执行相关的操作。types
表示方法实现的参数以及返回值类型。
1 | typedef struct method_t *Method; |
SEL
SEL
为方法选择器,观察下它的定义:
1 | typedef struct objc_selector *SEL; |
可以看出SEL
实际是objc_selector
指针类型的别名,它用于表示运行时方法的名字,以便进行方法实现的查找。因为要对应方法实现,所以每一个方法对应的SEL
都是唯一的。因此它不具备C++可以进行函数重载的特性,当两个方法名一样时,会发生编译错误,即使参数不一样。
IMP
IMP
的定义如下:
1 |
|
可以看出IMP
其实就是一个函数指针的别名,也可以把它理解为函数名。它有两个必须的参数:
id
,为self
指针,表示消息接收者SEL
,方法选择器,表示一个方法的selector
指针- 后面的为传送消息的一些参数
在某些情况下,通过获取IMP
而直接调用方法实现,可以直接跳过消息传递机制,像C语言调用函数那样,在一定程度上,可以提供程序的性能。
消息传递
了解完runtime中一些必要的元素,继续回到文章开头的代码:
1 | @implementation Dog : NSObject |
编译器将其转换成了:
- 为了看起来简洁点,我把一些强制转换变为别名
1 | typedef (Dog *(*)(id, SEL))(void *) MyImp; |
从上面的代码可以看出,第二个objc_msgSend
返回值是作为第一个objc_msgSend
的首个参数的。
上文已经说过,[receiver message]
会被转化成以下形式
1 | objc_msgSend(receiver, selector, ...) |
接下来看看它主要做了哪几件事情:
- 根据
receiver
的isa
指针,获取到所属类,先在类的cache
即缓存中查找selector
,如果没有找到,再在类的method_lists
即方法列表中查找 - 如果没有找到
selector
,则会沿着下图类的联系路径一直查找,直到NSObject
类 - 如果找到了
selector
,则获取实现方法并调用,并传入接收者对象以及方法的所有参数;没有找到时走方法解析和消息转发流程。 - 将实现的返回值作为它自己的返回值
除此之外,objc_msgSend
还会传递两个隐藏参数:
- 消息接收对象(
self
引用的对象) - 方法选择器(
_cmd
,调用的方法)
objc_msgSend
找到方法实现后,会在调用该实现时,传入这两个隐藏参数,这样就能够在方法实现里面里面获取消息接受对象,即方法调用者了。隐藏参数
表示这两个参数在源代码方法的定义中并没有声明这两个参数,这两个参数是在代码编译期间
,被插入
到实现中的。
self和super的联系
2019.5.28 勘误,下面都是错的,self 从本类查找方法,super 从父类查找方法,最终因为 class 只有根类 NSObject 实现,所以都调用的 object_getClass(self),最后的值也一样
根据上文对objc_msgSend
的了解,可以解决以下代码输出一致问题
1 | @implementation Dog : NSObject |
输出为:
1 | [5491:173185] Dog |
这是为什么呢?先来看看编译后的-run
方法的情况:
1 | static void _I_Dog_run(Dog * self, SEL _cmd) { |
这里面只要关注两句:
1 | // [self class] |
首先我们需要了解self
和super
的差异:
super
:编译标识符
,告诉编译器,调用方法时,去调用父类的方法,而不是本类的方法self
:隐藏参数
,每个方法的实现第一个参数就是self
这里可以看出,编译后,经过super
标识符修饰的方法调用,会调用objc_msgSendSuper
函数来进行消息的发送,而不是objc_msgSend
。先来了解下objc_msgSendSuper
的声明:
1 | id objc_msgSendSuper ( struct objc_super *super, SEL op, ... ); |
其中objc_super
的定义为:
1 | // receiver 消息实际接收者 |
结合以上信息,我们可以知道:
1 | (__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass("Dog")) } |
就是对结构体objc_super
的赋值,也就是说objc_super->receiver=self
。到这里可能就有点明了了,super
只是告诉编译器,去查找父类中的class
方法,当找到之后,使用objc_super->receiver
即self
进行调用。用流程表示就是:[super class]->objc_msgSendSuper(objc_super{self, superclass)}, sel_registerName("class"))->objc_msgSend(objc_super->self, sel_registerName("class"))=[self class]
。
可以看出两者输出结果一致的关键就是,[self class]
的消息接收者和[super class]
的消息接收者一样,都是调用方法的实例对象。
方法解析和消息转发
当上文objc_msgSend
处理流程中,selector
没有找到时,会触发三个阶段,在这三个阶段都可以进行相关处理使程序不抛出异常:
- Method Resolution (动态方法解析)
- Fast Forwarding (备用接收者)
- Normal Forwarding (完整转发)
由于实际代码中很少有看到这种操作,所以这里不做详细解释,参考这个资料即可Objective-C Runtime 运行时之三:方法与消息
参考
Objective-C Runtime 运行时之一:类与对象
Objective-C Runtime
What is a meta-class in Objective-C?