runtime(三)方法缓存
2020-8-6
| 2024-5-16
0  |  阅读时长 0 分钟
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
以上是在运行时使用到类的最终结果,那么在编译期,其实在之前的文章中也有提到,类结构略有不同,主要在于bits里指向的是class_ro_t,而非class_rw_t

1.1.2 class_ro_t结构

notion image
上述的结果,呈现的是定义在类里面的方法,就是我们写在代码里硬编码方法,而不是通过以下方式添加的方法:
  • 分类方法;
  • 通过运行时添加的方法;
程序在运行时,会重新组织bits里结构的内容,获取bits.data(),即class_rw_t结构,该结构是运行时的结构。

1.1.3 class_rw_t

class_rw_t在程序开始运行后,会加载分类方法,会将分类方法重新组织成下面的结构:二维数组
notion image

1.2 method_t 方法结构

根据上面的class_rw_t结构,我们可以清晰的观察到,在底层中方法的结构体是method_t,即一个方法对应一个method_t
下面是method_t结构体的组成。
notion image
针对method_t的成员变量,上面阐述的很清楚。
  • 该结构包含了函数指针,指向具体实现。
  • 通过types来声明该方法的返回值及参数,用于底层调用实现的校验。
  • SEL是方法名。
那么我们要调用一个函数,还需要解决两个问题:
第一个问题:如何根据SEL找到函数实现地址IMP
第二个问题:方法声明校验。

1.2.1 Type Encoding

先讨论第二个问题,方法声明校验,这个校验,是通过给方法指定一个编码实现的,相对应的编码如下:
notion image

二、方法缓存

我们继续讨论上面的第一个问题——如何根据SEL找到函数实现地址IMP
在无缓存时,找到isa指向的类结构,遍历class_rw_t中的方法列表method_array_t即可。
那么有缓存的时候呢?

2.1 窥探方法缓存

我们要窥探缓存的结构,从源码读起。下面是源码的的顺序图:
notion image
在对源码的剖析之后,我们有以下的成果:

2.1.1 方法缓存结构

其中bucket_t *_buckets就是存放缓存列表的结构,它本质是一个哈希表。
而哈希表中的存放的是bucket_t的结构体。
notion image

2.1.2 验证

验证的代码在01方法缓存探索
notion image

2.2 哈希表

通过代码的探索,我们整理了下面这些哈希表中最重要的节点处理
notion image

2.2.1 哈希表的处理

上面有一些需要注意的点:

(1)hash函数

mask为缓存空间大小-1,所以hash之后一定不会超过缓存空间大小;

(2)碰撞处理

碰撞的处理因平台而已,在iOS下做了如下处理,其实就是简单的开放寻址来进行碰撞处理:

(3)缓存扩容

缓存空间是会动态变化的,其变化如下:

(4)注意点

  • mask为缓存空间大小-1,所以hash之后一定不会超过缓存空间大小;

2.2.2 读写缓存

(1) 缓存方法

我们看看方法缓存的哈希表,是如何存放方法的。
  1. 传入key(@selector(method) )通过hash——cache_hash获得索引index
  1. 检查当前index是否被占用
      • 如果被占用,即本次哈希冲突,重新进行寻址——cache_next,算出index,回到2。
      • 如果没占用,存放到index处。

(2)查找缓存

那么又是如何查找缓存的呢?
  1. 传入key(@selector(method) )通过hash——cache_hash获得索引index
  1. 根据index获取bucket_t,检查该bucket._key是否与传入的key一致。
      • 若不一致,即由于存方法时,有hash冲突,再次hash——cache_next,算出index,回到2。
  1. 如果key一致性通过,即获取该bucket._imp返回;
  1. 调用bucket._imp处的函数。

三、总结

3.1 方法缓存

  1. 每个类对象都存有一个cache——方法缓存列表;
  1. cache本质是哈希表,其hash函数为:f(@selector()) = @selector() & _mask;
  1. 子类没有实现方法会调用父类的方法,并且将父类方法加入到子类自己的cache 里。

3.2 方法调用

经过上面的探讨,我们大致明白了如何调用一个方法。
notion image
当然,这并不是一个消息发送的完整流程,下篇文章,将会开启探索如何调用一个方法的完整流程之旅。

参考

链接

  1. Type Encodings
  1. Apple souce objc4

示例代码

  1. 01方法缓存探索
  • Objective-C
  • runtime
  • RunLoop(三)运行RunLoop(二)对象
    Loading...
    目录