消息的发送
前篇
iOS看源码:消息发送01
iOS看源码:方法缓存
iOS看源码:方法慢速查找
消息发送的本质是objc_msgsend()
,会先从消息接受者的缓存中查找,缓存中找不到则按照isa
的指向依次按照由本类向父类直到根类NSObject
的方法列表中查找。
消息动态转发
lookUpImpOrForward()各种流程都没找到方法实现 那么就会返回一个系统默认的(IMP)_objc_msgForward_impcache
方法实现。
这就是让你看到*** unrecognized selector sent to instance ***
这条崩溃信息的函数。
但是 ,在程序崩溃之前,你还是有为这条没有找到IMP
的方法提供一个IMP
的机会。
当上面的消息查找流程没有找到IMP
时,会有一次时机执行方法的转发机制。
// No implementation found. Try method resolver once.
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
动态转发过程
通过对标志位behavior
的判断和操作,对没有进行过动态转发的消息进行一次转发机制resolveInstanceMethod ()
。
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(inst, sel, cls);
}
else {
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNil(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
当走到这一步的时候,runtime会先查询对象是否实现了resolveInstanceMethod()
这个方法,如果实现了这个方法就直接调用这个方法。
然后会再执行一次方法的查找流程lookUpImpOrNil()
。
这里为什么要再执行一次查找流程呢?
因为resolveInstanceMethod()
这个方法就是提供一个让你提供一个被查找方法的IMP
的机会。
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
IMP imp = lookUpImpOrNil(inst, sel, cls);
if (resolved && PrintResolving) {
//...
}
}
动态决议第一阶段
先调用resolveInstanceMethod
为对象提供一次解决方案,如果对象实现了个方法并且提供了所查找方法的IMP
则再次进行lookUpImpOrNil()
方法查找流程继续走消息发送正常流程。
用代码演示一下:
@interface MyPerson : NSObject
- (void)say;
@end
@implementation MyPerson
@end
int main(int argc, const char * argv[]) {
MyPerson *person = [[MyPerson alloc]init];
[person say];
return 0;
}
person
发送了一条没有实现过的say()
方法程序直接崩溃
接下来让Person
类实现了+ resolveInstanceMethod();
方法并且添加了一个- cry()
的方法实现,并且吧这个实现作为@selector(say)
的方法实现。
- (void)cry{NSLog(@"say-->cry");}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(say)) {
IMP sayCry = class_getMethodImplementation(self, @selector(cry));
Method method = class_getInstanceMethod(self, @selector(cry));
const char* type = method_getTypeEncoding(method);
return class_addMethod(self, sel, sayCry, type);
}
return [super resolveClassMethod:sel];
}
结果就是程序没有崩溃,原来的-say
方法最终调用的是-cry
方法的实现。
这一阶段的转发,需要注意类方法和实例方法 不过最后的本质是一样的,只是区别于类和元类,最后的根元类也最终指向了NSObject
类。
注意,如果你把这个转发过程放在了NSObject
的分类中去实现而不不加处理,那么你可能就覆盖了很多系统级别的消息转发。
+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;
第二阶段
如果第一个阶段,我们并没有去实现那怎么办?
再开始这一阶段讲解之前,先讲一个小技巧。
前面在讲解方法缓存的时候会遇到这一步代码
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
cache_fill(cls, sel, imp, receiver);
}
这里SUPPORT_MESSAGE_LOGGING
提示我们logMessageSend()
是一个方法日志,它可以吧方法的调用记录下来。
开启的方法演示一下:
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
MyPerson *person = [[MyPerson alloc]init];
instrumentObjcMessageSends(true);
[person say];
instrumentObjcMessageSends(false);
return 0;
}
这样我们就可以查看方法崩溃前,都调用过什么。
回到第一步的崩溃案例,在崩溃方法前开启日志打印,这样会在Mac的tmp
目录下记录调用过程。
我们看到 崩溃前,调用了几个我们并不常见到的方法
+ resolveInstanceMethod
- forwardingTargetForSelector
- methodSignatureForSelector
-
- doesNotRecognizeSelector
第一个我们知道了,就是动态转发的第一个步骤。
那么接下来的2、3、4是做什么的呢?
NSObject
的头文件供了这些方法。
- (void)doesNotRecognizeSelector:(SEL)aSelector;
- (id)forwardingTargetForSelector:(SEL)aSelector OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
- (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE("");
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE("");
具体怎么用去看看官方文档提供的信息。
动态决议二阶段-快速转发
- forwardingTargetForSelector:
这个方法叫我们找一个能够解决这个SEL
实现的对象。自己不能解决,或许可以交给别人去解决。
自己的类没有实现-say
这个放么,把问题甩给MyStudent
的实例对象去解决。相应的MyStudent
这个类如果实现了这个甩过来的方法,就会执行调用。
代码演示:
@interface MyStudent : NSObject
- (void)say;
@end
@implementation MyStudent
- (void)say{NSLog(@"student - say");}
@end
@implementation MyPerson
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(say)) {
return [[MyStudent alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
person把消息给了student并调用
第三阶段
第二阶段讲了,自己没有实现的消息可以转发给其他实现了这个消息的对象。那么万一没有人实现了这个消息怎么办?
如果第二阶段失败,那么就进入第三阶段 消息的慢速转发
动态决议第三阶段-消息的慢速转发
- methodSignatureForSelector:
方法签名的处理
消息到了这一步会获取到一个NSInvocation
对象,里面包装了方法名和一些参数,这时候你可以选择让合适的对象来处理它invoke
,当然也可以不调用。最少可以把程序崩溃在这里处理了。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == @selector(say)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
SEL aSelector = [anInvocation selector];
id obj = [MyStudent alloc];
if ([[MyStudent alloc] respondsToSelector:aSelector])
[anInvocation invokeWithTarget:[MyStudent alloc]];
else
[super forwardInvocation:anInvocation];
}
最后的崩溃
如果你没有实现第三步那么程序就真的要走崩溃流程了
- doesNotRecognizeSelector:
然后闪退。
网友评论