Objective-C runtime機(jī)制(2)——消息機(jī)制

當(dāng)我們用中括號(hào)[]調(diào)用OC函數(shù)的時(shí)候,實(shí)際上會(huì)進(jìn)入消息發(fā)送和消息轉(zhuǎn)發(fā)流程:

消息發(fā)送(Messaging)年局,runtime系統(tǒng)會(huì)根據(jù)SEL查找對(duì)用的IMP级及,查找到拼卵,則調(diào)用函數(shù)指針進(jìn)行方法調(diào)用蓖议;若查找不到,則進(jìn)入動(dòng)態(tài)消息解析和轉(zhuǎn)發(fā)流程讥蟆,如果動(dòng)態(tài)解析和消息轉(zhuǎn)發(fā)失敗勒虾,則程序crash并記錄日志。

在進(jìn)入正題之前瘸彤,我們先思考兩個(gè)問(wèn)題:

  1. 類(lèi)實(shí)例可以調(diào)用類(lèi)方法嗎修然? 類(lèi)可以調(diào)用實(shí)例方法嗎? 為什么质况?

  2. 下面代碼輸出什么愕宋?

@interface Father : NSObject
@end

@implementation Father
@end

@interface Son : Father
- (void)showClass;
@end

@implementation Son
- (void)showClass {
    NSLog(@"self class = %@, super class = %@", [self class], [super class]);
}

...
Son *son = [Son new];
[son showClass];    // 這里輸出什么?
...

消息相關(guān)數(shù)據(jù)結(jié)構(gòu)


SEL

SEL被稱(chēng)之為消息選擇器结榄,它相當(dāng)于一個(gè)key中贝,在類(lèi)的消息列表中,可以根據(jù)這個(gè)key臼朗,來(lái)查找到對(duì)應(yīng)的消息實(shí)現(xiàn)邻寿。

在runtime中,SEL的定義是這樣的:

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

它是一個(gè)不透明的定義视哑,似乎蘋(píng)果故意隱藏了它的實(shí)現(xiàn)绣否。目前SEL僅是一個(gè)字符串。

<font color=red>這里要注意黎炉,即使消息的參數(shù)不同或方法所屬的類(lèi)也不同枝秤,但只要方法名相同,SEL也是一樣的慷嗜。所以淀弹,SEL單獨(dú)并不能作為唯一的Key,必須結(jié)合消息發(fā)送的目標(biāo)Class庆械,才能找到最終的IMP薇溃。</font>

我們可以通過(guò)OC編譯器命令@selector()或runtime函數(shù)sel_registerName,來(lái)獲取一個(gè)SEL類(lèi)型的方法選擇器缭乘。

method_t

當(dāng)需要發(fā)送消息的時(shí)候沐序,runtime會(huì)在Class的方法列表中尋找方法的實(shí)現(xiàn)。在方法列表中方法是以結(jié)構(gòu)體method_t存儲(chǔ)的堕绩。

struct method_t {
    SEL name;
    const char *types;
    IMP imp;

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};

可以看到method_t包含一個(gè)SEL作為key策幼,同時(shí)有一個(gè)指向函數(shù)實(shí)現(xiàn)的指針IMPmethod_t還包含一個(gè)屬性const char *types;
types是一個(gè)C字符串奴紧,用于表明方法的返回值和參數(shù)類(lèi)型特姐。一般是這種格式的:

v24@0:8@16

關(guān)于SEL type,可以參考Type Encodings

IMP

IMP實(shí)際是一個(gè)函數(shù)指針黍氮,用于實(shí)際的方法調(diào)用唐含。在runtime中定義是這樣的:

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif

IMP是由編譯器生成的浅浮,如果我們知道了IMP的地址,則可以繞過(guò)runtime消息發(fā)送的過(guò)程捷枯,直接調(diào)用函數(shù)實(shí)現(xiàn)滚秩。關(guān)于這一點(diǎn),我們稍后會(huì)談到淮捆。

在消息發(fā)送的過(guò)程中郁油,runtime就是根據(jù)idSEL來(lái)唯一確定IMP并調(diào)用之的。

消息


當(dāng)我們用[]向OC對(duì)象發(fā)送消息時(shí)攀痊,編譯器會(huì)對(duì)應(yīng)的代碼修改為objc_msgSend, 其定義如下:

OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

其實(shí)已艰,除了objc_msgSend,編譯器還會(huì)根據(jù)實(shí)際情況蚕苇,將消息發(fā)送改寫(xiě)為下面四個(gè)msgSend之一:
<font color=red>objc_msgSend
objc_msgSend_stret</font>

