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;
}
當對象在調(diào)用方法時奠宜,會先去cls
里的cache
查找是否有緩存辙芍,如果查找不到會進入bit
內(nèi)查找methodlist
,當在當前的類里查不到咆耿,會到父類中的cache
以及methodlist
中繼續(xù)查找德谅。在研究isa的過程中,有一張isa走位圖,圖上正好也有一條繼承鏈萨螺,由圖可知窄做,當方法查找最終,會查找到nil
慰技。而在源碼中椭盏,當cls
等于nil
時,imp
會被賦值為forward_imp
吻商。因此掏颊,也可知,當定義的方法沒有實現(xiàn)時艾帐,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);//查詢是否實現(xiàn)了+(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
捎稚,因為在方法調(diào)用中乐横,可能會受多線程的影響,可能在某個時候進行了方法緩存阳藻。然后經(jīng)過checkIsKnownClass(cls);
方法判斷當前cls
是否合法晰奖,然后會進入for循環(huán)
方法,查找當前類以及元類的一條繼承鏈腥泥,看看是否有實現(xiàn)的方法。 當沿著繼承鏈查找到父類為nil
時啃匿,則會退出循環(huán)蛔外,進行下一步方法決議流程蛆楞。
查找方法源碼:
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();
拿到方法列表,因為方法列表里有很多個方法夹厌,為了節(jié)省資源豹爹,蘋果這里使用了二分算法去查找方法列表。注:在二分查找方法的過程中矛纹,會有一層分類重名方法判斷臂聋。因為類的方法會先加入到內(nèi)存中,然后才會加載分類方法或南。當查找到方法后孩等,再調(diào)用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
最后权她,又調(diào)用了lookUpImpOrForward
方法,遞歸回去了逝薪,也就是說明隅要,在第一次imp
沒有處理后,蘋果不會立即報錯董济,而是給了一次處理imp
的機會拾徙,而處理的方法則是在resolveInstanceMethod
或者resolveClassMethod
中進行處理。我們注意到感局,在進行resolveClassMethod
處理中又加了一層resolveClassMethod
的處理尼啡,因為在元類中也有一條繼承鏈,而根元類的父類是根類NSObject
询微,也就是說崖瞭,NSObject
中也可能存在未實現(xiàn)的方法,因此需要多加一層判斷撑毛。
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
做了處理。通過源碼藻雌,我們可以發(fā)現(xiàn)對當前的cls
有一個objc_msgSend
的處理雌续,發(fā)送的sel
為 @selector(resolveInstanceMethod:)
,也就是說胯杭,我們可以實現(xiàn)一個resolveInstanceMethod
作為中間層驯杜,處理下一層未實現(xiàn)的方法。
2. 消息轉(zhuǎn)發(fā)
當消息方法決議未實現(xiàn)后做个,則會來到消息轉(zhuǎn)發(fā)流程鸽心。
2.1 快速轉(zhuǎn)發(fā)流程 - forwardingTargetForSelector
在lookUpImpOrForward
方法中滚局,我們看會看到gotodone
會實現(xiàn)log_and_fill_cache
這樣一個方法,點擊進去進入logMessageSend
顽频,我們會看到這個方法會打印出一些重要的信息藤肢。這里,向大家介紹一個方法instrumentObjcMessageSends(BOOL flag)
糯景,因為在源碼中嘁圈,flag
默認為0,所以logMessageSend
是不打開日志的蟀淮,所以我們需要使用instrumentObjcMessageSends
方法讓flag
變?yōu)?最住,這樣,就可以打開日志了。
#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]; //方法只定義了,并沒有實現(xiàn)
instrumentObjcMessageSends(NO);
}
return 0;
}
這里朋沮,我們借用extern
實現(xiàn)方法instrumentObjcMessageSends
次绘,意思是,我們這個文件沒有這個方法,讓編譯器去別的文件去找。當然,這是需要在源碼環(huán)境中的轧拄。
我們打開Finder
,然后前往文件夾/tmp/msgSends/
,運行代碼讽膏,發(fā)現(xiàn)當前文件夾多了一個msgSends-31644
的文件檩电,打開發(fā)現(xiàn),里面不僅有resolveInstanceMethod
,還有forwardingTargetForSelector
和methodSignatureForSelector
,說明方法決議后府树,并沒有立即報錯unrecognized selector
,而是又進行了兩步操作俐末。
在文件中,我們發(fā)現(xiàn)forwardingTargetForSelector
的實際調(diào)用者是ZCPerson
奄侠,也就是說卓箫,我們還有一次拯救的機會,就是在ZCPerson
中實現(xiàn)forwardingTargetForSelector
垄潮。
官方解釋:forwardingTargetForSelector:
Returns the object to which unrecognized messages should first be directed.(當消息沒有被識別時返回它的第一接受者烹卒。)
也就是說,當這個方法未被實現(xiàn)時弯洗,我們可以自己創(chuàng)建一個類實現(xiàn)方法作為接受者旅急,在forwardingTargetForSelector
中用創(chuàng)建的類代替,在創(chuàng)建的累中實現(xiàn)方法牡整。也可以使用runtime
對當前的sel
動態(tài)添加一個imp
藐吮。這也就是本篇文章介紹的快速轉(zhuǎn)發(fā)流程。
2.2 慢速轉(zhuǎn)發(fā)流程
我們在msgSends
文件中不僅發(fā)現(xiàn)會有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.
在消息發(fā)送過程中盟迟,對那些沒有進行慢速轉(zhuǎn)發(fā)的消息還會進行一次處理秋泳,并且會返回一個方法簽名NSMethodSignature
,在Discussion
解釋中潦闲,還會搭配著一個方法的使用,也就是forwordInvocation
。
于是迫皱,我們可以在ZCPerson
中實現(xiàn)方法:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation
{
}
寫完后再次運行會發(fā)現(xiàn)歉闰,代碼沒有崩潰了,我們進入NSInvocation
,發(fā)現(xiàn)其定義如下:
@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"
由此可知和敬,這個時候系統(tǒng)介入了,將NSInvocation
這個事物流放了戏阅,類似漂流瓶一樣昼弟。因此,我們在forwardInvocation
方法中既可以修改target
奕筐,也可以修改selector
舱痘。
-(void)forwardInvocation:(NSInvocation *)anInvocation
{
anInvocation.target = [[ZCTeacher alloc]init];
[anInvocation invoke];
}
你也可以不做任何處理,但是anInvocation
就會浪費了离赫。