iOS 消息查找和消息轉(zhuǎn)發(fā)

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,還有forwardingTargetForSelectormethodSignatureForSelector,說明方法決議后府树,并沒有立即報錯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;
.
.
.

于是卓起,我們將targetselector打印出來:

(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就會浪費了离赫。

3.0 消息轉(zhuǎn)發(fā)流程圖

消息轉(zhuǎn)發(fā)機制.png
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末芭逝,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子渊胸,更是在濱河造成了極大的恐慌旬盯,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翎猛,死亡現(xiàn)場離奇詭異胖翰,居然都是意外死亡,警方通過查閱死者的電腦和手機切厘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門萨咳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人迂卢,你說我怎么就攤上這事某弦。” “怎么了而克?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵靶壮,是天一觀的道長。 經(jīng)常有香客問我员萍,道長腾降,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任碎绎,我火速辦了婚禮螃壤,結(jié)果婚禮上抗果,老公的妹妹穿的比我還像新娘。我一直安慰自己奸晴,他們只是感情好冤馏,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著寄啼,像睡著了一般逮光。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上墩划,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天涕刚,我揣著相機與錄音,去河邊找鬼乙帮。 笑死杜漠,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的察净。 我是一名探鬼主播驾茴,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼塞绿!你這毒婦竟也來了沟涨?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤异吻,失蹤者是張志新(化名)和其女友劉穎裹赴,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體诀浪,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡棋返,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了雷猪。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片睛竣。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖求摇,靈堂內(nèi)的尸體忽然破棺而出射沟,到底是詐尸還是另有隱情,我是刑警寧澤与境,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布验夯,位于F島的核電站,受9級特大地震影響摔刁,放射性物質(zhì)發(fā)生泄漏挥转。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望绑谣。 院中可真熱鬧党窜,春花似錦、人聲如沸借宵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽暇务。三九已至泼掠,卻和暖如春怔软,著一層夾襖步出監(jiān)牢的瞬間垦细,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工挡逼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留括改,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓家坎,卻偏偏與公主長得像嘱能,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子虱疏,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容