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结构
![notion image](https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Ff283ca8c-3a00-40dd-87c9-43c248d44a31%2F7b1b3932-bc76-4e92-a081-2feb2bb6677f%2FUntitled.png?table=block&id=f3923271-e34f-457b-b1bf-ab843d4aff36&t=f3923271-e34f-457b-b1bf-ab843d4aff36&width=768&cache=v2)
以上是在运行时使用到类的最终结果,那么在编译期,其实在之前的文章中也有提到,类结构略有不同,主要在于
bits
里指向的是class_ro_t
,而非class_rw_t
。1.1.2 class_ro_t
结构
![notion image](https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Ff283ca8c-3a00-40dd-87c9-43c248d44a31%2F8a9e0570-3400-41db-b43a-bf0df4aa2ce6%2FUntitled.png?table=block&id=13c38a36-3cd5-4a9e-be83-5fb2bfbfa234&t=13c38a36-3cd5-4a9e-be83-5fb2bfbfa234&width=768&cache=v2)
上述的结果,呈现的是定义在类里面的方法,就是我们写在代码里硬编码方法,而不是通过以下方式添加的方法:
- 分类方法;
- 通过运行时添加的方法;
程序在运行时,会重新组织
bits
里结构的内容,获取bits.data()
,即class_rw_t
结构,该结构是运行时的结构。1.1.3 class_rw_t
class_rw_t
在程序开始运行后,会加载分类方法,会将分类方法重新组织成下面的结构:二维数组。![notion image](https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Ff283ca8c-3a00-40dd-87c9-43c248d44a31%2Fd20e39d3-7780-4f2c-a8c7-8a5672c93674%2FUntitled.png?table=block&id=e9c7150b-47e8-4109-8e29-59d381daba05&t=e9c7150b-47e8-4109-8e29-59d381daba05&width=768&cache=v2)
1.2 method_t
方法结构
根据上面的
class_rw_t
结构,我们可以清晰的观察到,在底层中方法的结构体是method_t
,即一个方法对应一个method_t
。下面是
method_t
结构体的组成。![notion image](https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Ff283ca8c-3a00-40dd-87c9-43c248d44a31%2F86be6c76-75e6-4ea1-8403-1d873b74e692%2FUntitled.png?table=block&id=9b8b511b-6b22-4fbb-bc4e-e9ae0a47a4ca&t=9b8b511b-6b22-4fbb-bc4e-e9ae0a47a4ca&width=576&cache=v2)
针对
method_t
的成员变量,上面阐述的很清楚。- 该结构包含了函数指针,指向具体实现。
- 通过
types
来声明该方法的返回值及参数,用于底层调用实现的校验。
SEL
是方法名。
那么我们要调用一个函数,还需要解决两个问题:
第一个问题:如何根据
SEL
找到函数实现地址IMP
。第二个问题:方法声明校验。
1.2.1 Type Encoding
先讨论第二个问题,方法声明校验,这个校验,是通过给方法指定一个编码实现的,相对应的编码如下:
![notion image](https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Ff283ca8c-3a00-40dd-87c9-43c248d44a31%2Fe4db8378-33bc-4761-8c74-e638abbdfc55%2FUntitled.png?table=block&id=1adb8cd6-edad-4acd-aa08-7b20047502e1&t=1adb8cd6-edad-4acd-aa08-7b20047502e1&width=768&cache=v2)
二、方法缓存
我们继续讨论上面的第一个问题——如何根据
SEL
找到函数实现地址IMP
。在无缓存时,找到
isa
指向的类结构,遍历class_rw_t
中的方法列表method_array_t
即可。那么有缓存的时候呢?
2.1 窥探方法缓存
我们要窥探缓存的结构,从源码读起。下面是源码的的顺序图:
![notion image](https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Ff283ca8c-3a00-40dd-87c9-43c248d44a31%2F6ec3dc99-801d-4a4d-bea1-2ed41e27844e%2FUntitled.png?table=block&id=74337524-ce32-4d02-bf7d-cfee7cc330f6&t=74337524-ce32-4d02-bf7d-cfee7cc330f6&width=816&cache=v2)
在对源码的剖析之后,我们有以下的成果:
2.1.1 方法缓存结构
其中
bucket_t *_buckets
就是存放缓存列表的结构,它本质是一个哈希表。而哈希表中的存放的是
bucket_t
的结构体。![notion image](https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Ff283ca8c-3a00-40dd-87c9-43c248d44a31%2Ff6c13794-a9c2-4a48-9af7-18b70b8675ff%2FUntitled.png?table=block&id=d9830594-49d1-4da2-82a9-57e67084d86f&t=d9830594-49d1-4da2-82a9-57e67084d86f&width=624&cache=v2)
2.1.2 验证
验证的代码在01方法缓存探索。
![notion image](https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Ff283ca8c-3a00-40dd-87c9-43c248d44a31%2Fea44d56a-cf11-4a1a-83c0-e635751a9d17%2FUntitled.png?table=block&id=d332d262-f45a-4273-aa60-6f9c9b70e507&t=d332d262-f45a-4273-aa60-6f9c9b70e507&width=768&cache=v2)
2.2 哈希表
通过代码的探索,我们整理了下面这些哈希表中最重要的节点处理。
![notion image](https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Ff283ca8c-3a00-40dd-87c9-43c248d44a31%2Ff8d13f15-3b09-4d6a-b963-3d3937aa57b0%2FUntitled.png?table=block&id=6830c621-1357-4f5d-a9d1-8bd3b9312ce4&t=6830c621-1357-4f5d-a9d1-8bd3b9312ce4&width=816&cache=v2)
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 方法调用
经过上面的探讨,我们大致明白了如何调用一个方法。
![notion image](https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Ff283ca8c-3a00-40dd-87c9-43c248d44a31%2F5c8e1c77-dfd3-4b6e-9322-138fa5e2da23%2FUntitled.png?table=block&id=33c8fe05-e112-4f00-b17a-4f5211fbbcac&t=33c8fe05-e112-4f00-b17a-4f5211fbbcac&width=432&cache=v2)
当然,这并不是一个消息发送的完整流程,下篇文章,将会开启探索如何调用一个方法的完整流程之旅。