<font color=blue>objc_msgSendSuper
objc_msgSendSuper_stret</font>

當(dāng)我們將消息發(fā)送給super class的時(shí)候哩掺,編譯器會(huì)將消息發(fā)送改寫(xiě)為**SendSuper的格式,如調(diào)用[super viewDidLoad]涩笤,會(huì)被編譯器改寫(xiě)為objc_msgSendSuper的形式嚼吞。

objc_msgSendSuper的定義如下:

OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

可以看到,調(diào)用super方法時(shí)蹬碧,msgSendSuper的第一個(gè)參數(shù)不是id self舱禽,而是一個(gè)objc_super *objc_super定義如下:

struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;
    /// Specifies the particular superclass of the instance to message. 
    __unsafe_unretained _Nonnull Class super_class;

};

objc_super 包含兩個(gè)數(shù)據(jù)恩沽,receiver指調(diào)用super方法的對(duì)象誊稚,即子類(lèi)對(duì)象,而super_class表示子類(lèi)的Super Class罗心。

這就說(shuō)明了在消息過(guò)程中調(diào)用了 super方法和沒(méi)有調(diào)用super方法里伯,還是略有差異的。我們將會(huì)在下面講解渤闷。

至于**msgSend中以_stret結(jié)尾的疾瓮,表明方法返回值是一個(gè)結(jié)構(gòu)體類(lèi)型。

objc_msgSend 的內(nèi)部飒箭,會(huì)依次執(zhí)行:

  1. 檢測(cè)selector是否是應(yīng)該忽略的狼电,比如在Mac OS X開(kāi)發(fā)中,有了垃圾回收機(jī)制弦蹂,就不會(huì)響應(yīng)retain肩碟,release這些函數(shù)。
  2. 判斷當(dāng)前receiver是否為nil凸椿,若為nil削祈,則不做任何響應(yīng),即向nil發(fā)送消息削饵,系統(tǒng)不會(huì)crash岩瘦。
  3. 檢查Class的method cache,若cache未命中窿撬,則進(jìn)而查找Classmethod list启昧。
  4. 若在Classmethod list中未找到對(duì)應(yīng)的IMP,則進(jìn)行消息轉(zhuǎn)發(fā)
  5. 若消息轉(zhuǎn)發(fā)失敗劈伴,程序crash

objc_msgSend

objc_msgSend 的偽代碼實(shí)現(xiàn)如下:

id objc_msgSend(id self, SEL cmd, ...) {
    if(self == nil)
        return 0;
    Class cls = objc_getClass(self);
    IMP imp = class_getMethodImplementation(cls, cmd);
    return imp?imp(self, cmd, ...):0;
}

而在runtime源碼中密末,objc_msgSend方法其實(shí)是用匯編寫(xiě)的。為什么用匯編跛璧?一是因?yàn)?code>objc_msgSend的返回值類(lèi)型是可變的严里,需要用到匯編的特性;二是因?yàn)閰R編可以提高代碼的效率追城。

對(duì)應(yīng)arm64刹碾,其匯編源碼是這樣的(有所刪減):

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    MESSENGER_START

    cmp x0, #0          // nil check and tagged pointer check
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
    ldr x13, [x0]       // x13 = isa
    and x16, x13, #ISA_MASK // x16 = class  
LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

LNilOrTagged:
    b.eq    LReturnZero     // nil check
END_ENTRY _objc_msgSend

雖然不懂匯編,但是結(jié)合注釋?zhuān)€是能夠猜大體意思的座柱。

首先迷帜,系統(tǒng)通過(guò)cmp x0, #0檢測(cè)receiver是否為nil。如果為nil色洞,則進(jìn)入LNilOrTagged戏锹,返回0;

如果不為nil火诸,則現(xiàn)將receiverisa存入x13寄存器锦针;

x13寄存器中,取出isa中的class置蜀,放到x16寄存器中奈搜;

調(diào)用CacheLookup NORMAL,在這個(gè)函數(shù)中盯荤,首先查找class的cache媚污,如果未命中,則進(jìn)入objc_msgSend_uncached廷雅。

objc_msgSend_uncached 也是匯編耗美,實(shí)現(xiàn)如下:

STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band x16 is the class to search
    
    MethodTableLookup
    br  x17

    END_ENTRY __objc_msgSend_uncached

其內(nèi)部調(diào)用了MethodTableLookupMethodTableLookup是一個(gè)匯編的宏定義航缀,其內(nèi)部會(huì)調(diào)用C語(yǔ)言函數(shù)_class_lookupMethodAndLoadCache3

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

