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?