我们在前面讲解了objc_msgSend()
的快速查找和慢速查找流程,那么如果它们两个都没找到方法呢,系统会走向哪里呢,我们分析下它后面的流程。
类的初始化补充
我们在前面分析objc_msgSend()
的慢速查找流程时,讲到类的相关初始化,它里面有调用一个非常重要的方法+(void)initialize()
,也就是说class第一次接收消息
的时候会调用该方法,调用流程:realizeAndInitializeIfNeeded_locked——>initializeAndLeaveLocked——>initializeAndMaybeRelock——>initializeNonMetaClass——>callInitialize
,我们在initializeNonMetaClass
这个方法中还能看到一个非常关键的点,父类的initialize
在子类之前,我们看下图:
我们可以看到,若父类存在且没有被初始化,会递归调用initializeNonMetaClass
方法,所以可以非常清楚父类的初始化在子类之前。
慢速查找中获取父类缓存的流程补充
我们之前讲的类的缓存快速查找流程时,最终没有缓存命中CacheHit
时,调用_objc_msgSend_uncached
方法,但在进入慢速查找lookUpImpOrForward
时,再次查找父类缓存,调用cache_getImp
方法时,汇编走到了这里:
STATIC_ENTRY _cache_getImp
GetClassFromIsa_p16 p0, 0
CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant
LGetImpMissDynamic:
mov p0, #0
ret
父类缓存没有命中时,直接调用LGetImpMissDynamic
,那么imp=cache_getImp(curClass,sel)
此时得到nil
,进入下一轮循环,接着进入父类的methodlist
查找,跟之前类的缓存查找流程稍微有点区别:
消息动态决议
我们在前面已经知道若在类和父类的缓存和方法列表中都没有找到imp
,则会调用forward_imp
,实际在项目中是直接崩溃了的,我们看下图:
我们在CTPerson
声明了一个eat
方法,但是我们没有实现它,接着调用下:
我们可以看到工程崩溃了,并提示unrecognized selector sent to instance 0x1330ee0
,那系统为什么会提示这个错误呢?我们就可以分析下forward_imp
,而在lookUpImpOrForward
方法中对forward_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
.macro TailCallFunctionPointer
braaz $0
.endmacro
b __objc_msgForward
:跳转到__objc_msgForward
;__objc_msgForward
中TailCallFunctionPointer
只简单的返回$0
,那么我们就重点找__objc_forward_handler
,全局搜并没有找到这个函数,说明只会进入到源码中去,我们找objc_forward_handler
:
这里我们已经看到我们崩溃信息的提示,它是调用_objc_fatal()
返回的。那我们在项目崩溃前没有办法处理吗,显然不会的,我们有三次机会处理它resolveInstanecMethod()[动态方法决议]——>forwardingTargetForSelector[快速转发]——>methodSignatureForSelector[慢速转发]——>消息无法处理
这个流程,我们画张图清晰点:
我们从forward_imp
函数后继续看,当imp==forward_imp
时,跳出当前循环:
后面执行resolveMethod_locked()
方法:
这里写了一个算法,保证resolveMethod_locked
只会执行一次,behavior = 3
,汇编那里带过的值,LOOKUP_RESOLVER = 2
,behavior & LOOKUP_RESOLVER = 2
,下面behavior ^= LOOKUP_RESOLVER
,behavior
此时为1
,那么当下来代码再执行到此次时,就是 1 & 2 = 0
,条件就不会满足,resolveMethod_locked
不会被调用。
我们接着看下resolveMethod_locked()
方法的实现:
对象方法的动态决议
可以看到,不是元类的时候,调用的resolveInstanceMethod()
方法,我们进去看下:
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
//判断cls是有有实现resolve_sel,这里系统有默认实现,所以不会进入
if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
//系统向当前类cls发送resolve_sel消息,看当前类cls是否有实现resolve_sel
bool resolved = msg(cls, resolve_sel, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
//从当前类的缓存和方法里再次查询,看是否存在sel对应的imp
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
//下面都是一些打印和输出,不用看
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
所以我们上面源码可以看到resolveInstanceMethod()
函数主要用途:1.判断当前类cls
是否实现了resolveInstanceMethod
方法;2.调用lookUpImpOrNilTryCache
对当前类cls
的缓存和methodlist
再次查询一次。这里还有一个问题就是,resolveMethod_locked()
函数本身还调用了一次lookUpImpOrForwardTryCache()
,这两次调用查询有什么区别呢?我们看下_lookUpImpTryCache
方法实现:
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertUnlocked();
//判断类、元类是否做过初始化,若没有从新调用lookUpImpOrForward
if (slowpath(!cls->isInitialized())) {
// see comment in lookUpImpOrForward
return lookUpImpOrForward(inst, sel, cls, behavior);
}
//缓存是否存在imp
IMP imp = cache_getImp(cls, sel);
//若imp不存在,执行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=null,系统会再给一次机会,重新在走一次慢速查找
if (slowpath(imp == NULL)) {
return lookUpImpOrForward(inst, sel, cls, behavior);
}
done:
//
if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
return nil;
}
return imp;
}
首先是cls->isInitialized()
,判断类、元类是否做过相关的初始化,若没有直接调用lookUpImpOrForward()
慢速查找,若已经初始化,从当前类cls
的缓存查找,若缓存没有,再次调用lookUpImpOrForward()
查询,若找到imp
,直接执行done
,我们看到这里有条件判断:
// resolveMethod_locked 调用
IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
return _lookUpImpTryCache(inst, sel, cls, behavior);
}
//resolveInstanceMethod 调用
IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
{
return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}
//_lookUpImpTryCache函数done:
if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
return nil;
}
return imp;
上面resolveInstanceMethod()
函数调用的lookUpImpOrNilTryCache(inst, sel, cls, behavior | LOOKUP_NIL(4))
,那么当前的behavior
值为4
,说明系统已经给过一次的补救的机会,此时imp
还是等于_objc_msgForward_impcache
,说明当前类没有实现resolveInstanceMethod
或resolveClassMethod
方法,直接返回nil
;而lookUpImpOrForwardTryCache(inst, sel, cls, behavior)
的behavior
传值为1
,是从resolveMethod_locked
带过来的值,behavior & LOOKUP_NIL = 0
,所以会return imp
。
我们研究了源码之后,在工程中实际验证下,我们在CTPerson中
声明了一个对象方法eat
,但没有做实现,然后再CTPerson.m
文件中实现resolveInstanceMethod
方法:
我们在
resolveInstanceMethod
方法中判读sel==@selector(eat)
,动态给eat
添加一个run
方法的实现,我们打印输出下:
可以看到,调用
eat
方法,输出了run
,而且程序也没有崩溃。
类方法的动态决议
resolveMethod_locked()
方法里,若是元类,调用的resolveClassMethod()
:
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
这里要讲下还会调用一次resolveInstanceMethod
的原因,因为类方法在元类里相当于实例方法存在的,其实在底层是不存在类方法和对象方法的区分的,我们找对象方法的路径:class——>suprerclass——>NSObject——>nil
,类方法:metaClass——>rootMetalClass——>NSObject——>nil
。
我们看下resolveClassMethod
方法实现:
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);
}
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
项目验证消息动态决议
我们可以看到流程跟对象方法的动态决议,大致是一样的,只不过这里是向nonmeta
发送resolveClassMethod
消息。所以我们同样在工程中验证下:
我们看到没有实现processClassMethod
这个类方法,通过resolveClassMethod
方法里动态添加varifyClassMethod
类方法的实现,成功调用。
其实到这里我们已经很清楚,所有的类的方法里都会调用resolveInstanceMethod
,所有元类的方法都会调用resolveClassMethod
,而他们最终又都会找到NSObject
这里,我们其实可以把他们放在NSObject
的分类里处理:
这里我们还要考虑一个问题,如果resolveInstanceMethod
和resolveClassMethod
我们都没实现呢,后面如何处理呢?这里我们可以通过instrumentObjcMessageSends
辅助分析:
这里run
方法没有实现,所以工程崩溃,但这时候在沙盒/tmp/msgSends
下会有一个日志文件msgSends-xxxx
,我们打开后可以看到:
在resolveInstanceMethod
后,还有forwardingTargetForSelector
和methodSignatureForSelector
方法,这些方法有什么用,做了什么呢,我们后面分析这里。
消息转发
其实我们在前面直接调用instrumentObjcMessageSends
是有点奇怪的,这个方法从哪里来的呢,我们看下lookUpImpOrForward
里有找到imp
调用log_and_fill_cache
插入缓存数据的地方:
当objcMsgLogEnabled && implementer
为真时,会调用logMessageSend
发送日志,implementer
是curClass
,来到这里它肯定是存在的,而objcMsgLogEnabled
默认为false
,所以我们源码全局搜objcMsgLogEnabled
找到它赋值的地方:
所以我们才把instrumentObjcMessageSends(BOOL flag)
扩展出去。
消息快速转发——forwardingTargetForSelector
forwardingTargetForSelector
这个方法在Developer Documentation
有详细介绍,这里我们直接项目操作下:
我们在项目中没有实现run
这个方法,添加了forwardingTargetForSelector
方法实现,系统会自动调用它。在这里我们可以进行重定向,把它交给另一个对象student
处理:
我们看到消息成功的发送,打印student run
;
那如果student
也没有实现run
方法呢,系统接下来还会进行一次慢速转发流程。
消息慢速转发——methodSignatureForSelector
我们在Apple Documentation
里可以看到它返回的是一个方法签名NSMethodSignature
,它和forwardInvocation
方法配合使用:
接下来我们在CTPerson.m
文件实现methodSignatureForSelector
方法:
我们看到系统已经调用了methodSignatureForSelector
方法:
我们在官方文档上可以看到methodSignatureForSelector
和forwardInvocation
要配合使用,而且附有demo:
- (void)forwardInvocation:(NSInvocation *)invocation{
SEL aSelector = [invocation selector];
if ([friend respondsToSelector:aSelector])
[invocation invokeWithTarget:friend];
else
[super forwardInvocation:invocation];
}
我们在工程中验证下,没有问题CTStudent
中run
方法正常打印:
HOPPER反汇编
我们上面讲到的消息快速转发和慢速转发流程是从相关日志中推测出来的,其实有点不太正规的,我们可以bt
打印堆栈看下相关信息:
上图中我们可以看到CoreFoundation
几个关键的函数,__forwarding___
和__forwarding_prep_0___
,这两个函数在苹果开源的CoreFoundation
相关源码中无法找到,所以使用HOPPER
打开CoreFoundation
可执行文件反汇编看下相关流程,全局搜forwarding
:
coreFoundation
可执行文件,可以创建一个ios工程(模拟器启动即可),然后控制台输入image list
指令读取相关文件路径,找到coreFoundation
的可执行文件路径。
在__forwarding_prep_0___
下可以看到__forwarding__
的调用,点进去看下:
上图我们可以看到一个关键函数forwardingTargetForSelector
,这里做了判断class_respondsToSelector(r12,@selecor(forwardingTargetForSelector:))
,是否当前类能够相应forwardingTargetForSelector
,若不能,go to loc_646a7
;若当前类有实现forwardingTargetForSelector
,调用forwardingTargetForSelector
,调用forwardingTargetForSelector
返回一个rax
,若rax
不存在或者rax
等于rbx(self)
,则 go to loc_646a7
,我们看下loc_646a7
:
上图我们看到class_respondsToSelector(r12,@selector(methodSignatureForSelector))
,判断类是否能够相应methodSignatureForSelector
方法,若可以响应,则直接调用。这里的_forwardStackInvocation
是系统内部方法,没有对外暴露。接着下面执行,就看到forwardInvocation
方法:
若forwardInvocation
没有实现,则调用doesNotRecognizeSelector
。我们看张图熟悉下消息转发的流程:
我们开始在沙盒/tmp/msgSends
下看到的日志文件中resolveInstanceMethod
被调用了两次,这里工程打印看下看下:
我们看到在慢速转发后,有一个_forwardStackInvocation
,之后又调用了一次resolveInstanceMethod
,说明系统在这里又触发了一次查询,我们在源码resovleInstanceMethod
里打个断点:
第二次调用resolveInstanceMethod
走到这里时,bt
打出堆栈信息,可以看到执行顺序:class_respondsToSelector_inst——>lookUpImpOrNilTryCache——>lookUpImpOrForward——>resolveMethod_locked——>resolveInstanceMethod
。