最終商架,會(huì)調(diào)用到lookUpImpOrForward來(lái)尋找classIMP實(shí)現(xiàn)或進(jìn)行消息轉(zhuǎn)發(fā)。

lookUpImpOrForward

lookUpImpOrForward方法的目的在于根據(jù)classSEL芥玉,在class或其super class中找到并返回對(duì)應(yīng)的實(shí)現(xiàn)IMP蛇摸,同時(shí),cache所找到的IMP到當(dāng)前class中灿巧。如果沒(méi)有找到對(duì)應(yīng)IMP赶袄,lookUpImpOrForward會(huì)進(jìn)入消息轉(zhuǎn)發(fā)流程揽涮。

lookUpImpOrForward 的簡(jiǎn)化版實(shí)現(xiàn)如下:

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // 先在class的cache中查找imp
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }
     
   runtimeLock.read();

    if (!cls->isRealized()) {
        runtimeLock.unlockRead();
        runtimeLock.write();
        // 如果class沒(méi)有被relize,先relize
        realizeClass(cls);

        runtimeLock.unlockWrite();
        runtimeLock.read();
    }

    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        // 如果class沒(méi)有init饿肺,則先init
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
    }

    
 retry:    
    runtimeLock.assertReading();

    // relaized并init了class蒋困,再試一把cache中是否有imp
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // 先在當(dāng)前class的method list中查找有無(wú)imp
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // 在當(dāng)前class中沒(méi)有找到imp,則依次向上查找super class的方法列表
    {
        unsigned attempts = unreasonableClassCount();
        // 進(jìn)入for循環(huán)敬辣,沿著繼承鏈雪标,依次向上查找super class的方法列表
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // 先找super class的cache
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // 在super class 的cache中找到imp,將imp存儲(chǔ)到當(dāng)前class(注意溉跃,不是super  class)的cache中 
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    // Found a forward:: entry in a superclass.
                    // Stop searching, but don't cache yet; call method 
                    // resolver for this class first.
                    break;
                }
            }
            
            // 在Super class的cache中沒(méi)有找到村刨,調(diào)用getMethodNoSuper_nolock在super class的方法列表中查找對(duì)應(yīng)的實(shí)現(xiàn)
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }

    // 在class和其所有的super class中均未找到imp,進(jìn)入動(dòng)態(tài)方法解析流程resolveMethod
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }

    // 如果在class撰茎,super classes和動(dòng)態(tài)方法解析 都不能找到這個(gè)imp嵌牺,則進(jìn)入消息轉(zhuǎn)發(fā)流程,嘗試讓別的class來(lái)響應(yīng)這個(gè)SEL
   
    // 消息轉(zhuǎn)發(fā)結(jié)束龄糊,cache結(jié)果到當(dāng)前class
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();

    return imp;
}

通過(guò)上的源碼髓梅,我們可以很清晰的知曉runtime的消息處理流程:

  1. 嘗試在當(dāng)前receiver對(duì)應(yīng)的class的cache中查找imp
  2. 嘗試在class的方法列表中查找imp
  3. 嘗試在class的所有super classes中查找imp(先看Super class的cache,再看super class的方法列表)
  4. 上面3步都沒(méi)有找到對(duì)應(yīng)的imp绎签,則嘗試動(dòng)態(tài)解析這個(gè)SEL
  5. 動(dòng)態(tài)解析失敗枯饿,嘗試進(jìn)行消息轉(zhuǎn)發(fā),讓別的class處理這個(gè)SEL

在查找class的方法列表中是否有SEL的對(duì)應(yīng)實(shí)現(xiàn)時(shí)诡必,是調(diào)用函數(shù)getMethodNoSuper_nolock

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    assert(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?

    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

方法實(shí)現(xiàn)很簡(jiǎn)單奢方,就是在class的方法列表methods中,根據(jù)SEL查找對(duì)應(yīng)的imp爸舒。

PS:這里順便說(shuō)一下Category覆蓋類(lèi)原始方法的問(wèn)題蟋字,由于在methods中是線(xiàn)性查找的,會(huì)返回第一個(gè)和SEL匹配的imp扭勉。而在classrealizeClass方法中,會(huì)調(diào)用methodizeClass來(lái)初始化class的方法列表涂炎。在methodizeClass方法中忠聚,會(huì)將Category方法和class方法合并到一個(gè)列表,同時(shí)唱捣,會(huì)確保Category方法位于class方法前面两蟀,這樣,在runtime尋找SEL的對(duì)應(yīng)實(shí)現(xiàn)時(shí)震缭,會(huì)先找到Category中定義的imp返回赂毯,從而實(shí)現(xiàn)了原始方法覆蓋的效果。 關(guān)于Category的底層實(shí)現(xiàn),我們會(huì)Objective-C runtime機(jī)制(4)——深入理解Category中講解党涕。

關(guān)于消息的查找烦感,可以用下圖更清晰的解釋?zhuān)?/p>

