type
status
date
slug
summary
tags
category
icon
password
在了解类的基本结构之后,本文开始探讨iOS 中的消息发送,即消息调用。
首先开始讨论的是——在真正消息调用之前,我们会去方法缓存里面寻找真实的函数地址,iOS提供的缓存机制用于提高效率。
For speed, objc_msgSend does not acquire any locks when it reads method caches. Instead, all cache changes are performed so that any objc_msgSend running concurrently with the cache mutator will not crash or hang or get an incorrect result from the cache.
一、方法的相关结构
1.1 回顾Class结构
不管讨论,什么离不开最基本的类结构,现在我们又回到
Class
结构,不过这次需要关注的是方法。1.1.1 Class结构
以上是在运行时使用到类的最终结果,那么在编译期,其实在之前的文章中也有提到,类结构略有不同,主要在于
bits
里指向的是class_ro_t
,而非class_rw_t
。1.1.2 class_ro_t
结构
上述的结果,呈现的是定义在类里面的方法,就是我们写在代码里硬编码方法,而不是通过以下方式添加的方法:
- 分类方法;
- 通过运行时添加的方法;
程序在运行时,会重新组织
bits
里结构的内容,获取bits.data()
,即class_rw_t
结构,该结构是运行时的结构。1.1.3 class_rw_t
class_rw_t
在程序开始运行后,会加载分类方法,会将分类方法重新组织成下面的结构:二维数组。1.2 method_t
方法结构
根据上面的
class_rw_t
结构,我们可以清晰的观察到,在底层中方法的结构体是method_t
,即一个方法对应一个method_t
。下面是
method_t
结构体的组成。针对
method_t
的成员变量,上面阐述的很清楚。- 该结构包含了函数指针,指向具体实现。
- 通过
types
来声明该方法的返回值及参数,用于底层调用实现的校验。
SEL
是方法名。
那么我们要调用一个函数,还需要解决两个问题:
第一个问题:如何根据
SEL
找到函数实现地址IMP
。第二个问题:方法声明校验。
1.2.1 Type Encoding
先讨论第二个问题,方法声明校验,这个校验,是通过给方法指定一个编码实现的,相对应的编码如下:
二、方法缓存
我们继续讨论上面的第一个问题——如何根据
SEL
找到函数实现地址IMP
。在无缓存时,找到
isa
指向的类结构,遍历class_rw_t
中的方法列表method_array_t
即可。那么有缓存的时候呢?
2.1 窥探方法缓存
我们要窥探缓存的结构,从源码读起。下面是源码的的顺序图:
在对源码的剖析之后,我们有以下的成果:
2.1.1 方法缓存结构
其中
bucket_t *_buckets
就是存放缓存列表的结构,它本质是一个哈希表。而哈希表中的存放的是
bucket_t
的结构体。2.1.2 验证
验证的代码在01方法缓存探索。
2.2 哈希表
通过代码的探索,我们整理了下面这些哈希表中最重要的节点处理。
2.2.1 哈希表的处理
上面有一些需要注意的点:
(1)hash函数
mask
为缓存空间大小-1,所以hash之后一定不会超过缓存空间大小;(2)碰撞处理
碰撞的处理因平台而已,在iOS下做了如下处理,其实就是简单的开放寻址来进行碰撞处理:
(3)缓存扩容
缓存空间是会动态变化的,其变化如下:
(4)注意点
mask
为缓存空间大小-1,所以hash之后一定不会超过缓存空间大小;
2.2.2 读写缓存
(1) 缓存方法
我们看看方法缓存的哈希表,是如何存放方法的。
- 传入key(@selector(method) )通过hash——cache_hash获得索引
index
;
- 检查当前index是否被占用
- 如果被占用,即本次哈希冲突,重新进行寻址——cache_next,算出index,回到2。
- 如果没占用,存放到
index
处。
(2)查找缓存
那么又是如何查找缓存的呢?
- 传入key(@selector(method) )通过hash——cache_hash获得索引
index
;
- 根据
index
获取bucket_t
,检查该bucket._key
是否与传入的key一致。 - 若不一致,即由于存方法时,有hash冲突,再次hash——
cache_next
,算出index,回到2。
- 如果key一致性通过,即获取该
bucket._imp
返回;
- 调用
bucket._imp
处的函数。
三、总结
3.1 方法缓存
- 每个类对象都存有一个cache——方法缓存列表;
- cache本质是哈希表,其hash函数为:f(@selector()) = @selector() & _mask;
- 子类没有实现方法会调用父类的方法,并且将父类方法加入到子类自己的cache 里。
3.2 方法调用
经过上面的探讨,我们大致明白了如何调用一个方法。
当然,这并不是一个消息发送的完整流程,下篇文章,将会开启探索如何调用一个方法的完整流程之旅。