一、介绍runtime中的数据结构
首先在runtime中,有几个常用的数据类型结构体,objc_objcet、objc_class、isa指针类型、method_t、cached_t、class_data_bits_t、class_rw_t、class_ro_t。
1.objc_object就是常见的在runtime中代表一个对象类型的结构体,其包含的内容有:1.isa_t数据结构类型的指针,2.关于isa指针操作相关的方法,3.关于弱引用指针操作相关的方法,4.关联对象相关的方法,5.内存管理相关的方法
2.objc_class,这是一个继承objc_object的结构体,其中包含的主要内容有:superClass指针,cache_t结构体,class_data_bits_t的结构体三个,主要的作用就是记录类中的父类指针,缓存方法与类内的数据结构,后面再详细介绍其中包含的这三种数据结构。
3.isa指针,isa指针分为两种,一种是指针型isa,一种是非指针型isa,指针型isa表明isa的值就代表指针的地址,而非指针型表示isa的部分地址表示对象的地址。这就体现了一个内存节约的思想,因为在一般的64位操作系统中,isa地址用不到那么多位来存储,其他的位可以存储一些别的数据,所以就产生了非指针型的isa指针。关于isa指针的指向分为两种,第一种是对于一个对象objcet,它的isa指针是指向他的类Class的,对于一个类,也可以称为一个类对象,他的isa指针是指向他的元类的meta class。
4.method_t数据结构,method_t数据结构就是用来表明一个完整方法的结构体,方法的四要素为:名称,返回值,参数以及其实现,也叫函数体。在OC中提现为1.名称:SEL name,2.返回值与参数:在苹果中用一个const*char types表示,其使用了type Encodings技术,这个我们稍后详解。
5.cache_t数据结构:cache_t是用来存放每一个类中的方法缓存的数据结构,其本质是一个可增量扩展的哈希表数据结构,使用cache_t可以快速查找方法的执行函数,也是局部性原理的最佳应用。试想一下,如果一个类中有众多的类方法与对象方法,有些方法可能在程序执行的生命周期中只会调用一次,而在其他方法每一次调用时,系统都会遍历类中所有的方法后找到其方法的相应IMP进行执行,是一个非常浪费性能的工作,所以以此原因引入了cache_t,缓存方法列表,系统将一些常用方法储存在类的缓存列表cache_t中,在每次方法调用时,先从缓存列表中查找,如果找到了对应方法,就直接调用其函数体,这是一种非常节约计算成本的方式。
关于cache_t中的的数据结构,使用的一张哈希表存储的众多bucket_t数据结构,在bucket_t中包含的是方法的实现IMP与其对应的key,在一次消息传递的过程中,系统首先会在对应的类中进行缓存查找,利用发送消息的SEL查找cache_t哈希表,其过程是通过计算将SEL转换成一个cache_key_t对象,利用该"key"进行cache_t哈希表定位,随后取出bucket_t,直接拿到IMP指针,进行消息的发送。
6.class_data_bits_t数据结构。class_data_bits_t主要的作用是包含类中的方法、成员变量、协议等诸多主要信息,随后再对一些零散信息的封装。其实class_data_bits_t的主要作用也是对class_rw_t的一个封装,重要的信息其实都在class_rw_t中
7.class_rw_t数据结构。class_rw_t代表了类相关的读写信息,以及对class_ro_t的封装。顾名思义,rw就是读写,ro就是只读。class_rw_t内的数据结构有,class_ro_t,protocols,properties,methods,第一个class_ro_t稍后详细说明。后面三个其实就是对协议、属性、方法的封装,这三个数据结构都是以二维数组的形式提现的,但是为什么是二维数组呢?我之前写过一篇关于分类理解的文章,里面说到分类方法添加的问题,就是分类在运行时决议的过程中,会把分类的方法都以数组的形式都添加到类方法中,其实过程就在这里,分类中众多method_t以数组[method1,method2,method3]的形式提现,但一个类可能有众多分类,那么分类1,分类2,分类3在objc_class的class_data_bits_t的class_rw_t中的体现就是[[method1,method2,method3],[method1,method2,method3],[method1,method2,method3]],其都是method_t的数据形式。协议,属性的体现方式都是相似的,就同理后推就好了。所以到这里肯定会有一个疑问,为什么class_rw_t中没有成员变量只有属性。因为class_rw_t中包含的是一个读写数据的列表,换言之其实就是分类的列表,class_rw_t只管分类中的数据,之前分类那篇文章说到过,为分类添加属性是不会生成相应的成员变量的,如果要生成对应的成员变量必须用关联对象技术把使其达到一个类似于可读写效果,而其关联对象都储存在一个全局容器中,这也呼应了开头介绍objc_object数据结构中存储的相关关联对象的方法。好了,越绕越远了,现在继续介绍class_ro_t数据结构
8.class_ro_t数据结构。这个数据结构包含的信息就是类本身的信息,包括name(类名),ivars(instence variables成员变量列表),properties(属性列表),protocols(协议列表),methodList(方法列表)。所以这下好理解了吧,class_ro_t中装的是类本身编译的信息,class_rw_t中装的是类中分类的信息,而class_data_bits_t封装了class_rw_t封装了class_ro_t。不过需要注意区别的是class_ro_t中包含的ivas,properties,protocols,methodList都是一位数组的形式存在(因为没有分类了嘛)。所以这里还需要注意一个点就是:我们没法向一个编译后的类动态添加信息,比如方法,成员变量,属性等,第一是因为这些都存在于class_ro_t中,其名称含义就告诉你readOnly,人好好的存在那,说了不让你改,你硬要改,那肯定不行啊,第二是因为所谓向一个类中动态添加信息,都是指的用runtime动态添加的类,通俗点讲就是用代码写的类,这个东西要区分一下。
二、详解消息传递的机制
1.以上就大致对runtime中的常用数据结构作了一个比较详细的介绍了,接下来就进行ios中消息传递与消息转发以及相关的诸多问题解读。
首先,我们区分一个概念,类对象与元类对象->class and meta Class。
平时有一些朋友刚接触到这一块就问我class和meta class有啥区别啊,都是干什么的,我就简而言之一句话,OC中万物皆对象,类对象是存储对象方法的地方,元类对象就是存储类方法的地方。
现在我们拿到了三个对象,分别是:对象,类对象、元类对象,所以就来简单梳理一下这三者之间的关系:
对象,在runtime中的体现为objc_object,其他两者在runtime中都是objc_class,前面说过objc_class继承自objc_objcet,所以,这三者都有一个显著的特点就是,都存在一个isa指针。对象的isa指针指向其父类class,其父类的isa指针指向其元类meta class,元类meta class的isa指针都指向其根元类,root meta class,就连root meta class自己的isa指针也是指向自己的。而在类对象与元类对象中,都有一个superClass指针,在class中,superClass指针指向类对象的父类,而在最高一级父类也就是NSObjcet中,superClass指针指向nil,关系也就是
class->superClass->bigDadClass(NSObjcet)->nil。
但在元类中的superClass指针就不一样了,其根元类的superClass指针是指向根类的,梳理一下逻辑
metaClass->superMetaClass->rootMetaClass->NSObject->nil
对于isa指针的关系而言,也梳理一下结构吧,为:
objcet->class->metaClass->rootMetaClass->指向自己rootMetaClass(其中需要注意,每一级metaClass的isa指针都指向的是rootmetaClass,就连他自己也是)
以上就是对象,类对象,元类对象的相互关系。
2.接下来继续探究消息传递的机制
消息传递机制在runtime中的提现是一个方法objc_msgSend(object,SEL,types),当我们在OC中用[]调起一个方法的同时,runtime内部就会去调取这个方法进行消息方法,还有一个方法是objc_msgSendSuper,参数还是一样,这个方法是我们用关键字super去调起方法时runtime内部所执行的方法,其内部包含一个变量receiver,这个变量指向的就是super的子类self自己。之前看过一个案例,是在init内部调用[self class]和[super class],然后同时打印这两个类名。其实结果不出意外是一样的,但分析原因的话就是因为[super class]调起时,调用的是objc_msgSendSuper这个方法,其中包含一个指针receiver,指向的就是super对象调用者self自己,也就是消息的接收者还是self,而class方法都是存在于较高父类(系统类中的),所以在方法遍历的时候,[super class]和[self class]的区别就在于,super是从父类开始向上遍历直至找到class方法,而self是从本类开始向上遍历最后找到对应方法,而调用class最后的结果取决于消息的接收者,因为两个方法的消息接收者都是self,所以不出意外,最后打印出的结果都是self类本身。
而实际消息当一个方法调用的消息发出后,消息传递的机制为:
首先判断方法为类方法还是实例方法 ->object找到父类class(若为类方法则再向上取到metaClass)->哈希查找objc_class的缓存列表cache_t试试能否用对用SEL直接找到函数体IMP->若找到,则直接进行函数体调用后结束消息放松,若找不到则遍历对应的类或者元类的方法列表,如果方法列表是排序好的则使用二分法遍历,如果没有排序好则使用普通遍历->如果找到了则进行函数体调用,结束消息传递流程,若找不到则继续根据superClass指针找到父类,再重复之前的工作(遍历缓存后遍历本类中方法列表),在途中若找到了相应函数体实现IMP则进行调用,结束消息传递流程,如果至到NSObject中还没有找到则进行消息转发的流程。
以上就是整个ios中消息传递的流程了。
关于消息转发的流程,其实是系统给的一个消息再利用的过程,当消息传递流程中没有方法来响应此消息时,开发者可通过重写以下这四个方法来实现消息转发的过程,以及去让计算机做相应的工作。
1.+ (BOOL)resolveInstanceMethod:(SEL)sel
若返回YES,则表明消息已处理,结束流程,若返回NO则进行下一个方法的调用
2.- (id)forwardingTargetForSelector:(SEL)aSelector
此方法可以返回转发消息的目标,若返回为nil则调用下一个方法
3.- (NSMethodSignature *)methodSignatureForSelector
此方法可以返回方法的签名,若返回nil则直接抛出异常,表明方法函数体指针无法被找到,若返回方法签名,则调用下一个方法
4.- (void)forwardInvocation:(NSInvocation *)anInvocation
此方法内部会决定是否已处理方法,如果到这个方法也没法处理,则抛出异常,报错
以上四个方法是系统留给开发者处理消息转发的入口,开发者可以通过重写以上四个方法来手动处理消息转发的流程。
以上就是自己对runtime内部结构以及消息传递流程的一些见解,不足之处请多多指教!
本文属作者原创,未经允许不得转载。