這里寫(xiě)圖片描述

runtime用isa找到receiver對(duì)應(yīng)的class,用superClass找到class的父類(lèi)膛堤。

這里用<font color=#3299cc>藍(lán)色的表示實(shí)例方法的消息查找流程:通過(guò)類(lèi)對(duì)象實(shí)例的isa查找到對(duì)象的class手趣,進(jìn)行查找。</font>

用<font color=#ff00ff>紫色表示類(lèi)方法的消息查找流程: 通過(guò)類(lèi)的isa找到類(lèi)對(duì)應(yīng)的<font color=#a67d3d>元類(lèi)</font>, 沿著元類(lèi)的super class鏈一路查找</font>

關(guān)于<font color=#a67d3d>元類(lèi)</font>骑祟,我們?cè)谏弦徽轮幸呀?jīng)提及,元類(lèi)是“類(lèi)的類(lèi)”气笙。因?yàn)樵趓untime中次企,類(lèi)也被看做是一種對(duì)象,而對(duì)象就一定有其所屬的類(lèi)潜圃,因此缸棵,類(lèi)所屬的類(lèi),被稱(chēng)為類(lèi)的<font color=#a67d3d>元類(lèi)(meta class)</font>谭期。

<font color=red>我們所定義的類(lèi)方法堵第,其實(shí)是存儲(chǔ)在元類(lèi)的方法列表中的。</font>

關(guān)于元類(lèi)的更多描述隧出,可以查看這里踏志。

類(lèi)調(diào)用實(shí)例方法

這里有一個(gè)很好玩的地方,注意到在SEL查找鏈的最上方:Root ClassRoot meta Class胀瞪。

我們上面說(shuō)到针余,對(duì)于類(lèi)方法,是沿著紫色的路線(xiàn)依次查找Super類(lèi)方法列表凄诞。一路上各個(gè)節(jié)點(diǎn)圆雁,都是<font color=#a67d3d>元類(lèi)(meta class)</font>。而注意到帆谍,Root meta Class的super class竟然是Root class! 也就是說(shuō)伪朽,當(dāng)在Root meta Class中找不到類(lèi)方法時(shí),會(huì)轉(zhuǎn)而到Root class中查找類(lèi)方法汛蝙。而在Root class中存儲(chǔ)的烈涮,其實(shí)都是<font color=blue>實(shí)例方法</font>。

換句話(huà)說(shuō)窖剑,我們?cè)谕ㄟ^(guò)類(lèi)方法的形式調(diào)用Root class中的實(shí)例方法跃脊,在OC中, 也是可以被解析的苛吱!

比如酪术,在NSObject中,有一個(gè)實(shí)例方法:

@interface NSObject <NSObject> {
...
- (IMP)methodForSelector:(SEL)aSelector;
...
}

然后,我們自定義類(lèi):

@interface MyObj : NSObject
- (void)showY;
@end

@implementation MyObj
- (void)showY {
    NSLog(@"AB");
}
@end

我們分別通過(guò)類(lèi)方法的形式調(diào)用methodForSelectorshowY

 [MyObj methodForSelector:@selector(test)];
 [MyObj showY];

會(huì)發(fā)現(xiàn)绘雁,編譯器允許methodForSelector的調(diào)用橡疼,并能夠正常允許。
而對(duì)于showY庐舟,則會(huì)編譯錯(cuò)誤欣除。

至于原因,就是因?yàn)閷?duì)于Root meta class挪略,其實(shí)會(huì)到Root class中尋找對(duì)應(yīng)的SEL實(shí)現(xiàn)历帚。

類(lèi)似的,還有一個(gè)好玩的例子杠娱,在子類(lèi)中挽牢,我們重寫(xiě)NSObjectrespondsToSelector方法,然后通過(guò)類(lèi)方法和實(shí)例方法兩種形式來(lái)調(diào)用摊求,看看分別會(huì)發(fā)生什么情況:

@interface NSObject <NSObject> {
...
- (BOOL)respondsToSelector:(SEL)aSelector;
...
}

@interface MyObj : NSObject
@end

@implementation MyObj
- (BOOL)respondsToSelector:(SEL)aSelector {
    NSLog(@"It is my overwrite");
    return YES;
}
@end

然后通過(guò)類(lèi)方法和實(shí)例方法分別調(diào)用

 [MyObj methodForSelector:@selector(test)];
 
 MyObj *obj = [[MyObj alloc] init];
 [obj showY];

