文章目录
-
- 信息传递机制
-
- 选择子SEL
- objc_msgSend
- 源码解析
-
- 快速查找imp过程
- 快速搜索总结
- 方法缓冲
- 慢速查找
- 总结慢速搜索
- 消息转发
-
- 动态决议
-
- 动态分析添加方法
- 消息转发
-
- 快速转发
- 快速转发测试
- 慢速转发
- 总结
-
- 动态决议
- 消息转发
- 三次拯救消息
- 流程图
- Q&A
-
- runtime是如何通过selector找到对应的IMP地址的?
- 以上两次打印的原因?
信息传递机制
在OC在语言中,调用对象的方法称为信息传递。 Objective-C 新闻直到时间绑定到实现方法上。编译器将信息表达式转换为调用信息函数。
例如:OC信息表达式如下(方法调用)
id returnValue = [someObject messageName:parameter];
someObject
称为接受者(receiver
),messageName
称为“选择子和参数一起称为,当编译看到这个消息时,它将转换为一个标准 C 语言函数调用。
id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);
选择子SEL
OC根据方法名称(包括参数序列)进行编译,生成唯一用来区分这种方法的方法ID,这个ID就是SEL类型的。 我们需要注意的是,只要方法的名字(包括参数序列)相同,那么他们的ID是一样的。所以不管是父子,名字都一样。ID是一样的。
SEL sell1 = @selector(eat:); NSLog(@"sell1:%p", sell1); SEL sell2 = @selector(eat); NSLog(@"sell2:%p", sell2); //sell1:0x100000f63 //sell2:0x100000f68
其中需要注意的是:@selector等于把方法名翻译成方法名SEL方法名。它只关心方法名和参数数数,而不关心返回值和参数类型
生成SEL这个过程是固定的,因为它只是一种表示方法的方法ID,不管是哪一类,这个都是写的eat方法,SEL值是固定的
在Runtime中维护一个SEL表,这个表存储SEL不按类别存储,只要相同SEL它将被视为一个并存储在表中。项目加载时,所有方法都将加载到表中,动态生成方法也将加载到表中。
不同类别可以有相同的方法,不同类别的实例对象执行相同的方法selector按照各自的方法列表,时间会去SEL去寻找自己类对应的IMP。
IMP本质上是一个函数指针,它包含一个接收信息的对象id,调用方法的SEL,以及一些方法参数,并返回一个id。所以我们可以通过SEL获得它对应的IMP,获得函数指针后,意味着我们获得了需要执行方法的代码入口,这样我们就可以像普通C语言函数调用一样使用函数指针。
objc_msgSend
我们可以看到转换被使用了objc_msgSend 以消息接收者和方法名为主要参数的函数,如下所示:
objc_msgSend(receiver, selector) // 不带参数 objc_msgSend(receiver, selector
, arg1
, arg2
,
.
.
.
)
// 带参数
objc_msgSend 通过以下几个步骤实现了动态绑定机制:
- 首先,获取 selector 指向的方法实现。由于相同的方法可能在不同的类中有着不同的实现,因此根据 receiver 所属的类进行判断。
- 其次,传递 receiver 对象、方法指定的参数来调用方法实现。
- 最后,返回方法实现的返回值。
消息传递的关键在于objc_class结构体,其有三个关键的字段:
isa
:指向类的指针。superclass
:指向父类的指针。methodLists
:类的方法分发表(dispatch table)。
当创建一个新对象时,先为其分配内存,并初始化其成员变量。其中 isa 指针也会被初始化,让对象可以访问类及类的继承链。
下图所示为消息传递过程的示意图:
- 当消息传递给一个对象时,首先从运行时系统缓存objc_cache中进行查找。如果找到,则执行。否则,继续执行下面步骤。
- objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法分发表methodLists中查找方法的selector。如果未找到,将沿着类的superclass找到其父类,并在父类的分发表methodLists中继续查找。
- 以此类推,一直沿着类的继承链追溯至NSObject类。一旦找到selector,传入相应的参数来执行方法的具体实现,并将该方法加入缓存objc_cache。如果最后仍然没有找到selector,则会进入消息转发流程。
源码解析
快速查找imp过程
用伪代码的原因就是objc_msgSend 是用汇编语言写的,针对不同架构有不同的实现。苹果为什么objc_msgSend这部分代码要使用汇编来编写呢?答案很简单–效率。。另外苹果在所有的汇编方法命值钱都会用下划线开头,目的是为了防止符号冲突。
以下是在arm64下的汇编:
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
GetTaggedClass
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY _objc_msgSend
- 从
cmp p0,#0
开始看,对比p0和0,b.le
是如果小于等于就跳转,b指令是跳转的意思,然后如果小于等于就跳转到LNilOrTagged
,执行LReturnZero
直接结束,重新在进入这个objc_msgSend流程。 - 如果消息接受者不为
nil
,汇编继续跑,到CacheLookup NORMAL,来看一下具体的实现:
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // if (sel != _cmd) {
b.ne 3f // scan more
// } else {
2: CacheHit \Mode // hit: call or return imp
// }
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
cmp p13, p10 // } while (bucket >= buckets)
b.hs 1b
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
add p13, p10, w11, UXTW #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
// p13 = buckets + (mask << 1+PTRSHIFT)
// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
add p13, p10, p11, LSL #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = first probed bucket
// do {
4: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // if (sel == _cmd)
b.eq 2b // goto hit
cmp p9, #0 // } while (sel != 0 &&
ccmp p13, p12, #0, ne // bucket > first_probed)
b.hi 4b
LLookupEnd\Function:
LLookupRecover\Function:
b \MissLabelDynamic
#if CONFIG_USE_PREOPT_CACHES
#if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16
#error config unsupported
#endif
LLookupPreopt\Function:
#if __has_feature(ptrauth_calls)
and p10, p11, #0x007ffffffffffffe // p10 = buckets
autdb x10, x16 // auth as early as possible
#endif
// x12 = (_cmd - first_shared_cache_sel)
adrp x9, _MagicSelRef@PAGE
ldr p9, [x9, _MagicSelRef@PAGEOFF]
sub p12, p1, p9
// w9 = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
#if __has_feature(ptrauth_calls)
// bits 63..60 of x11 are the number of bits in hash_mask
// bits 59..55 of x11 is hash_shift
lsr x17, x11, #55 // w17 = (hash_shift, ...)
lsr w9, w12, w17 // >>= shift
lsr x17, x11, #60 // w17 = mask_bits
mov x11, #0x7fff
lsr x11, x11, x17 // p11 = mask (0x7fff >> mask_bits)
and x9, x9, x11 // &= mask
#else
// bits 63..53 of x11 is hash_mask
// bits 52..48 of x11 is hash_shift
lsr x17, x11, #48 // w17 = (hash_shift, hash_mask)
lsr w9, w12, w17 // >>= shift
and x9, x9, x11, LSR #53 // &= mask
#endif
ldr x17, [x10, x9, LSL #3] // x17 == sel_offs | (imp_offs << 32)
cmp x12, w17, uxtw
.if \Mode == GETIMP
b.ne \MissLabelConstant // cache miss
sub x0, x16, x17, LSR #32 // imp = isa - imp_offs
SignAsImp x0
ret
.else
b.ne 5f // cache miss
sub x17, x16, x17, LSR #32 // imp = isa - imp_offs
.if \Mode == NORMAL
br x17
.elseif \Mode == LOOKUP
orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
SignAsImp x17
ret
.else
.abort unhandled mode \Mode
.endif
5: ldursw x9, [x10, #-8] // offset -8 is the fallback offset
add x16, x16, x9 // compute the fallback isa
b LLookupStart\Function // lookup again with a new isa
.endif
#endif // CONFIG_USE_PREOPT_CACHES
.endmacro
CacheHit
就是命中的话,则call or return imp
(返回这个方法的地址)。没找到的话就执行__objc_msgSend_uncached
。
查看一下它的实现:
就是去MethodTableLookup
查找方法,
其中bl表示调用了方法_lookUpImpOrForward,_lookUpImpOrForward在汇编里找不到,因为汇编的函数比C++的多一个下划线,需要去掉下划线,去找到lookUpImpOrForward方法实现。
快速查找总结
1.检查消息接收者receiver是否存在,为nil则不做任何处理 2.如果不为nil,通过receiver的isa指针找到对应的class类对象 3.找到class类对象进行内存平移,找到cache 4.从cache中获取buckets 5.从buckets中对比参数sel,看在缓存里有没有同名方法 6.如果buckets中有对应的sel --> cacheHit --> 调用imp 7.如果buckets中没有对应的sel --> _objc_msgSend_uncached -> _lookUpImpOrForward (c/c++慢速查找)
至此快速查找imp汇编部分就结束了,接下来到了慢速查找过程:c/c++环节
方法缓冲
在学习慢速查找方法缓冲之前,先要理解方法缓冲的概念:
苹果认为如果一个方法被调用了,那个这个方法有更大的几率被再此调用,既然如此直接维护一个缓存列表,把调用过的方法加载到缓存列表中,再次调用该方法时,先去缓存列表中去查找,如果找不到再去方法列表查询。这样避免了每次调用方法都要去方法列表去查询,大大的提高了速率。
慢速查找
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
runtimeLock.assertUnlocked();
if (slowpath(!cls->isInitialized())) {
...省略部分
for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// curClass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = forward_imp;
break;
}
}
// Halt if there is a cycle in the superclass chain.
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
if (fastpath(imp)) {
// Found the method in a superclass. Cache it in this class.
goto done;
}
}
// 未找到实现。请尝试一次方法解析器。
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
done_unlock:
runtimeLock.unlock();
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
- 检查类是否被初始化、是否是个已知的关系、确定继承关系等准备工作。
for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
// 如果是常量优化缓存
// 再一次从cache查找imp
// 目的:防止多线程操作时,刚好调用函数,此时缓存进来了
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// curClass方法列表。
method_t *meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
// 每次判断都会把curClass的父类赋值给curClass
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// 没有找到实现,方法解析器没有帮助。
// 使用转发。
imp = forward_imp;
break;
}
}
// 如果超类链中存在循环,则停止。
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
// 超类缓存。
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {
// 在超类中找到forward::条目。
// 停止搜索,但不要缓存;调用方法
// 首先为这个类解析器。
break;
}
if (fastpath(imp)) {
// 在超类中找到方法。在这个类中缓存它。
goto done;
}
}
进入了一个循环逻辑:
- 从本类的method list查找imp(查找的方式是getMethodNoSuper_nolock,一会分析);
- 从本类的父类的cache查找imp(cache_getImp汇编写的)
- 从本类的父类的method list查找imp,…继承链遍历…(父类->…->根父类)
- 若上面环节有任何一个环节查找到了imp,跳出循环,缓存方法到本类的cache(log_and_fill_cache);
- 直到查找到nil,指定imp为消息转发,跳出循环。
跳出循环后的逻辑:
done:
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
done_unlock:
runtimeLock.unlock();
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
如果找到了imp,就会把imp缓存到本类cache里(log_and_fill_cache):(注意这里不管是本类还是本类的父类找到了imp,都会缓存到本类中去)。
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
if (slowpath(objcMsgLogEnabled && implementer)) {
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
cls->cache.insert(sel, imp, receiver); // 插入缓存
}
getMethodNoSuper_nolock
查找方式
tatic method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
// fixme nil cls?
// fixme nil sel?
auto const methods = cls->data()->methods();
for (auto mlists = methods.beginLists(),
end = methods.endLists();
mlists != end;
++mlists)
{
// <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest
// caller of search_method_list, inlining it turns
// getMethodNoSuper_nolock into a frame-less function and eliminates
// any store from this codepath.
method_t *m = search_method_list_inline(*mlists, sel);
if (m) return m;
}
return nil;
}
在search_method_list_inline
里找到了method_t
就会返回出去了(search_method_list_inline
):
ALWAYS_INLINE static method_t *
search_method_list_inline(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->isExpectedSize();
if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
return findMethodInSortedMethodList(sel, mlist);
} else {
// Linear search of unsorted method list
if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
return m;
}
#if DEBUG
// sanity-check negative results
if (mlist->isFixedUp()) {
for (auto& meth : *mlist) {
if (meth.name() == sel) {
_objc_fatal("linear search worked when binary search did not");
}
}
}
#endif
return nil;
}
这里就是使用findMethodInSortedMethodList
和findMethodInUnsortedMethodList
通过sel找到method_t
的。这两个函数的区别就是: 前者是排好序的,后者是未排好序的;前者方法中的查询方式是二分查找,后者则是普通查找。
总结慢速查找
- 从本类的 method list (二分查找/遍历查找)查找imp
- 从本类的父类的cache查找imp(汇编)
- 从本类的父类的method list (二分查找/遍历查找)查找imp …继承链遍历…(父类->…->根父类)里找cache和method list的imp
- 若上面环节有任何一个环节查找到了imp,跳出循环,缓存方法到本类的cache,并返回imp
- 直到查找到nil,指定imp为消息转发,跳出循环,执行动态方法解析
resolveMethod_locked
消息转发
上面介绍到的是找到了消息,然后发送消息,那如果没有找到消息,该怎么处理呢?就需要消息转发。
动态决议
通过之前的源码发现,如果没找到方法则尝试调用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()) {
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
上述代码大致流程:
- 先判断判断进行解析的是否是元类
- 如果不是元类,则调用
resolveInstanceMethod
进行对象方法动态解析 - 如果是元类,则调用
resolveClassMethod
进行类方法动态解析,完成类方法动态解析后,再次查询cls中的imp,如果没有找到,则进行一次对象方法动态解析。
而这两个方法resolveInstanceMethod
和resolveClassMethod
则称为。
执行完上述代码后返回lookUpImpOrForwardTryCache
:
IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
return _lookUpImpTryCache(inst, sel, cls, behavior);
}
这个方法调用的是_lookUpImpTryCache
方法:
ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertUnlocked();
if (slowpath(!cls->isInitialized())) {
// see comment in lookUpImpOrForward
return lookUpImpOrForward(inst, sel, cls, behavior);
}
IMP imp = cache_getImp(cls, sel);
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
if (slowpath(imp == NULL)) {
return lookUpImpOrForward(inst, sel, cls, behavior);
}
done:
if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
return nil;
}
return imp;
}
进入_lookUpImpTryCache
源码,可以看到这里有cache_getImp
;也就是说在进行一次动态决议之后,还会通过cache_getImp
从cache
里找一遍方法的sel
。 如果还是没找到(imp == NULL)
?也就是无法通过动态添加方法的话,还会执行一次lookUpImpOrForward,这时候进lookUpImpOrForward
方法,这里behavior
传的值会发生变化。
第二次进入lookUpImpOrForward
方法后,执行到if (slowpath(behavior & LOOKUP_RESOLVER))
这个判断时:
// 这里就是消息转发机制第一层的入口
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
根据变化后的behavior
值和LOOKUP_RESOLVER
值之间的关系导致该if语句内部只能进入第一次,因此这个判断相当于单例。解释了为什么开头说的该动态解析resolveMethod_locked
为什么只执行一次。
动态解析添加方法
在动态决议阶段可以为类添加方法,以保证程序正常运行
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
@cls : 给哪个类对象添加方法
@name : SEL类型,给哪个方法名添加方法实现
@imp : IMP类型的,要把哪个方法实现添加给给定的方法名
@types : 就是表示返回值和参数类型的字符串
程序代码:
Person.m
#import "Person.h"
#import <objc/runtime.h>
@interface Person : NSObject
- (void)print;
@end
@implementation Person
@end
可以看到print方法并未实现,所以在主函数中调用程序一定会崩溃, 然后我们将代码修改为下面这样: 在.m文件中增加这个方法:
+(BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"%s, sel = %@", __func__, NSStringFromSelector(sel));
return [super resolveInstanceMethod:sel];
}
运行程序: 程序依然会崩溃。那么控制台为什么会打印两次信息呢也就是为什么会调用两次resolveInstanceMethod
方法呢?这个最后来说。
我们看看程序崩溃的原因: 是因为找不到imp而崩溃,那么我们可以在这个方法里通过runtime的class_addMethod
,给sel动态的生成imp。其中第四个参数是返回值类型,用void用字符串描述:“v@:”
- (void)addMethod {
NSLog(@"%s", __func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"%s, sel = %@", __func__, NSStringFromSelector(sel));
if(sel == @selector(print)) {
IMP imp = class_getMethodImplementation(self, @selector(addMethod));
class_addMethod(self, sel, imp, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
消息转发
如果系统在动态决议阶段没有找到实现,就会进入消息转发阶段。
快速转发
当cache没有找到imp,类的继承链里的方法列表都没有找到imp,并且resolveInstanceMethod / resolveClassMethod
返回NO就会进入消息转发。也就是所以如果本类没有能力去处理这个消息,那么就转发给其他的类,让其他类去处理。
从imp == (IMP)_objc_msgForward_impcache
进入消息转发机制。 查看一下这个方法: 竟然是汇编实现的这就又印证了汇编速度更快的结论:
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
之后就没有开源了🥹。
快速转发测试
- Person类中定义func1方法但是不实现,利用
forwardingTargetForSelector:(SEL)aSelector
方法进行消息快速转发。 - Blank类中定义func1方法且实现:
Blank.h
#import <Foundation/Foundation.h>
@interface Blank : NSObject
- (void)print;
@end
Blank.m
#import "Blank.h"
@implementation Blank
- (void)print {
NSLog(@"%s", __func__);
}
@end
Person.h
#import "Person.h"
#import "Blank.h"
#import <objc/runtime.h>
@implementation Person
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(print)) {
NSLog(@"%s, aSelector = %@",__func__, NSStringFromSelector(aSelector));
return [Blank alloc];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
转发的作用在于,如果当前对象无法响应消息,就将它转发给能响应的对象。 此时,方法缓冲在接收转发消息的对象。
慢速转发
如果消息的快速转发也没有找到方法;后面还有个methodSignatureForSelector
方法,作用是。 将刚才使用快速转发forwardingTargetForSelector
方法注释后,添加上methodSignatureForSelector
方法后能否正常运行?
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%s, aSelector = %@",__func__, NSStringFromSelector(aSelector));
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
运行之后发现程序会崩溃,因为这个方法需要搭配forwardInvocation
:
forwardInvocation
方法提供了一个入参,类型是NSInvocation
;它提供了target
和selector
用于指定目标里查找方法实现。
- (void)forwardInvocation:(NSInvocation *)anInvocation;
总结
防止系统崩溃的三个救命稻草:、、。 OC方法调用的本质就是消息发送,消息发送是SEL-IMP的查找过程。
动态决议
实例方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel;
// 系统通过该方法调用上面OC类里的实现
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
类方法:
+ (BOOL)resolveClassMethod:(SEL)sel;
消息转发
消息快速转发:
- (id)forwardingTargetForSelector:(SEL)aSelector;
消息慢速转发:
// 方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
// 正向调用
- (void)forwardInvocation:(NSInvocation *)anInvocation;
消息的三次拯救
- 动态方法解析
- 备援接收者
- 完整消息转发
流程图
Q&A
runtime是如何通过selector找到对应的IMP地址的?
答:
上面两次打印的原因?
答:
运行后,lldb输入指令bt可以看到打印的信息: 第一次进入断点时: 第二次进入断点时:
调用了___forwarding___符号,还有熟悉的慢速转发methodSignatureForSelector方法 ,可知第二次是消息转发;
在消息的第一次动态决议和快速转发都没找到方法后,进入到慢速转发。过程中,runtime还会调用一次lookUpImpOrForward,这个方法里包含了动态决议,这才造成了二次动态决议。