消息传递是什么
Objective-C是一种动态类型语言,这意味着在编译时并不确定对象的具体类型,而是在运行时决定。消息传递机制允许程序在运行时向对象发送消息,对象再决定如何响应这些消息。
当你通过对象调用方法时,例如像这样[obj someMethod]
编译器会将其转换为一个消息发送的底层调用,通常是 objc_msgSend(obj, @selector(someMethod))
。这个函数接受两个主要参数:方法的调用者和方法选择器(也就是方法名)。
void objc_msgSend(id self, SEL cmd, ....)
第一 个参数代表接收者也就是方法调用者,第二个参数代表方法选择器(SEL 是选择子的类型)也就是方法的名字,后续参数就是消息中的 那些参数,其顺序不变。
选择子SEL
选择子(Selector)是用于表示方法名的数据类型。它是一个在运行时由编译器生成的唯一的标识符,用于在对象中查找并调用相应的方法。
OC在编译时会根据方法的名字(包括参数序列),生成一个用来区分这个方法的唯一的一个ID,这个ID就是SEL类型的。我们需要注意的是,只要方法的名字(包括参数序列)相同,那么他们的ID就是相同的。所以不管是父类还是子类,名字相同那么ID就是一样的
IMP
IMP
是一个函数指针,它指向方法的实际实现。当运行时系统找到了与选择器相匹配的方法时,它会获取该方法的IMP
,然后调用这个函数指针来执行方法的代码。
IMP
通常被声明为id
返回类型和接受id
类型的self
和SEL
类型的_cmd
参数的函数指针。例如:
typedef id (*IMP)(id, SEL, ...);
SEL与IMP的关系
在运行时系统中,SEL
和IMP
是紧密相关的。当你调用一个方法时,运行时系统首先将方法名解析为SEL
,然后使用这个SEL
去查找与之对应的IMP
。一旦找到IMP
,运行时系统就会调用这个函数指针来执行方法的代码。
消息传递的流程
首先,Runtime系统会通过obj的 isa 指针找到其所属的class
接着在这个类的缓存中查找与选择器匹配的方法实现
如果缓存中没找到接着在这个类的方法列表(method list)中查找与选择器(someMethod)匹配的方法实现(IMP)。
如果在当前类中没有找到,Runtime会沿着类的继承链往它的 superclass 中查找,也是先查缓存再查方法列表,直到到达根类(通常为 NSObject)。
一旦找到someMethod这个函数,就去执行它的实现IMP 。
如果直到根类还是没找到就会进行消息转发流程。
消息发送(Messaging)是 Runtime 通过 selector 快速查找 IMP 的过程,有了函数指针就可以执行对应的方法实现;消息转发(Message Forwarding)是在查找 IMP 失败后执行一系列转发流程的慢速通道,如果不作转发处理,则会打日志和抛出异常。
objc_msgSend()的伪代码如下:
id objc_msgSend(id self, SEL _cmd, ...) { //获取当前对象的类对象用于在运行时获取方法实现 Class class = object_getClass(self); //根据类对象和选择器_cmd获取IMP指针 IMP imp = class_getMethodImplementation(class, _cmd); //判空IMP并返回该方法实现 return imp ? imp(self, _cmd, ...) : 0; }
上面代码之所以是伪代码是因为objc_msgSend
是用汇编语言写的,针对不同架构有不同的实现。汇编语言的效率比c/c++更快,它直接对寄存器进行访问和操作,相比较内存的操作更加底层效率更高。
其源代码比较多,下面是核心部分:
ENTRY _objc_msgSend // 开始_objc_msgSend函数的定义 MESSENGER_START // 标记消息传递的开始 NilTest NORMAL // 检查self是否为nil,如果是nil则引发异常 GetIsaFast NORMAL // 快速获取self对象的isa指针,isa指向对象的类信息 // r11 = self->isa CacheLookup NORMAL // 在缓存中查找IMP,如果找到则直接调用IMP // 这里利用了方法缓存,以提高性能 NilTestSupport NORMAL // 如果self是nil的备用处理 GetIsaSupport // 如果需要,更全面地获取isa信息 // 这是为了支持特殊情况下的isa获取 // cache miss: go search the method lists LCacheMiss: // 缓存中没找到,开始在方法列表中查找IMP // isa仍然在r11寄存器中 MethodTableLookup %a1, %a2 // 在方法表中查找与选择器匹配的IMP // r11 = IMP,这里查找方法实现 cmp %r11, %r11 // 设置eq标志位,用于非 stret (simple) 的方法转发 // 这里比较r11与自身,实际上是设置条件码,用于判断是否需要转发 jmp *%r11 // 跳转到IMP地址并执行 // 这里直接调用了方法实现 END_ENTRY _objc_msgSend // 结束_objc_msgSend函数的定义
MethodTableLookup
宏是重点,负责在缓存没命中时在方法表中负责查找 IMP:
.macro MethodTableLookup MESSENGER_END_SLOW SaveRegisters // _class_lookupMethodAndLoadCache3(receiver, selector, class) movq $0, %a1 movq $1, %a2 movq %r11, %a3 call __class_lookupMethodAndLoadCache3 // IMP is now in %rax movq %rax, %r11 RestoreRegisters .endmacro
从上面的代码可以看出方法查找 IMP 的工作交给了 OC 中的 _class_lookupMethodAndLoadCache3
函数,并将 IMP 返回(从 r11
挪到 rax
)。最后在 objc_msgSend
中调用 IMP。
消息传递快速查找
首先检查消息接收者
receiver
是否存在,不存在则不做任何处理。接着通过
receiver
的isa指针找到对应的class
类对象,从
cache
中获取buckets
,从buckets
中对比参数sel
,看在缓存里有没有同名方法如果buckets中有对应的SEL则返回它对应的IMP如果在缓存中没有找到匹配的方法选择子,则执行慢速查找过程,即调用 _objc_msgSend_uncached 函数,并进一步调用 _lookUpImpOrForward 函数进行全局的方法查找。
简单来说快速查找就是在所属类的缓存中查找SEL对应的IMP指针
buckets是cache中的结构体,bucket_t的结构体中存储了一个unsigned long和一个IMP。IMP是一个函数指针,指向了一个方法的具体实现。
cache_t中的bucket_t *_buckets其实就是一个散列表,用来存储Method的链表。
消息传递慢速查找
当消息发送的快速查找过程无法找到匹配的方法实现时,就会进入 _lookUpImpOrForward
函数根据继承链和协议信息逐级查找方法。
- 从本类的 method list **(二分查找/遍历查找)**查找imp
- 从本类的父类的cache查找imp**(汇编)**
- 从本类的父类的method list **(二分查找/遍历查找)**查找imp …继承链遍历…(父类->…->根父类)里找cache和method list的imp
- 若上面环节有任何一个环节查找到了imp,跳出循环,缓存方法到本类的cache,并返回imp
- 直到查找到nil,指定imp为消息转发,跳出循环,执行动态决议resolveMethod_locked(消息转发的内容)
简单来说慢速查找就是在所属类的方法列表中查找SEL对应的IMP指针,没找到就沿着继承链一直查找方法缓存和方法列表,找到就返回IMP,没找到就执行动态决议
lookUpImpOrForward函数
前面提到的 _class_lookupMethodAndLoadCache3
函数其实就是简单的调用了 lookUpImpOrForward
函数:
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls) { return lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/); }
注意 lookUpImpOrForward
调用时使用缓存参数传入为 NO
,因为之前快速查找的时候已经尝试过查找缓存了。
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver)
实现了一套查找 IMP 的标准路径,也就是在消息转发(Forward)之前的逻辑。
下面是lookUpImpOrForward函数源码:
NEVER_INLINE IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior) { const IMP forward_imp = (IMP)_objc_msgForward_impcache; // 定义转发的IMP IMP imp = nil; // 初始化IMP为nil Class curClass; runtimeLock.assertUnlocked(); // 确认运行时锁未被持有 // 如果类未初始化,设置LOOKUP_NOCACHE,防止缓存单个条目的情况 if (slowpath(!cls->isInitialized())) { behavior |= LOOKUP_NOCACHE; } runtimeLock.lock(); // 锁住运行时锁 // 验证类是否是已知的类 checkIsKnownClass(cls); // 确保类已实现和初始化 cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE); curClass = cls; // 当前类 for (unsigned attempts = unreasonableClassCount(); ;) { // 如果类的缓存是常量优化缓存 // 再一次从cache查找imp // 目的:防止多线程操作时,刚好调用函数,此时缓存进来了 if (curClass->cache.isConstantOptimizedCache(true)) { #if CONFIG_USE_PREOPT_CACHES imp = cache_getImp(curClass, sel); // 从缓存中查找IMP if (imp) goto done_unlock; // 如果找到IMP,结束循环 curClass = curClass->cache.preoptFallbackClass(); #endif } else { // 从当前类的方法列表中查找匹配的选择器 method_t *meth = getMethodNoSuper_nolock(curClass, sel); if (meth) { imp = meth->imp(false); // 获取IMP goto done; } // 每次判断都会把curClass的父类赋值给curClass if (slowpath((curClass = curClass->getSuperclass()) == nil)) { imp = forward_imp; // 使用转发IMP break; } } // 防止超类链中出现循环 if (slowpath(--attempts == 0)) { _objc_fatal("Memory corruption in class list."); } // 在超类缓存中查找IMP imp = cache_getImp(curClass, sel); if (slowpath(imp == forward_imp)) { // 在超类中发现转发条目,停止搜索 break; } if (fastpath(imp)) { // 在超类中找到方法,缓存结果 goto done; } } // 尝试方法解析器 if (slowpath(behavior & LOOKUP_RESOLVER)) { behavior ^= LOOKUP_RESOLVER; return resolveMethod_locked(inst, sel, cls, behavior); } done: if (fastpath((behavior & LOOKUP_NOCACHE) == )) { #if CONFIG_USE_PREOPT_CACHES while (cls->cache.isConstantOptimizedCache(true)) { cls = cls->cache.preoptFallbackClass(); } #endif log_and_fill_cache(cls, imp, sel, inst, curClass); // 填充缓存 } done_unlock: runtimeLock.unlock(); // 解锁 // 如果设置LOOKUP_NIL并且IMP是转发IMP,则返回nil if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) { return nil; } return imp; // 返回找到的IMP }
总体的流程如下:
首先检查接收者 inst
是否为空,如果为空则直接返回空。
接下来,代码根据接收者的类对象 cls
进行一系列的处理和查找操作,以找到适当的方法实现 imp
。包括:
检查类对象是否已经初始化,如果尚未初始化则将 LOOKUP_NOCACHE 标志添加到 behavior 中,避免缓存查找。
通过 realizeAndInitializeIfNeeded_locked 函数对类对象进行实例化和初始化处理,确保类对象已经准备就绪。
使用循环逐级查找方法实现,包括在类的缓存中查找、在类的方法列表中查找、在父类链中查找。如果找到了匹配的方法实现,则跳转到 done 标签处。
如果在查找过程中找不到匹配的方法实现,则说明需要进行消息转发。将消息转发的默认实现 forward_imp 赋给 imp。
如果设置了 LOOKUP_RESOLVER 标志,说明需要调用方法解析器进行进一步处理,跳转到 resolveMethod_locked 函数进行解析。
在查找或转发结束后,如果未设置 LOOKUP_NOCACHE 标志,将找到的方法实现 imp 缓存到类对象的缓存中。
代码解锁runtime锁,根据需要返回找到的方法实现
imp
或空值
在循环中,首先检查当前类对象的缓存是否是常量优化缓存(isConstantOptimizedCache)。如果是常量优化缓存,代码尝试从缓存中获取方法实现(cache_getImp(curClass, sel))。如果成功获取到方法实现,则跳转到 done_unlock 标签处,结束查找。
如果当前类对象的缓存不是常量优化缓存,代码继续执行。通过调用 getMethodNoSuper_nolock 函数在当前类对象的方法列表中查找方法(meth = getMethodNoSuper_nolock(curClass, sel))。如果找到匹配的方法,则获取对应的方法实现(imp = meth->imp(false)),跳转到 done 标签处,结束查找。
如果在当前类对象的方法列表中没有找到匹配的方法实现,代码继续执行。将当前类对象的父类赋值给 curClass,并判断是否为 nil。如果父类为 nil,说明已经到达了继承链的顶端,没有找到匹配的方法实现。此时将默认的转发实现 forward_imp 赋给 imp,并跳出循环。
在循环的每次迭代中,会将 attempts 的值减一,表示尚未完成的查找次数。如果 attempts 的值减到零,则说明类对象的继承链中存在循环,这是不合理的。此时会触发一个错误,终止程序执行。
如果在当前类对象的缓存中找到了转发的条目(imp == forward_imp),表示在父类的缓存中找到了转发的方法实现。这时会停止循环,但不会将转发的方法实现缓存,而是先调用方法解析器来处理。
最后,在循环结束后,会根据需要将找到的方法实现缓存到类对象的缓存中,然后解锁运行时锁,并根据需要返回找到的方法实现或空值。
动态决议 resolveMethod_locked
当慢速查找依然没有找到IMP
时,会进入方法动态解析阶段,源码如下:
// 尝试方法解析器 if (slowpath(behavior & LOOKUP_RESOLVER)) { behavior ^= LOOKUP_RESOLVER; return resolveMethod_locked(inst, sel, cls, behavior); }
这里调用了resolveMethod_locked方法,下面是它的源代码:
static NEVER_INLINE IMP resolveMethod_locked(id inst, SEL sel, Class cls, int behavior) { runtimeLock.assertLocked(); ASSERT(cls->isRealized()); //**加锁** runtimeLock.unlock(); //**判断是否是元类** if (! cls->isMetaClass()) { //**不是元类,调用resolveInstanceMethod方法** resolveInstanceMethod(inst, sel, cls); } else { //**是元类,调用resolveClassMethod** resolveClassMethod(inst, sel, cls); //**如果调用上面的方法还没有找到,尝试调用resolveInstanceMethod** //**原因是根据isa的继承链,根元类的父类是NSObject,所以在元类中如果没有找到** //**最后可能会在NSObjct中找到目标方法** if (!lookUpImpOrNilTryCache(inst, sel, cls)) { resolveInstanceMethod(inst, sel, cls); } } //**重新调用lookUpImpOrForwardTryCache方法,返回方法查找流程** //**因为已经进行过一次动态方法决议,下次将不会再进入,所以不会造成死循环** return lookUpImpOrForwardTryCache(inst, sel, cls, behavior); }
首先判断进行解析的是否是元类
如果不是元类,则调用
_class_resolveInstanceMethod
进行实例方法动态解析如果是元类,则调用
_class_resolveClassMethod
进行类方法动态解析b完成类方法动态解析后,再次查询cls中的imp,如果没有找到,则进行一次对象方法动态解析
最后执行
lookUpImpOrForwardTryCache
函数resolveInstanceMethod
和resolveClassMethod
也称为方法的动态决议。resolveInstanceMethod方法
static void resolveInstanceMethod(id inst, SEL sel, Class cls) { runtimeLock.assertUnlocked(); //**如果目标类没有初始化,直接报错** ASSERT(cls->isRealized()); //**创建一个方法名为resolveInstanceMethod的SEL** SEL resolve_sel = @selector(resolveInstanceMethod:); //**判断resolveInstanceMethod是否在目标类中实现** //**如果我们的类是继承自NSObject的话,那么这个判断就永远为false** //**因为在NSObject中已经默认实现了resolveInstanceMethod方法** //**因为是去cls->ISA也就是元类中查找,所以我们可以断定,resolveInstanceMethod是个类方法** if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) { // Resolver not implemented. return; } //**强制类型转换objc_msgSend** BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend; //**通过objc_msgSend方法调用resolveInstanceMethod** bool resolved = msg(cls, resolve_sel, sel); //**拼接上LOOKUP_NIL参数后,重新调用方法查找流程** //**虽然调用了resolveInstanceMethod,但是这个返回值是bool** //**所以我们要获取对应的imp,还是需要通过方法查找流程** //**如果通过resolveInstanceMethod添加了方法,就缓存在类中** //**没添加,则缓存forward_imp** IMP imp = lookUpImpOrNilTryCache(inst, sel, cls); //**组装相应的信息** if (resolved && PrintResolving) { if (imp) { ……… } else { ……… } } }
首先创建一个方法名为resolveInstanceMethod
的SEL
对象resolve_sel
;
然后判断resolve_sel
是否实现,如果继承NSObject
则必定已经实现,这里通过cls->ISA
可以知道,resolveInstanceMethod
是个类方法
;
通过objc_ msgSend
直接调用resolveInstanceMethod
方法,因为是直接对cls
发送消息,所以也可以看出resolveInstanceMethods
是类方法
;
调用lookUpImpOrNilTryCache
方法,重新返回到方法查找的流程当中去;
resolveClassMethod方法
static void resolveClassMethod(id inst, SEL sel, Class cls) { runtimeLock.assertUnlocked(); ASSERT(cls->isRealized()); ASSERT(cls->isMetaClass()); //**判断resolveClassMethod是否实现,NSObject中默认实现** if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) { // Resolver not implemented. return; } //**获取目标类** Class nonmeta; { mutex_locker_t lock(runtimeLock); nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst); // +initialize path should have realized nonmeta already if (!nonmeta->isRealized()) { _objc_fatal("nonmeta class %s (%p) unexpectedly not realized", nonmeta->nameForLogging(), nonmeta); } } //**强制类型转换objc_msgSend** BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend; //**通过objc_msgSend调用类中的resolveClassMethod方法** bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel); //**拼接上LOOKUP_NIL参数后,重新调用方法查找流程** //**类方法实际上就是元类对象中的对象方法,所以可以通过方法查找流程在元类中查找** //**如果通过resolveClassMethod添加了,就缓存方法在元类中** //**没添加,则缓存forward_imp** IMP imp = lookUpImpOrNilTryCache(inst, sel, cls); if (resolved && PrintResolving) { if (imp) { …… } else { …… } } }
这个方法与resolveInstanceMethod
比较类似,如果通过resolveClassMethod
方法添加了目标imp
,则将其缓存在目标元类
中,否则缓存forward_imp
。
lookUpImpOrNilTryCache方法
在resolveInstanceMethod
和resolveClassMethod
中都会调用的lookUpImpOrNilTryCache
方法
extern IMP lookUpImpOrNilTryCache(id obj, SEL, Class cls, int behavior = 0); IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior) { //**这里behavior没传,所以是默认值0** //**behavior | LOOKUP_NIL = 0 | 4 = 4** return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL); }
给参数behavior
拼接上LOOKUP_NIL
然后调用_lookUpImpTryCache
方法
lookUpImpTryCache方法
ALWAYS_INLINE static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior) { runtimeLock.assertUnlocked(); //**判断类是否初始化** if (slowpath(!cls->isInitialized())) { //**没有初始化直接调用lookUpImpOrForward** //**里面针对没初始化的类,有相关处理** return lookUpImpOrForward(inst, sel, cls, behavior); } //**去缓存中,查找sel是否有对应的imp** IMP imp = cache_getImp(cls, sel); //**找到了则跳去done** if (imp != NULL) goto done; //**没找到继续往下走,去共享缓存中查找** #if CONFIG_USE_PREOPT_CACHES if (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) { imp = cache_getImp(cls->cache.preoptFallbackClass(), sel); } #endif //**没找到对应的imp,调用lookUpImpOrForward方法查找,behavior = 4** //** 4 & 2 = 0 ,所以这次方法查找不会再次进行动态方法决议** //**将_objc_msgForward_impcache缓存起来,方便下次直接返回** if (slowpath(imp == NULL)) { return lookUpImpOrForward(inst, sel, cls, behavior); } done: //**命中缓存中,并且sel对应的imp为_objc_msgForward_impcache** //**说明动态方法决议已经执行过,且没有添加imp,则直接返回空** if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) { return nil; } //**说明动态方法决议中添加了对应的imp** return imp; }
首先判断类是否初始化,如果没有初始化则直接调用
lookUpImpOrForward
,里面有针对没初始化的类进行相应的处理;然后去缓存中进行
方法的快速查找
,找到了就去done
缓存中没找到,如果支持共享缓存,则去共享缓存中查找
都没有查找到,则通过慢速方法查找去查找方法,由于
behavior
的值发生改变,这次慢速查找不会再次调用动态方法决议
在
done
流程中,如果已经执行过动态方法决议
且并没有添加imp
,则缓存中的sel
对应imp
为_objc_msgForward_impcache
,这时直接返回nil
。否则返回添加的imp
实现。
如果系统在动态决议阶段没有找到实现,就会进入消息转发阶段。