1. 消息慢速查找流程
1.1 forward_imp探索
@interface ZCPerson : NSObject
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, strong) NSString *name;
-(void)sayHello;
+(void)sayHappy;
@end
#import "ZCPerson.h"
@implementation ZCPerson
-(void)sayHello
{
NSLog(@"---%s",__func__);
}
+(void)sayHappy
{
NSLog(@"---%s",__func__);
}
@end
Class pClass = ZCPerson.class;
lgIMP_classToMetaclass(pClass);
IMP imp1 = class_getMethodImplementation(pClass, @selector(sayHello));
IMP imp2 = class_getMethodImplementation(metaClass, @selector(sayHello));
IMP imp3 = class_getMethodImplementation(pClass, @selector(sayHappy));
IMP imp4 = class_getMethodImplementation(metaClass, @selector(sayHappy));
NSLog(@"%p-%p-%p-%p",imp1,imp2,imp3,imp4);
输出:
2020-12-25 10:59:03.621174+0800 Objc[3615:37638] 0x100001be0-0x1002c3640-0x1002c3640-0x100001bb0
源码:
if (slowpath((curClass = curClass->superclass) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = forward_imp;
break;
}
当对象在调用方法时,会先去cls
里的cache
查找是否有缓存,如果查找不到会进入bit
内查找methodlist
,当在当前的类里查不到,会到父类中的cache
以及methodlist
中继续查找。在研究isa的过程中,有一张isa走位图,图上正好也有一条继承链,由图可知,当方法查找最终,会查找到nil
。而在源码中,当cls
等于nil
时,imp
会被赋值为forward_imp
。因此,也可知,当定义的方法没有实现时,imp的地址也不会为0x0
,而是forward_imp
的地址。
1.2 慢速查找方法lookUpImpOrForward
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();
// Optimistic cache lookup
if (fastpath(behavior & LOOKUP_CACHE)) {
imp = cache_getImp(cls, sel);
if (imp) goto done_nolock;
}
runtimeLock.lock();
checkIsKnownClass(cls);
if (slowpath(!cls->isRealized())) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
// runtimeLock may have been dropped but is now locked again
}
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);//查询是否实现了+(void)initialize方法
}
runtimeLock.assertLocked();
curClass = cls;
for (unsigned attempts = unreasonableClassCount();;) {
// curClass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp;
goto done;
}
if (slowpath((curClass = curClass->superclass) == 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); // 汇编方法,cache_getImp - lookup - lookUpImpOrForward
if (slowpath(imp == forward_imp)) {
// Found a forward:: entry in a superclass.
break;
}
if (fastpath(imp)) {
// Found the method in a superclass. Cache it in this class.
goto done;
}
}
// No implementation found. Try method resolver once.
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
log_and_fill_cache(cls, imp, sel, inst, curClass);
runtimeLock.unlock();
done_nolock:
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
由源码可知,在lookUpImpOrForward
方法中,还是先在缓存中查找了是否有imp
,因为在方法调用中,可能会受多线程的影响,可能在某个时候进行了方法缓存。然后经过checkIsKnownClass(cls);
方法判断当前cls
是否合法,然后会进入for循环
方法,查找当前类以及元类的一条继承链,看看是否有实现的方法。 当沿着继承链查找到父类为nil
时,则会退出循环,进行下一步方法决议流程。
查找方法源码:
static 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;
}
通过cls->data()->methods();
拿到方法列表,因为方法列表里有很多个方法,为了节省资源,苹果这里使用了二分算法去查找方法列表。注:在二分查找方法的过程中,会有一层分类重名方法判断。因为类的方法会先加入到内存中,然后才会加载分类方法。当查找到方法后,再调用cache_fill
方法将方法写入缓存中。
1.3 方法决议流程
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
在lookUpImpOrForward
方法中有一段如上代码,其中slowpath(behavior & LOOKUP_RESOLVER)
说明在此时有一个方法决议的控制条件,也就是说,if
里的判断条件只会走一次。然后进入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 (!lookUpImpOrNil(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
在resolveMethod_locked
最后,又调用了lookUpImpOrForward
方法,递归回去了,也就是说明,在第一次imp
没有处理后,苹果不会立即报错,而是给了一次处理imp
的机会,而处理的方法则是在resolveInstanceMethod
或者resolveClassMethod
中进行处理。我们注意到,在进行resolveClassMethod
处理中又加了一层resolveClassMethod
的处理,因为在元类中也有一条继承链,而根元类的父类是根类NSObject
,也就是说,NSObject
中也可能存在未实现的方法,因此需要多加一层判断。
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())) {
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
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 = lookUpImpOrNil(inst, sel, cls);
//下面是一些警告判断
.
.
.
}
在resolveClassMethod
中我们注意到最后又进行了lookUpImp
的处理,说明在这之前又对imp
做了处理。通过源码,我们可以发现对当前的cls
有一个objc_msgSend
的处理,发送的sel
为 @selector(resolveInstanceMethod:)
,也就是说,我们可以实现一个resolveInstanceMethod
作为中间层,处理下一层未实现的方法。
2. 消息转发
当消息方法决议未实现后,则会来到消息转发流程。
2.1 快速转发流程 - forwardingTargetForSelector
在lookUpImpOrForward
方法中,我们看会看到gotodone
会实现log_and_fill_cache
这样一个方法,点击进去进入logMessageSend
,我们会看到这个方法会打印出一些重要的信息。这里,向大家介绍一个方法instrumentObjcMessageSends(BOOL flag)
,因为在源码中,flag
默认为0,所以logMessageSend
是不打开日志的,所以我们需要使用instrumentObjcMessageSends
方法让flag
变为1,这样,就可以打开日志了。
#import <Foundation/Foundation.h>
#import "ZCPerson.h"
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
ZCPerson *person = [ZCPerson alloc];
instrumentObjcMessageSends(YES);
[person say666]; //方法只定义了,并没有实现
instrumentObjcMessageSends(NO);
}
return 0;
}
这里,我们借用extern
实现方法instrumentObjcMessageSends
,意思是,我们这个文件没有这个方法,让编译器去别的文件去找。当然,这是需要在源码环境中的。
我们打开Finder
,然后前往文件夹/tmp/msgSends/
,运行代码,发现当前文件夹多了一个msgSends-31644
的文件,打开发现,里面不仅有resolveInstanceMethod
,还有forwardingTargetForSelector
和methodSignatureForSelector
,说明方法决议后,并没有立即报错unrecognized selector
,而是又进行了两步操作。
在文件中,我们发现forwardingTargetForSelector
的实际调用者是ZCPerson
,也就是说,我们还有一次拯救的机会,就是在ZCPerson
中实现forwardingTargetForSelector
。
官方解释:forwardingTargetForSelector:
Returns the object to which unrecognized messages should first be directed.(当消息没有被识别时返回它的第一接受者。)
也就是说,当这个方法未被实现时,我们可以自己创建一个类实现方法作为接受者,在forwardingTargetForSelector
中用创建的类代替,在创建的累中实现方法。也可以使用runtime
对当前的sel
动态添加一个imp
。这也就是本篇文章介绍的快速转发流程。
2.2 慢速转发流程
我们在msgSends
文件中不仅发现会有forwardingTargetForSelector
方法,还有一个方法methodSignatureForSelector
,官方文档如下:
Returns an NSMethodSignature object that contains a description of the method identified by a given selector.
Discussion:
This method is used in the implementation of protocols. This method is also used in situations where an NSInvocation object must be created, such as during message forwarding. If your object maintains a delegate or is capable of handling messages that it does not directly implement, you should override this method to return an appropriate method signature.
在消息发送过程中,对那些没有进行慢速转发的消息还会进行一次处理,并且会返回一个方法签名NSMethodSignature
,在Discussion
解释中,还会搭配着一个方法的使用,也就是forwordInvocation
。
于是,我们可以在ZCPerson
中实现方法:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation
{
}
写完后再次运行会发现,代码没有崩溃了,我们进入NSInvocation
,发现其定义如下:
@interface NSInvocation : NSObject
+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;
@property (readonly, retain) NSMethodSignature *methodSignature;
- (void)retainArguments;
@property (readonly) BOOL argumentsRetained;
@property (nullable, assign) id target;
@property SEL selector;
.
.
.
于是,我们将target
和selector
打印出来:
(lldb) po anInvocation.target
<ZCPerson: 0x10070a350>
(lldb) po anInvocation.selector
"say666"
由此可知,这个时候系统介入了,将NSInvocation
这个事物流放了,类似漂流瓶一样。因此,我们在forwardInvocation
方法中既可以修改target
,也可以修改selector
。
-(void)forwardInvocation:(NSInvocation *)anInvocation
{
anInvocation.target = [[ZCTeacher alloc]init];
[anInvocation invoke];
}
你也可以不做任何处理,但是anInvocation
就会浪费了。
网友评论