這兩種調(diào)用方式會(huì)有什么不同禽拔?
這當(dāng)做一個(gè)思考題,如果大家理解了上面IMP的查找流程室叉,那么應(yīng)該能夠知道答案睹栖。

objc_msgSendSuper

看完了objc_msgSend方法的調(diào)用流程,我們?cè)賮?lái)看一下objc_msgSendSuper是如何調(diào)用的茧痕。
當(dāng)我們?cè)诖a里面顯示的調(diào)用super 方法時(shí)野来,runtime就會(huì)調(diào)用objc_msgSendSuper來(lái)完成消息發(fā)送。

objc_msgSendSuper的定義如下:

OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

當(dāng)我使用super關(guān)鍵字時(shí)踪旷,在這里梁只,super并不代表某個(gè)確定的對(duì)象,而是編譯器的一個(gè)符號(hào)埃脏,編譯器會(huì)將super替換為objc_super *類(lèi)型來(lái)傳入objc_msgSendSuper方法中搪锣。

objc_super結(jié)構(gòu)體定義如下:

struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;
    /// Specifies the particular superclass of the instance to message. 
    __unsafe_unretained _Nonnull Class super_class;

};

第一個(gè)成員id receiver,表明要將消息發(fā)送給誰(shuí)彩掐。它應(yīng)該是我們的類(lèi)實(shí)例(注意构舟,是當(dāng)前類(lèi)實(shí)例,而不是super)
第二個(gè)成員Class super_class堵幽,表明要到哪里去尋找SEL所對(duì)應(yīng)的IMP狗超。它應(yīng)該是我們類(lèi)實(shí)例所對(duì)應(yīng)類(lèi)的super class。(即要直接到super class中尋找IMP朴下,而略過(guò)當(dāng)前class的method list)

簡(jiǎn)單來(lái)說(shuō)努咐,當(dāng)調(diào)用super method時(shí),runtime會(huì)到super class中找到IMP殴胧,然后發(fā)送到當(dāng)前class的實(shí)例上渗稍。因此佩迟,雖然IMP的實(shí)現(xiàn)是用的super class,但是竿屹,最終作用對(duì)象报强,仍然是當(dāng)前class 的實(shí)例。這也就是為什么

NSLog(@"%@ %@",[self class],  [super class]);

會(huì)輸出同樣的內(nèi)容拱燃,即[self class]的內(nèi)容秉溉。

