本文主要简述类(元类)对象里面的方法缓存、消息发送(包括消息发送,动态方法解析与消息转发)与super关键字的底层原理
方法
Class对象的结构
1 | struct objc_class { |
其中bits
成员变量与FAST_DATA_MASK
进行一次与运算之后,会获得一个其属性可读可写的对象的地址,这个对象长这样
1 | struct class_rw_t { |
其中 class_ro_t
里面存放的是类的原始信息(不包括分类里面的东西),是仅可读的,结构如下
1 | struct class_ro_t { |
class_rw_t
class_rw_t
里面的methods、properties、protocols是二维数组,是可读可写的,比如方法列表随时可以新增method_list_t
类型的数据进去。class_rw_t
包含了类的初始内容和分类的内容,其中方法列表类似如下结构
1 | method_array_t: [ |
objc_class
内有一个data()
函数,其返回值一开始是指向class_ro_t
类型的对象的。在合并分类内的内容时,才会产生class_rw_t
类型的对象,并指向这个对象。可以参考runtime源码,objc-runtime-new.mm
中realizeClassWithoutSwift
函数的实现,这里贴出关键部分
1 | static Class realizeClassWithoutSwift(Class cls, Class previously) |
class_ro_t
class_ro_t
里面的baseMethodList、baseProtocols、ivars、baseProperties是一维数组,是只读的,所以不能新增内容,包含了类的初始内容,其中方法列表类似如下结构
1 | method_list_t : [ |
在runtime源码,
objc-runtime-new.mm
中的attachCategories
方法中我们可以看到分类合并到class_rw_t
对象的过程,比如方法的合并如下
1 | method_list_t *mlist = entry.cat->methodsForMeta(isMeta); |
method_t
method_t
是对方法/函数的封装(下面是缩减版定义)
1 | struct method_t { |
IMP
代表具体函数的实现typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
SEL
可以代表方法\函数名,一般叫做选择器,底层结构跟char *
类似typedef struct objc_selector *SEL
;- 可以通过
@selector()
和sel_registerName()
获得 - 可以通过
sel_getName()
和NSStringFromSelector()
转成字符串 - 不同类中相同名字的方法,所对应的方法选择器是相同的
- 可以通过
types
包含了函数返回值、参数编码的字符串,称作Type Encodings
(类型编码),具体可以参考苹果官方文档- 比如
-(void)test:(int)a;
的方法编码为v@:i
,v = void,@表示指针变量(因为编译后第一个参数是self),:表示选择器(因为编译后第二个参数是_cmd), - 有时类型编码会带上数字,比如
v16@0:8
,第一个数字表示这个函数的参数一共占用16个字节,第二个数字开始往后的数字都代表偏移值,0代表第一个参数的字节偏移值为0,第三个数字8代表偏移8个字节,也就是第一个参数已经占用了8个字节了,所以第二个参数就从第8个字节开始
- 比如
cache_t
- Class内部结构中有个方法缓存(cache_t),用散列表来缓存曾经调用过的方法,可以提高方法的查找速度
1 | struct cache_t { |
其中,通过buckets()
函数我们可以得知_bucketsAndMaybeMask
是一个存放bucket_t
数组的指针(即_bucketsAndMaybeMask
指针指向的是数组的第一个元素),是通过位运算取出来的
1 | struct bucket_t *cache_t::buckets() const |
而bucket_t
的结构如下
1 | struct bucket_t { |
通过IMP和SEL,我们就可以调用方法了,所以综上所述,一个实例对象调用方法,其类对象从方法缓存里面找方法的大概流程就是
1 | 获取sel : class - isa - 偏移16个字节 - cache_t - buckets() - [bucket_t] - 计算出下标index - bucket_t - sel() |
另外_bucketsAndMaybeMask
之所以叫这个名字是因为它不仅存放着buckets
还存放着maybeMask
,在arm64位真机环境下,取高16位,如下:
1 | mask_t cache_t::mask() const |
方法缓存的流程
先看看runtime源码中,将方法插入缓存的函数(摘抄核心流程)
1 | void cache_t::insert(SEL sel, IMP imp, id receiver) |
- 首次进入isConstantEmptyCache分支。会创建一个容量为4的空buckets。这个时候由于旧buckets不存在不需要释放所以参数传递false。
- 当容量大于等于3/4或7/8的情况下扩容。arm64的条件下为7 / 8。
- arm64条件下容量小于等于8的时候会占用100%才扩容。
- 扩容是直接翻倍,默认值4。最大值MAX_CACHE_SIZE为216(65536)。在扩容的时候直接释放了旧值。
- mask值为capacity - 1
- 通过cache_hash(下文会提及的散列表算法)计算插入的index,后面会通过cache_next再进行计算hash解决冲突问题。
- 循环判断通过b[i].set插入bucket数据。
- reallocate函数在开辟控件的同时,把缓存给直接清空了,清空之后再把现在要缓存的方法放进去,所以扩容后occupied会为1。
散列表(哈希表)缓存
方法缓存的容器,不是简单的数组,而是用散列表的方式进行存储。
假如现在散列表长度为10,那么mask(即cache_t里面的_maybeMask)就是10-1 = 9,当selector传进来的时候,会跟mask进行一次与运算,如下:
@selector(personTest) & mask = 4
假如得到的结果是4的话,那么就会插在列表的下标为4的位置,其他位置因为已经开辟好空间了,所以有值就放值,没值就NULL
这种列表的好处是,当想取到特定的bucket_t的时候,只需要把selector跟mask进行一次与运算,就可以直接得到下标,然后直接从列表取出,这样子就不用遍历查找了,大大节约了性能。
另外,由于与运算的特性,跟mask进行与运算之后的值,都不可能比mask更大,这样子就保证不会插入越界的位置。
如果通过计算后的下标值,插入列表的时候发现已经有东西了那怎么办?
这也就是所谓的Hash冲突。为了处理这种问题,系统会调用
cache_next
函数1
2
3static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;
}也就是说如果
@selector(personTest) & mask = 4
的4已经有东西了,那么就取 4 - 1 = 3,如果3还有东西,就放在2的位置,如果2还有,就放在1,以此类推,如果直到0都还没有可以插入的位置,那么就从mask的位置开始找,也就是9,然后再找9看看是否可以插入,插不进去再找8,以此类推,找到为止。由于列表在存放数量达到容量的87.5%的时候就会两倍的扩容(arm64),扩容后又会清空缓存,所以一定能找到合适的位置插入的。
消息发送机制
调用一个不存在的方法的时候,他会经历这么一个流程
消息发送 - (找不到方法的话) -> 动态方法解析 - (没有实现的话) -> 消息转发 -> (没有实现的话) -> 抛出异常
消息发送
假如我们这么调用一个方法
1 | [person personTest]; |
底层会转换为
1 | objc_msgSend(person,sel_registerName("personTest")); |
- 这里的person我们称作消息接受者(receiver),就是调用方法的对象,如果这里还是调用类方法的话,那么这里就会传入一个类对象
sel_registerName()
函数等价于@selector
- 为了性能,
objc_msgSend
方法底层是使用汇编和C++实现的
消息发送的流程如下:
- receiver 是否为空,如果是退出,否则继续
- 从receiver的类对象(如果传入的是类则找的是元类对象,下文统称为receiverClass)的cache中查找方法,如果找到则调用方法,如果找不到则继续
- 从receiverClass中的
class_rw_t
中查找方法,如果找到则调用方法,并将方法插入缓存,如果找不到则继续 - 从
superClass
的cache中查找方法,有则调用并缓存到当前receiverClass的cache中(不是superClass的cache),否则继续 - 从
superClass
的class_rw_t
中找,有则调用并缓存到receiverClass的cache中,否则继续 - 继续通过
superClass
的superClass
找方法,流程回到4,直到再也没有父类了,并且也找不到方法,那么将会进入动态方法解析阶段。
其中:
- 如果是从
class_rw_t
中查找方法,若方法列表已经排序好,那么就使用二分查找法查找 - 如果是还没排序的方法,那么就使用遍历的方法查找
- 在缓存中查找方法的过程也称作快速查找(使用汇编实现),在
class_rw_t
中查找方法的过程也称作慢速查找(使用汇编和C++实现),C++部分方法源码在lookUpImpOrForward
函数中,如下:
1 | IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior) |
慢速查找流程图:

消息发送流程图:

动态方法解析(也称:动态方法决议)
当消息发送流程找不到方法后就会进入动态方法解析流程。
动态方法解析是当消息发送阶段找不到方法的时候,开发者可以通过重写特定方法(如果是类方法就重写+ (BOOL)resolveClassMethod:(SEL)sel
,如果是对象方法就重写+ (BOOL)resolveInstanceMethod:(SEL)sel
),给原先不存在的方法添加方法实现。主要是用到runtime里面的class_addMethod
函数,并且动态解析后,会重新走”消息发送“的流程
使用
假如这里的person没有实现print方法
1 | Person *person = [Person new]; |
那我们可以在Person.m
添加一个方法实现用来替换丢失的print
,这里我们用truePrint
来替代
1 | - (void)truePrint { |
Method的结构
点击Method我们可以看到结构如下
1 | typedef struct objc_method *Method; |
而ojbc_method
的定义如下
1 | struct objc_method { |
可以看到跟上面所说的method_t
是类似的东西
以下是一些拿到Method
对象后可以使用的一些函数
1 | // 函数调用,但是不接收返回值类型为结构体 |
Runtime中的原理
在上面的消息发送的流程函数lookUpImpOrForward
中,有一段代码如下
1 | if (slowpath(behavior & LOOKUP_RESOLVER)) { |
当找不到消息的时候就会进入动态方法解析的流程即resolveMethod_locked
,如下
1 | static NEVER_INLINE IMP resolveMethod_locked(id inst, SEL sel, Class cls, int behavior) { |
这里面我们可以发现resolveInstanceMethod
和resolveClassMethod
两个函数,分别是实现实例方法的动态消息解析和类方法的动态消息解析,这两个长得差不多,所以我们拿实例对象的函数查看一下
1 | static void resolveInstanceMethod(id inst, SEL sel, Class cls) |
在这里我们可以看到对resolveInstanceMethod
方法进行了一次objc_msgSend
调用,调用之后又执行了lookUpImpOrNilTryCache
函数进行了缓存,如果缓存成功,那么下次调用这个方法就不会在进入动态方法解析阶段,直接通过消息发送阶段就调用成功了。
优化
根据我们以前所学的知识可以知道,方法查找的流程为
实例方法:类 – 父类 – 父类 – … – 根类 – nil
类方法:元类 – 父元类 – 父元类 – … – 根元类 – 根类 – nil
也就是说丢失的方法最后都会回到根类去找方法,一般情况下是NSObject
通过这个特性,我们可以直接在NSObject
上添加分类,然后实现动态解析方法,根据方法名前缀进行判断,然后统一处理未被找到的方法添加默认实现。通过这种方式我们可以减少一些找不到方法导致的崩溃问题,提升用户体验。
消息转发
如果没有实现动态方法解析,那么将进入消息转发阶段。消息转发即把消息交给别人发送的意思。
从上面的lookUpImpOrForward
函数的源码我们可以看到,最后是调用了_objc_msgForward_impcache
这个函数,这个函数是通过汇编实现的
要实现消息转发,我们有两种方式,一种是实现一个方法-forwardingTargetForSelector
,一种是实现-methodSignatureForSelector
和-forwardInvocation
,比如
1 | - (id)forwardingTargetForSelector:(SEL)aSelector { |
实现了这个方法之后,最后系统会去我们提供的Student
对象去寻找print
方法,找到了的话就会进行调用。注意这个方法每次都会进来,每次都会生成新的Student
对象对其print
方法进行调用。
如果forwardingTargetForSelector
没实现或者返回空的话
1 | - (id)forwardingTargetForSelector:(SEL)aSelector { |
那么就会寻找-methodSignatureForSelector
和-forwardInvocation
1 | /// 方法签名: 返回值类型、参数类型 |
首先我们在方法签名中指定方法类型,然后在forwardInvocation
方法中,修改target,然后直接调用,这样子就会直接调用Student的print方法了

NSInvocation
当系统执行到forwardInvocation
的时候,无论在方法内有任何实现,都不会执行到doseNotRecognizeSelector
(抛出异常),所以实现了就不会崩,即便什么都不做。
在forwardInvocation
内我们可以拿到方法调用的很多信息,比如方法调用者anInvocation.target
、方法名anInvocation.selector
和方法参数
比如调用方法为[person print:1]
,进入到forwardInvocation
后我们可以通过以下方法拿到1
这个参数
1 | - (void)forwardInvocation:(NSInvocation *)anInvocation { |
如果要拿返回值的话,就可以这么做
1 | - (void)forwardInvocation:(NSInvocation *)anInvocation { |
NSMethodSignature
[NSMethodSignature signatureWithObjCTypes:"v16@0:8"]
,注意这里的方法编码要跟下面forwardInvocation
要调用的方法的方法编码一致
我们除了可以通过方法编码拿到NSMethodSignature
对象之外,还可以这么做
1 | - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { |
类方法的消息转发
+ (id)forwardingTargetForSelector:(SEL)aSelector
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
+ (void)forwardInvocation:(NSInvocation *)anInvocation
这几个方法也有类方法版本,但是代码补全没出来,但是实际使用是可行的
+ (id)forwardingTargetForSelector:(SEL)aSelector
在这个方法里面返回的消息接受者既可以是类对象也可以是实例对象。
@synthesize 和 @dynamic
@synthesize
用来自动给成员变量名生成setter
和getter
的声明和实现
@dynamic
用来告诉编译器不要自动生成setter
和getter
的实现(声明还是会声明的),待到运行时开发者自己实现,并且不会为属性实现带下划线的成员变量
super
假如我们调用Student
实例的一个父类方法
[super run:xx]
那么编译后会变成如下代码
objc_msgSendSuper(self,class_getSuperclass(objc_getClass("Student")),sel_registerName("run"),xx);
所以super
发送消息的本质是调用objc_msgSendSuper2
函数,最终的消息接受者依旧是self
,只是实例方法寻找从父类的类对象开始寻找,类方法从父类的元类对象开始寻找。
所以当我们调用方法[super class]
的时候,跟[self class]
没区别,返回结果是一致的