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 国际许可协议 进行许可,完整转载、部分转载、图片转载时均请注明原文链接。