1、method 与 Class
先看下 Class 的结构:class_rw_t
里面的 methods
、properties
、protocols
是二维数组,是可读可写的,包含了类的初始内容、分类的内容:
也就是说,这里的 methods
既包含分类中的方法,也包含了类的初始方法。实际上,我们在使用 category 为类增加方法时,会将 category 中的方法插到初始的方法前面。
class_ro_t
里面的 baseMethodList
、baseProtocols
、ivars
、baseProperties
是一维数组,是只读的,包含了类的初始内容:
也就是说,class 中 method
是 method_t
结构体的形式进行存储的,method_t
结构体就是对方法/函数的一个封装。
2、method_t
method_t
是对方法/函数的封装:
1 | struct method_t { |
(1) IMP
IMP 是指向函数的指针,即 imp 存储了函数的地址,是函数的具体实现:
1 | typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); |
- 前面的
id
代表返回值。 - 第一个参数指向
self
(它代表当前类实例的地址,如果是类则指向的是它的元类),作为消息的接受者; - 第二个参数代表方法的
SEL
; - … 代表可选参数;
(2) SEL
SEL 一般称做选择器或选择子,代表方法\函数名,底层结构跟 char *
类似,是方法在 Runtime 期间的标识符。
1 | typedef struct objc_selector *SEL |
- 可以通过
@selector()
和sel_registerName()
获得 - 可以通过
sel_getName()
和NSStringFromSelector()
转成字符串 - 在类加载的时候,编译器会生成与方法相对应的 SEL,并注册到 Objective-C 的 Runtime 运行系统。不论两个类是否存在依存关系,只要他们拥有相同的方法名,那么他们的 SEL 都是相同的。
(3) types
types 是个 char
指针,其实存储着方法的参数类型、返回值类型,即是 Type Encoding 编码。
iOS 中提供了一个叫做 @encode
的指令,可以将具体的类型表示成字符串编码,例如:
1 | NSLog(@"%s", @encode(int)); // 打印 i |
types 对照表:
例如,对于以下方法:
1 | - (int)test:(int)age height:(float)height; |
其 types 为:i24@0:8i16f20
(开发时也可以简写为 i@:if
)
i
代表返回值类型为int
@
代表id
类型参数(self
):
代表SEL
类型参数(_cmd
)i
代表int
类型参数(age
)f
代表float
类型参数(height
)
对于数字:
24
代表所有参数占用的字节数- (
id
和SEL
为指针类型,各占 8 字节;int
、float
均占4
字节。总计 8+8+4+4=24 字节) 0
代表@
(self
)从第 0 个字节开始8
代表:
(_cmd
)从第 8 个字节开始16
代表i
(age
)从第 16 字节开始20
代表f
(height
)从第 20 字节开始
2、方法缓存
(1) cache_t 的结构
再来来看下 class 结构:
每一个类都维护了一个 cache
,当我们在调用一个方法时,runtime 会先在 cache
中查找对应方法,没有找到对应方法,再从 methods
中寻找。并且每次调用方法的时候,都会将方法存到 cache
中。
接下来看下 cache_t
的数据结构:
Class 内部结构中有个方法缓存(cache_t
),用散列表(哈希表)来缓存曾经调用过的方法,可以提高方法的查找速度。
cache_t
结构体里面有三个元素:
_buckets
散列表,是一个数组,数组里面的每一个元素就是一个bucket_t
,bucket_t
结构:SEL
作为key
- 函数的实现
IMP
为value
_mask
散列表的长度_mask
它代表的是散列表的长度 -1,最初分配的值是 4,方法SEL & _mask
得到的值,就是对应bucket_t
在散列表中的索引。
_occupied
已经缓存的方法数量
(2) cache_t 的存入
1 | void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver) |
总结:
先看缓存中是否已经存在了该方法,如果已经存在,直接 return 掉,不再缓存。
如果当前
cache
还没被初始化,则分配一个大小为 4 的数组,并设置_mask
为 3。如果存入缓存后的大小小于当前大小的 3/4,则当前缓存大小还可以使用,无需扩容。
否则缓存太满,需要扩容,扩容为原来大小的 2 倍。放弃旧的缓存,已缓存的数据全部清空,新扩容的缓存为空,并将
_occupied
重新初始化为 0。方法的存储并不是按照数组一样从上到下存储,而是通过
SEL & _mask
(例如:@selector(myMethod:) & _mask)
的值作为索引来存储的,所以难免会存在内存利用率低,但是加快了方法查找的速度,即:空间换时间。在 arm64 架构下,如果
SEL & _mask
结果为i
,若索引i
位置已经有bucket_t
值了,就取出bucket_t
中的key
与SEL
进行比较,如果不相同,则继续往i -1
继续寻找,直到找到未被占用的位置,然后将对应bucket
存进去。如果找到i = 0
仍然未找到合适位置,则从i = _mask
(即最后一个索引)开始继续往上寻找。方法缓存是先于
isa
的方法查找,就是说,缓存中找不到,再到自己的方法列表中查找,找到之后也会缓存到cache_t
中,如果是父类的方法,也是会缓存到自己的表当中的。
- 本文章采用 知识共享署名 4.0 国际许可协议 进行许可,完整转载、部分转载、图片转载时均请注明原文链接。