我們來(lái)看一下objc_msgSendSuper的匯編實(shí)現(xiàn):

    ENTRY _objc_msgSendSuper
    MESSENGER_START
    
    ldr r9, [r0, #CLASS]    // r9 = struct super->class
    CacheLookup NORMAL
    // cache hit, IMP in r12, eq already set for nonstret forwarding
    ldr r0, [r0, #RECEIVER] // load real receiver
    MESSENGER_END_FAST
    bx  r12         // call imp

    CacheLookup2 NORMAL
    // cache miss
    ldr r9, [r0, #CLASS]    // r9 = struct super->class
    ldr r0, [r0, #RECEIVER] // load real receiver
    MESSENGER_END_SLOW
    b   __objc_msgSend_uncached
    
    END_ENTRY _objc_msgSendSuper

可以看到,它就是在struct super->class的method list 中尋找對(duì)應(yīng)的IMP碗誉,而real receiver則是super->receiver召嘶,即當(dāng)前類(lèi)實(shí)例。

如果在super class的cache中沒(méi)有找到IMP的話(huà)哮缺,則同樣會(huì)調(diào)用__objc_msgSend_uncached弄跌,這和objc_msgSend是一樣的,最終都會(huì)調(diào)用到

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

只不過(guò)蝴蜓,這里傳入lookUpImpOrForward里面的cls碟绑,使用了super class而已俺猿。

動(dòng)態(tài)解析

如果在類(lèi)的繼承體系中茎匠,沒(méi)有找到相應(yīng)的IMP,runtime首先會(huì)進(jìn)行消息的動(dòng)態(tài)解析押袍。<font color=red>所謂動(dòng)態(tài)解析诵冒,就是給我們一個(gè)機(jī)會(huì),將方法實(shí)現(xiàn)在運(yùn)行時(shí)動(dòng)態(tài)的添加到當(dāng)前的類(lèi)中谊惭。</font>然后汽馋,runtime會(huì)重新嘗試走一遍消息查找的過(guò)程:

// 在class和其所有的super class中均未找到imp,進(jìn)入動(dòng)態(tài)方法解析流程resolveMethod
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }

在源碼中圈盔,可以看到豹芯,runtime會(huì)調(diào)用_class_resolveMethod,讓用戶(hù)進(jìn)行動(dòng)態(tài)方法解析驱敲,而且設(shè)置標(biāo)記triedResolver = YES铁蹈,僅執(zhí)行一次。當(dāng)動(dòng)態(tài)解析完畢众眨,<font color=blue>不管用戶(hù)是否作出了相應(yīng)處理</font>握牧,runtime,都會(huì)goto retry娩梨, 重新嘗試查找一遍類(lèi)的消息列表沿腰。

根據(jù)是調(diào)用的實(shí)例方法或類(lèi)方法,runtime會(huì)在對(duì)應(yīng)的類(lèi)中調(diào)用如下方法:

+ (BOOL)resolveInstanceMethod:(SEL)sel  // 動(dòng)態(tài)解析實(shí)例方法
+ (BOOL)resolveClassMethod:(SEL)sel     // 動(dòng)態(tài)解析類(lèi)方法

resolveInstanceMethod

+ (BOOL)resolveInstanceMethod:(SEL)sel用來(lái)動(dòng)態(tài)解析實(shí)例方法狈定,我們需要在運(yùn)行時(shí)動(dòng)態(tài)的將對(duì)應(yīng)的方法實(shí)現(xiàn)添加到類(lèi)實(shí)例所對(duì)應(yīng)的類(lèi)的消息列表中:

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(singSong)) {
        class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(unrecoginzedInstanceSelector)), "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

- (void)unrecoginzedInstanceSelector {
    NSLog(@"It is a unrecoginzed instance selector");
}

resolveClassMethod

+ (BOOL)resolveClassMethod:(SEL)sel用于動(dòng)態(tài)解析類(lèi)方法颂龙。 我們同樣需要將類(lèi)的實(shí)現(xiàn)動(dòng)態(tài)的添加到相應(yīng)類(lèi)的消息列表中。

<font color=blue>但這里需要注意,調(diào)用類(lèi)方法的‘對(duì)象’實(shí)際也是一個(gè)類(lèi)厘托,而類(lèi)所對(duì)應(yīng)的類(lèi)應(yīng)該是元類(lèi)友雳。要添加類(lèi)方法,我們必須把方法的實(shí)現(xiàn)添加到元類(lèi)的方法列表中铅匹。</font>

在這里押赊,我們就不能夠使用[self class]了,它僅能夠返回當(dāng)前的類(lèi)包斑。而是需要使用object_getClass(self)流礁,它其實(shí)會(huì)返回isa所指向的類(lèi),即類(lèi)所對(duì)應(yīng)的元類(lèi)(注意罗丰,因?yàn)楝F(xiàn)在是在類(lèi)方法里面神帅,self所指的是Class,而通過(guò)object_getClass(self)獲取self的類(lèi)萌抵,自然是元類(lèi))找御。

+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(payMoney)) {
        class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(unrecognizedClassSelector)), "v@:");
        return YES;
    }
    return [class_getSuperclass(self) resolveClassMethod:sel];
}

+ (void)unrecognizedClassSelector {
    NSLog(@"It is a unrecoginzed class selector");
}

這里主要弄清楚,類(lèi)绍填,元類(lèi)霎桅,實(shí)例方法類(lèi)方法在不同地方存儲(chǔ),就清楚了讨永。

關(guān)于class方法和object_getClass方法的區(qū)別:

當(dāng)self是實(shí)例對(duì)象時(shí)滔驶,[self class]object_getClass(self)等價(jià),因?yàn)榍罢邥?huì)調(diào)用后者卿闹,都會(huì)返回對(duì)象實(shí)例所對(duì)應(yīng)的類(lèi)揭糕。

當(dāng)self是類(lèi)對(duì)象時(shí),[self class]返回類(lèi)對(duì)象自身锻霎,而object_getClass(self)返回類(lèi)所對(duì)應(yīng)的元類(lèi)著角。

消息轉(zhuǎn)發(fā)

當(dāng)動(dòng)態(tài)解析失敗,則進(jìn)入消息轉(zhuǎn)發(fā)流程旋恼。所謂消息轉(zhuǎn)發(fā)吏口,是將當(dāng)前消息轉(zhuǎn)發(fā)到其它對(duì)象進(jìn)行處理。

- (id)forwardingTargetForSelector:(SEL)aSelector  // 轉(zhuǎn)發(fā)實(shí)例方法
+ (id)forwardingTargetForSelector:(SEL)aSelector  // 轉(zhuǎn)發(fā)類(lèi)方法蚌铜,id需要返回類(lèi)對(duì)象
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if(aSelector == @selector(mysteriousMethod:)){
        return alternateObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}
