上篇文章OC之消息发送的结尾,在lookUpImpOrForward
函数中,我们提到,如果一个函数在cache、本类中、父类中都没有找到,那么就会调用resolveMethod_locked
进行动态解析,本文就主要看下该过程。
动态方法决议
我们知道,在lookUpImpOrForward
函数中,有如下代码:
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
复制代码
记得我们在objc_msgSend
未找到方法,然后调用lookUpImpOrForward
的时候,在汇编下调用了MethodTableLookup
方法,如下:
.macro MethodTableLookup
SAVE_REGS MSGSEND
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
bl _lookUpImpOrForward
// IMP in x0
mov x17, x0
RESTORE_REGS MSGSEND
复制代码
这里我们可以看到lookUpImpOrForward
传入的第一个参数为实例对象obj
,第二个为方法名sel
,第三个类cls
,第四个为LOOKUP_INITIALIZE | LOOKUP_RESOLVER
为1|2
,也就是3
我们因此可以知道
behavior
在初次进入的时候值为3;LOOKUP_RESOLVER
定义的该值为2;behavior & LOOKUP_RESOLVER
为3 & 2
值为2;behavior ^= LOOKUP_RESOLVER
为behavior = 3 ^ 2 = 1
;
然后进入到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()) {
//cls类如果不是元类就走这里
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else {
//cls类如果是元类就走这里,然后调用类的决议方法
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
//如果cls类中的缓存和cls本类及继承链中都没有imp,则调用下方的方法
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
复制代码
我们知道cls->isMetaClass()
的作用是判断cls
是否是元类,并且对象的实例方法是存在类中的,而类方法是存在元类中的,因此这里:
- 如果
cls是类
,也就是实例方法会调用resolveInstanceMethod
方法, - 如果
cls是元类
,类方法则会调用resolveClassMethod
方法,
这里的两个方法:resolveInstanceMethod
和resolveClassMethod
。也称为方法的动态决议。
实例方法动态决议
我们首先看下resolveInstanceMethod
resolveInstanceMethod
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
//首先定义resolveInstanceMethod的方法
SEL resolve_sel = @selector(resolveInstanceMethod:);
//先尝试在类的缓存中查找是否有该resolveInstanceMethod方法
if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
// Resolver not implemented.
//没有返回
return;
}
//调用类的resolveInstanceMethod方法
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
//objc_msgSend(消息接收者,方法名,参数),相当于在类中调用resolveInstanceMethod方法,返回true代表处理了该方法,否则就有问题。
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
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
if (resolved && PrintResolving) {
//处理错误信息
}
}
复制代码
在这里resolveInstanceMethod
函数的入参依次为,实例对象、方法名、类对象
。
- 如果这个类之前缓存有
resolveInstanceMethod
方法,那么就直接调用然后返回了; - 如果这个类之前没有缓存,那么就需要这个类来调用
resolveInstanceMethod
方法。
不管哪一种,我们看到,cls
类都是作为第一个参数来调用,我们可以知道resolveInstanceMethod
方法其实是一个类方法。(实例方法在调用的时候,第一个参数为实例对象)也就是系统在找不到方法实现的时候,就会运行到这里,去类中找一个resolveInstanceMethod
方法。我们可以验证下:
先创建一个FMEmployee
的类,并只添加test1
的方法声明,不做实现,并在类中添加resolveInstanceMethod
的类方法,如下:
@interface FMEmployee : NSObject
- (void)test1;
@end
@implementation FMEmployee
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"%s ---> %@",__func__,NSStringFromSelector(sel));
return [super resolveInstanceMethod:sel];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Hello World!");
FMEmployee *p = [[FMEmployee alloc] init];
[p test1];
}
}
复制代码
由于我们并没有做方法的实现,所以程序崩掉了,我们看到在崩溃前有打印
test1
方法,说明resolveInstanceMethod
被调用了。
- 那么该如何
避免此崩溃呢
? - 另外在崩溃前,我们发现
resolveInstanceMethod 函数被调用了两次
,这又是为什么呢?
我们先看下如何避免崩溃,首先我们可以通过runtime
来动态的添加方法的实现,如下图所所示
运行结果如下:
发现其可以正常运行。这时如果我们打印下
FMEmployee
的缓存,可以看到cache中
缓存有test1函数
,并且缓存的方法的实现为调用了resolveInstanceMethod
函数进行方法决议后的method
方法。
类方法动态决议
回到resolveMethod_locked
方法中,我们看到如果是元类则会调用resolveClassMethod
,我们先看下方法定义:
resolveClassMethod
/***********************************************************************
* resolveClassMethod
* Call +resolveClassMethod, looking for a method to be added to class cls.
* cls should be a metaclass.
* Does not check if the method already exists.
**********************************************************************/
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
// cls为元类,在元类中查找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);
if (resolved && PrintResolving) {
。。。。
}
}
复制代码
在这里我们可以先看下注释,说
- 调用
+resolveClassMethod
, - 查找要添加到
类cls
的resolveClassMethod
方法, cls是一个元类
;
根据注释来看整一套流程其实也就是元类调用resolveClassMethod
函数的过程。
既然如此,我们在FMEmployee
中按照实例方法的流程添加一个类方法callFunc
,但是不做实现,然后看看是否会调用resolveClassMethod
,代码及实际运行如下:
@interface FMEmployee : NSObject
- (void)test1;
+ (void)callFunc;
@end
@implementation FMEmployee
+ (BOOL)resolveClassMethod:(SEL)sel{
NSLog(@"%s ---> %@",__func__,NSStringFromSelector(sel));
return [super resolveInstanceMethod:sel];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Hello World!");
[FMEmployee callFunc];
}
}
复制代码
我们看到最终也是调用了resolveClassMethod
方法,如果我们也通过运行时,把callFunc方法
动态添加个方法实现,这时我们看到callFunc
没有报错,且调用了method2的imp
。
看完resolveClassMethod
函数,我们再次回到resolveMethod_locked
方法中,我们看到元类在调用了resolveClassMethod
之后,如果元类中没有imp,那么又再一次调用了resolveInstanceMethod
,这是为什么呢?
- 我们知道
实例方法
是存在类里边,而类方法
是存放在元类中; - 当实例方法找不到的时候,就会进行实例方法决议,调用类中的
resolveInstanceMethod
方法; - 当类方法找不到的时候,我们调用类中的
resolveClassMethod
方法;但是如果没有resolveClassMethod
方法,那么我们本应该调用元类的resolveInstanceMethod
方法,但是元类我们是无法修改的,根据类与元类的继承关系,就会继续往根元类找,最终找到NSObject
的resolveInstanceMethod
方法。这一整套调用链路会变得非常长,影响系统运行效率;(如果NSObject
没有resolveInstanceMethod
方法,我们可以通过写分类进行添加) - 因此苹果提供
resolveClassMethod
方法,其实就是为了简化类方法的查找流程,方便在类方法找不到时,直接通过resolveClassMethod
来进行类方法决议,提升调用效率; - 而
resolveInstanceMethod
其实才是获取方法决议的根本,如果提供的resolveClassMethod
找不到,就需要再次调用resolveInstanceMethod
。
另: 在苹果的注释中有这么两行代码,
如果我们不调用
resolveClassMethod
改成[cls resolveInstanceMethod:sel];
,并在NSObject
的分类中添加上resolveInstanceMethod
函数对callfunc
函数的重定向,就会发现没有报错,并且走到NSObject分类
中进行方法决议:
这也正是上文中所说的resolveInstanceMethod
方法是决议根本。
resolveInstanceMethod 函数被调用了两次
那resolveInstanceMethod 函数被调用了两次
这又是怎么回事呢?对于这个现象可以分别打印一下这两次resolveInstanceMethod
的堆栈信息:
- 我们看到,第一次进入的时候堆栈信息显示,走的是方法的慢速查找,然后方法动态解析的流程,之后调用了
resolveInstanceMethod
方法; - 第二次进入的时候,先调用了
___forwarding___
,然后又调用methodSignatureForSelector
等方法;这是是因为走了消息转发的流程,如果消息转发过程没有处理,又会调用class_getInstanceMethod
函数,这个函数又会调用一次lookUpImpOrForward
进行慢速查找,所以又会再调用一次resolveInstanceMethod
;
总结
总结这部分动态方法决议的流程如下:
- 如果没有找到目标方法就会调用
resolveMethod_locked(inst, sel, cls, behavior)
- 判断
cls
是否是元类cls
是元类- 调用
resolveClassMethod(inst, sel, cls)
,使用类方法的动态方法决议- 去类中调用
resolveClassMethod
方法,如果没有,看继承链或者分类
中是否有实现resolveClassMethod
。
- 去类中调用
lookUpImpOrNilTryCache(inst, sel, cls)
,类方法解析未找到或者为空- 如果没有找到或者为空,则执行
resolveInstanceMethod(inst, sel, cls)
,
- 如果没有找到或者为空,则执行
- 调用
cls
不是元类- 执行
resolveInstanceMethod(inst, sel, cls)
,使用对象方法的动态方法决议,- 去类中调用
resolveClassMethod
方法,如果没有,看继承链或者分类
中是否有实现resolveInstanceMethod
。
- 去类中调用
- 执行
graph TB
meizhaodao[方法调用没找到]-->sNode[resolveMethod_locked] -->|不是元类|shili[resolveInstanceMethod]
shili -->|通过objc_msgSend|msgSendIns[调用类中的resolveInstanceMethod方法]-->other[缓存并调用具体实现的imp]
msgSendIns
shili .->|本类中没实现resolveInstanceMethod|object[看继承链或分类是否处理resolveInstanceMethod]
sNode -->|是元类|yuanlei[resolveClassMethod]
yuanlei-->|通过objc_msgSend|msgSendYL[调用类中的resolveClassMethod方法]-->other
yuanlei-->|元类中没实现resolveClassMethod|fenleiclass[看继承链或分类是否处理resolveClassMethod]
fenleiclass -->|也没处理 看缓存中是否有|huancun[lookUpImpOrNilTryCache].->object-->category[缓存并调用具体实现的imp或者崩溃]
消息转发
如果系统在动态决议阶段没有找到实现,就会进入消息转发阶段。在前边分析resolveInstanceMethod
执行两次的时候,我们查看堆栈也看到,如果类中没有实现resolveInstanceMethod
方法,就会调用methodSignatureForSelector
方法,我们就看下消息转发流程。
日志打印
我们在看lookUpImpOrForward方法的时候,如果找到了方法,代码会跳转至done
位置,然后调用log_and_fill_cache
。
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int 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);
}
省略代码...
}
-------
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);
}
复制代码
log_and_fill_cache
方法中最主要的就是进行方法缓存cache.insert
,但这里还有一个logMessageSend
进行消息信息打印的过程,那么我们该如何获取这个打印的消息呢?
objcMsgLogEnabled
我们看到if
判断中有对打印控制的参数,说明可以通过设置这个参数来进行打印日志;全局查找,我们可以找到设置objcMsgLogEnabled
的函数,也就是通过instrumentObjcMessageSends
函数来设置是否打印日志。/tmp/msgSends-%d
通过查看logMessageSend
的源码如下图可以看到,日志输出的路径为/tmp/msgSends-XXX
,我们可以在此路径下找到这个日志打印文件。
我们调用
instrumentObjcMessageSends
函数前,需要先声明下该函数,如下:
extern void instrumentObjcMessageSends(BOOL flag);
复制代码
然后就可以通过以下方式打开该功能。
instrumentObjcMessageSends(YES);
FMEmployee *p = [[FMEmployee alloc] init];
[p test1];
instrumentObjcMessageSends(YES);
复制代码
如果有遇到lock 0x100803080 (runtimeLock) acquired before 0x100803000 (objcMsgLogLock) with no defined lock order
这个问题,导致日志内容不打印,可以把下方代码注释掉 运行后,就可以在
/tmp
目录下找到该文件:
可以看到这里在崩溃之前还调用了两个方法forwardingTargetForSelector
和methodSignatureForSelector
方法。消息发送在经过动态方法解析仍然没有查找到真正的方法实现,此时动态方法决议进入imp = forward_imp
消息转发流程。转发流程分两步快速转发
和慢速转发
。
消息的快速转发
我们在FMEmployee
类中声明test1
方法,不去实现,然后定义一个FMBoy
类在FMBoy
类中实现test1
,然后在FMEmployee
类中实现forwardingTargetForSelector:
方法,将FMEmployee
的test1
方法转发到FMBoy
类中,也就是说,快速转发后的类必须有同名的方法。如下代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
FMEmployee *p = [[FMEmployee alloc] init];
[p test1];
}
}
----
@implementation FMEmployee
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s",__func__);
if (aSelector == @selector(test1)) {
return [FMBoy new];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
----
@interface FMBoy : NSObject
- (void)test1;
@end
@implementation FMBoy
- (void)test1{
NSLog(@"%s",__func__);
}
@end
复制代码
然后运行 我们可以看到,虽然
FMEmployee
类没有定义test1
方法,但是通过转发,FMBoy
类的实例对象处理了该方法。
转发的作用在于,如果当前对象无法响应消息,就将它转发给能响应的对象。那么这时候方法缓存在哪?我们可以打印下:
我们发现
方法缓存在接收转发消息的对象的cache中
消息的慢速转发
在快速转发过程中,如果我们不做处理,此时就会进入到methodSignatureForSelector
方法, 也就是慢速转发。
@implementation FMEmployee
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%s, aSelector = %@",__func__, NSStringFromSelector(aSelector));
if (aSelector == @selector(test1)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"---%@---%@",anInvocation.target,NSStringFromSelector(anInvocation.selector));
}
@end
复制代码
这里 methodSignatureForSelector
函数要跟forwardInvocation
函数搭配使用,否则会报错。methodSignatureForSelector
函数的作用是方法有效性签名,在提供了一个方法的有效签名之后,系统会去调用forwardInvocation
方法来处理这个签名。
因此可以看到,
test1
函数虽然没有imp,但通过消息的转发,避免了崩溃。
消息转发流程图
graph TB
kaishi[对象接收到消息]-->fuleilian[首先从类 到 父类到继承链中查找方法]
fuleilian-->fineFunc[找到方法]-->cacheAndexecute[缓存并执行方法]
fuleilian-->nofineFunc[没有找到方法]
nofineFunc-->|进入动态方法决议流程|fangfajueyi[resolveInstanceMethod resolveClassMethod]
fangfajueyi-->addfangfajueyi[动态添加了方法]-->cacheAndexecute
fangfajueyi-->|没有动态添加方法|kuaisuzhuanfa[forwardingTargetForSelector快速转发]
kuaisuzhuanfa-->|转发消息|otherzhuanfa[其他对象执行方法]
kuaisuzhuanfa-->|不转发消息|manzhuanfa[methodSignatureForSelector慢速转发]
manzhuanfa-->|有效的选择器|forward[forwardInvocation]-->endNode[手动处理消息]
forward.->|不处理消息|throwerr[doesNotRecognizeSelector抛出异常]
manzhuanfa.->|没有方法签名 走forwarding|diaoyong[调用class_getInstanceMethod]-->ercijueyi[二次调用resolveInstanceMethod]
ercijueyi.->throwerr
从方法调用的角度看如何避免崩溃
方法决议阶段
如果一个函数没有实现imp
,那么可以通过NSObject
的分类,实现动态决议方法,来把没有具体实现imp
的崩溃问题统一到分类中去处理,如下图所示:
快速转发阶段
如果一个函数没有实现imp
,又没有做动态方法决议的相关处理,为了防止崩溃,也可以将这个消息转发给另一个对象去处理(另一个对象需有同名方法),如下图所示:
慢速转发阶段
那当一个函数没有实现imp
,又没有做动态方法决议的相关处理,也没有进行消息的转发,那还可以通过慢速转发来处理该函数调用,如下图所示:
因此,
动态决议、快速转发、慢速转发
合称为三个救命稻草
,用于防止方法查找导致的系统崩溃。