上篇文章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,又没有做动态方法决议的相关处理,也没有进行消息的转发,那还可以通过慢速转发来处理该函数调用,如下图所示:
因此,
动态决议、快速转发、慢速转发合称为三个救命稻草,用于防止方法查找导致的系统崩溃。