+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if(aSelector == @selector(xxx)) {
        return NSClassFromString(@"Class name");
    }
    return [super forwardingTargetForSelector:aSelector];
}

如果forwardingTargetForSelector沒(méi)有實(shí)現(xiàn)拘荡,或返回了nilself漾根,則會(huì)進(jìn)入另一個(gè)轉(zhuǎn)發(fā)流程递胧。
它會(huì)依次調(diào)用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector募寨,然后runtime會(huì)根據(jù)該方法返回的值,組成一個(gè)NSInvocation對(duì)象审葬,并調(diào)用- (void)forwardInvocation:(NSInvocation *)anInvocation 深滚。<font color='red'>注意奕谭,當(dāng)調(diào)用到forwardInvocation時(shí),無(wú)論我們是否實(shí)現(xiàn)了該方法痴荐,系統(tǒng)都默認(rèn)消息已經(jīng)得到解析血柳,不會(huì)引起crash。</font>

整個(gè)消息轉(zhuǎn)發(fā)流程可以用下圖表示:


這里寫(xiě)圖片描述

<font color=orange>注意生兆,和動(dòng)態(tài)解析不同难捌,由于消息轉(zhuǎn)發(fā)實(shí)際上是將消息轉(zhuǎn)發(fā)給另一種對(duì)象處理。而動(dòng)態(tài)解析仍是嘗試在當(dāng)前類(lèi)范圍內(nèi)進(jìn)行處理鸦难。</font>

消息轉(zhuǎn)發(fā) & 多繼承

通過(guò)消息轉(zhuǎn)發(fā)流程根吁,我們可以模擬實(shí)現(xiàn)OC的多繼承機(jī)制。詳情可以參考官方文檔合蔽。

這里寫(xiě)圖片描述

直接調(diào)用IMP

runtime的消息解析击敌,究其根本,實(shí)際上就是根據(jù)SEL查找到對(duì)應(yīng)的IMP拴事,并調(diào)用之沃斤。如果我們可以直接知道IMP的所在,就不用再走消息機(jī)制這一層了刃宵。似乎不走消息機(jī)制會(huì)提高一些方法調(diào)用的速度衡瓶,但現(xiàn)實(shí)是這樣的嗎?

我們比較一下:

CGFloat BNRTimeBlock (void (^block)(void)) {
    mach_timebase_info_data_t info;
    if (mach_timebase_info(&info) != KERN_SUCCESS) return -1.0;

    uint64_t start = mach_absolute_time ();
    block ();
    uint64_t end = mach_absolute_time ();
    uint64_t elapsed = end - start;

    uint64_t nanos = elapsed * info.numer / info.denom;
    return (CGFloat)nanos / NSEC_PER_SEC;

} // BNRTimeBlock

 Son *mySon1 = [Son new];
 setter ss = (void (*)(id, SEL, BOOL))[mySon1 methodForSelector:@selector(setFilled:)];
    CGFloat timeCost1 = BNRTimeBlock(^{
        for (int i = 0; i < 1000; ++i) {
            ss(mySon1, @selector(setFilled:), YES);
        }
    });
    
CGFloat timeCost2 = BNRTimeBlock(^{
        for (int i = 0; i < 1000; ++i) {
            [mySon1 setFilled:YES];
        }
    });

將timeCost1和timeCost2打印出來(lái)组去,你會(huì)發(fā)現(xiàn)鞍陨,僅僅相差0.000001秒步淹,幾乎可以忽略不計(jì)从隆。這樣是因?yàn)樵谙C(jī)制中,有緩存的存在缭裆。

總結(jié)

在本文中键闺,我們了解了OC語(yǔ)言中方法調(diào)用實(shí)現(xiàn)的底層機(jī)制——消息機(jī)制。并了解了self method和super method的異同澈驼。
最后辛燥,讓我們回答文章開(kāi)頭的兩個(gè)問(wèn)題:

  1. 類(lèi)實(shí)例可以調(diào)用類(lèi)方法嗎? 類(lèi)可以調(diào)用實(shí)例方法嗎缝其? 為什么挎塌?

類(lèi)實(shí)例不可用調(diào)用類(lèi)方法,因?yàn)轭?lèi)實(shí)例查找消息IMP的流程僅會(huì)沿著繼承鏈查找class 的method list内边,而對(duì)于類(lèi)方法來(lái)說(shuō)榴都,是存于meta class的method list 的,因此類(lèi)實(shí)例通過(guò)objc_msgSend方法是找不到對(duì)應(yīng)的實(shí)現(xiàn)的漠其。

類(lèi)大多數(shù)情況下是不能夠調(diào)用實(shí)例方法的嘴高,除非實(shí)例方法定義在root class——NSObject中竿音。因?yàn)椋?dāng)調(diào)用類(lèi)方法時(shí)拴驮,會(huì)在meta class的繼承鏈的method list 查找對(duì)應(yīng)的IMP春瞬,而root meta class對(duì)應(yīng)的super class是NSObject,因此在NSObject中定義的實(shí)例方法套啤,其實(shí)是可以通過(guò)類(lèi)方法形式來(lái)調(diào)用的宽气。

  1. 下面代碼輸出什么?
@interface Father : NSObject
@end

@implementation Father
@end

@interface Son : Father
- (void)showClass;
@end

@implementation Son
- (void)showClass {
    NSLog(@"self class = %@, super class = %@", [self class], [super class]);
}

...
Son *son = [Son new];
[son showClass];    // 這里輸出什么潜沦?
...

會(huì)輸出

self class = Son, super class = Son

至于原因抹竹,可以到本文關(guān)于objc_msgSendSuper相關(guān)講解中查看。

參考文獻(xiàn)

Objective-C Runtime
Objective-C 消息發(fā)送與轉(zhuǎn)發(fā)機(jī)制原理
object_getClass與objc_getClass的不同

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末止潮,一起剝皮案震驚了整個(gè)濱河市窃判,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌喇闸,老刑警劉巖袄琳,帶你破解...
    沈念sama閱讀 211,743評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異燃乍,居然都是意外死亡唆樊,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)刻蟹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)逗旁,“玉大人,你說(shuō)我怎么就攤上這事舆瘪∑В” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,285評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵英古,是天一觀的道長(zhǎng)淀衣。 經(jīng)常有香客問(wèn)我,道長(zhǎng)召调,這世上最難降的妖魔是什么膨桥? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,485評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮唠叛,結(jié)果婚禮上只嚣,老公的妹妹穿的比我還像新娘。我一直安慰自己艺沼,他們只是感情好册舞,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著澳厢,像睡著了一般环础。 火紅的嫁衣襯著肌膚如雪囚似。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,821評(píng)論 1 290
  • 那天线得,我揣著相機(jī)與錄音饶唤,去河邊找鬼。 笑死贯钩,一個(gè)胖子當(dāng)著我的面吹牛募狂,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播角雷,決...
    沈念sama閱讀 38,960評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼祸穷,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了勺三?” 一聲冷哼從身側(cè)響起雷滚,我...
    開(kāi)封第一講書(shū)人閱讀 37,719評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎吗坚,沒(méi)想到半個(gè)月后祈远,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,186評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡商源,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評(píng)論 2 327
  • 正文 我和宋清朗相戀三年车份,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片牡彻。...
    茶點(diǎn)故事閱讀 38,650評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡扫沼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出庄吼,到底是詐尸還是另有隱情缎除,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布霸褒,位于F島的核電站伴找,受9級(jí)特大地震影響盈蛮,放射性物質(zhì)發(fā)生泄漏废菱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評(píng)論 3 313
  • 文/蒙蒙 一抖誉、第九天 我趴在偏房一處隱蔽的房頂上張望殊轴。 院中可真熱鬧,春花似錦袒炉、人聲如沸旁理。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,757評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)孽文。三九已至驻襟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間芋哭,已是汗流浹背沉衣。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,991評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留减牺,地道東北人豌习。 一個(gè)月前我還...
    沈念sama閱讀 46,370評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像拔疚,于是被迫代替她去往敵國(guó)和親肥隆。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評(píng)論 2 349

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

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉稚失,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,690評(píng)論 0 9
  • 我們常常會(huì)聽(tīng)說(shuō) Objective-C 是一門(mén)動(dòng)態(tài)語(yǔ)言栋艳,那么這個(gè)「動(dòng)態(tài)」表現(xiàn)在哪呢?我想最主要的表現(xiàn)就是 Obje...
    Ethan_Struggle閱讀 2,182評(píng)論 0 7
  • 轉(zhuǎn)載:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麥子閱讀 729評(píng)論 0 2
  • 本文詳細(xì)整理了 Cocoa 的 Runtime 系統(tǒng)的知識(shí)句各,它使得 Objective-C 如虎添翼嘱巾,具備了靈活的...
    lylaut閱讀 795評(píng)論 0 4
  • 文中的實(shí)驗(yàn)代碼我放在了這個(gè)項(xiàng)目中。 以下內(nèi)容是我通過(guò)整理[這篇博客] (http://yulingtianxia....
    茗涙閱讀 914評(píng)論 0 6