objc源碼之Method消息發(fā)送

Objc源碼之對(duì)象創(chuàng)建alloc和init
Objc源碼之initialize實(shí)現(xiàn)
Objc源碼之Load方法實(shí)現(xiàn)
Objc源碼之NSObject和isa
Objc源碼之引用計(jì)數(shù)實(shí)現(xiàn)
objc源碼之Method消息發(fā)送

前言

?? 在我們進(jìn)行方法調(diào)用的時(shí)候,我們的對(duì)象是如何找到我們的方法呢恃逻?這個(gè)問題大家基本都知道是通過isa找到的溺蕉,實(shí)例對(duì)象通過isa找到類對(duì)象屿良,在類對(duì)象中查找方法朽砰,類對(duì)象通過isa指針找到元類,在元類對(duì)象中查找,那么在這個(gè)過程中究竟查找過程是怎么實(shí)現(xiàn)的,除了查找方法勾习,還會(huì)進(jìn)行哪些操作呢?這篇文章我們通過objc的源碼來看下具體的查找過程懈玻。

一巧婶、方法調(diào)用過程

TestObject *obj = [TestObject new];
[obj test];

我們以實(shí)例對(duì)象的方法調(diào)用為例,來說明一下方法的調(diào)用過程:
1.首先[obj test]會(huì)轉(zhuǎn)換成objc_msgSend(self,@ selector(test))函數(shù)調(diào)用。

2.obj通過isa指針找到類對(duì)象粹舵,實(shí)例對(duì)象的方法列表存在于類對(duì)象中钮孵。

3.類對(duì)象是一個(gè)objc_class結(jié)構(gòu)體,objc_class結(jié)構(gòu)中存在一個(gè)cache_t類型的cache眼滤,從cache里面的bucket_t中通過@ selector(test)為key來查找方法實(shí)現(xiàn)IMP巴席。

4.如果objc_class的cache中沒有查找到,就通過class_data_bits_t來獲取class_rw_t來獲取中的methods方法列表來查找test方法诅需。

5.如果類對(duì)象中沒有查找到對(duì)應(yīng)的方法漾唉,就通過objc_class結(jié)構(gòu)體中的superclass來找到對(duì)象的父類對(duì)象,然后重復(fù)3堰塌、4赵刑、5這個(gè)過程,如果還沒有查找到场刑,就會(huì)到到根類NSObject般此,NSObject的父對(duì)象是nil的(參考下面經(jīng)典的類關(guān)系圖),這個(gè)時(shí)候如果還沒有查找到牵现,就開始進(jìn)入消息轉(zhuǎn)發(fā)了铐懊。

類關(guān)系圖.png

6.進(jìn)入消息轉(zhuǎn)發(fā)階段以后。

  • 首先是調(diào)用resolveInstanceMethod:或者resolveClassMethod:瞎疼,這一步可以給當(dāng)前類添加方法科乎,來響應(yīng)這個(gè)過程。
  • 調(diào)用forwardingTargetForSelector:贼急,這一步是尋找一個(gè)備援接受者來響應(yīng)這個(gè)而方法茅茂。
  • 調(diào)用methodSignatureForSelector和forwardInvocation,完整的消息轉(zhuǎn)發(fā)太抓,通過NSInvocation來響應(yīng)這個(gè)方法空闲。


    消息轉(zhuǎn)發(fā)過程.png

7.如果上述過程都沒有響應(yīng),那么則會(huì)crash腻异,報(bào)unrecognized selector sent to instance的錯(cuò)誤进副。

二、objc_msgSend

?? 當(dāng)編譯器遇到一個(gè)方法調(diào)用時(shí)悔常,它會(huì)將方法的調(diào)用翻譯成以下函數(shù)中的一個(gè) objc_msgSend影斑、objc_msgSend_stret、objc_msgSendSuper 和 objc_msgSendSuper_stret机打。發(fā)送給對(duì)象的父類的消息會(huì)使用 objc_msgSendSuper 有數(shù)據(jù)結(jié)構(gòu)作為返回值的方法會(huì)使用 objc_msgSendSuper_stret 或 objc_msgSend_stret 其它的消息都是使用 objc_msgSend 發(fā)送的矫户。

?? 在objc_msgSend是OC實(shí)例對(duì)象和類對(duì)象發(fā)送消息的核心引擎,用來查找方法實(shí)現(xiàn)残邀,對(duì)性能要求較高皆辽,因此這一部分是通過匯編代碼來編寫的柑蛇。下面是歐陽(yáng)大哥通過匯編代碼,翻譯的c代碼深入解構(gòu)objc_msgSend函數(shù)的實(shí)現(xiàn)

//下面的結(jié)構(gòu)體中只列出objc_msgSend函數(shù)內(nèi)部訪問用到的那些數(shù)據(jù)結(jié)構(gòu)和成員驱闷。

/*
其實(shí)SEL類型就是一個(gè)字符串指針類型耻台,所描述的就是方法字符串指針
*/
typedef char * SEL;

/*
IMP類型就是所有OC方法的函數(shù)原型類型。
*/
typedef id (*IMP)(id self, SEL _cmd, ...); 


/*
  方法名和方法實(shí)現(xiàn)桶結(jié)構(gòu)體
*/
struct bucket_t  {
    SEL  key;       //方法名稱
    IMP imp;       //方法的實(shí)現(xiàn)空另,imp是一個(gè)函數(shù)指針類型
};

/*
   用于加快方法執(zhí)行的緩存結(jié)構(gòu)體盆耽。這個(gè)結(jié)構(gòu)體其實(shí)就是一個(gè)基于開地址沖突解決法的哈希桶。
*/
struct cache_t {
    struct bucket_t *buckets;    //緩存方法的哈希桶數(shù)組指針扼菠,桶的數(shù)量 = mask + 1
    int  mask;        //桶的數(shù)量 - 1
    int  occupied;   //桶中已經(jīng)緩存的方法數(shù)量摄杂。
};

/*
    OC對(duì)象的類結(jié)構(gòu)體描述表示,所有OC對(duì)象的第一個(gè)參數(shù)保存是的一個(gè)isa指針循榆。
*/
struct objc_object {
  void *isa;
};

/*
   OC類信息結(jié)構(gòu)體析恢,這里只展示出了必要的數(shù)據(jù)成員。
*/
struct objc_class : objc_object {
    struct objc_class * superclass;   //基類信息結(jié)構(gòu)體秧饮。
    cache_t cache;    //方法緩存哈希表
    //... 其他數(shù)據(jù)成員忽略映挂。
};



/*
objc_msgSend的C語(yǔ)言版本偽代碼實(shí)現(xiàn).
receiver: 是調(diào)用方法的對(duì)象
op: 是要調(diào)用的方法名稱字符串
*/
id  objc_msgSend(id receiver, SEL op, ...)
{

    //1............................ 對(duì)象空值判斷。
    //如果傳入的對(duì)象是nil則直接返回nil
    if (receiver == nil)
        return nil;
    
   //2............................ 獲取或者構(gòu)造對(duì)象的isa數(shù)據(jù)盗尸。
    void *isa = NULL;
    //如果對(duì)象的地址最高位為0則表明是普通的OC對(duì)象袖肥,否則就是Tagged Pointer類型的對(duì)象
    if ((receiver & 0x8000000000000000) == 0) {
        struct objc_object  *ocobj = (struct objc_object*) receiver;
        isa = ocobj->isa;
    }
    else { //Tagged Pointer類型的對(duì)象中沒有直接保存isa數(shù)據(jù),所以需要特殊處理來查找對(duì)應(yīng)的isa數(shù)據(jù)振劳。
        
        //如果對(duì)象地址的最高4位為0xF, 那么表示是一個(gè)用戶自定義擴(kuò)展的Tagged Pointer類型對(duì)象
        if (((NSUInteger) receiver) >= 0xf000000000000000) {
            
            //自定義擴(kuò)展的Tagged Pointer類型對(duì)象中的52-59位保存的是一個(gè)全局?jǐn)U展Tagged Pointer類數(shù)組的索引值。
            int  classidx = (receiver & 0xFF0000000000000) >> 52
            isa =  objc_debug_taggedpointer_ext_classes[classidx];
        }
        else {
            
            //系統(tǒng)自帶的Tagged Pointer類型對(duì)象中的60-63位保存的是一個(gè)全局Tagged Pointer類數(shù)組的索引值油狂。
            int classidx = ((NSUInteger) receiver) >> 60;
            isa  =  objc_debug_taggedpointer_classes[classidx];
        }
    }
    
   //因?yàn)閮?nèi)存地址對(duì)齊的原因和虛擬內(nèi)存空間的約束原因历恐,
   //以及isa定義的原因需要將isa與上0xffffffff8才能得到對(duì)象所屬的Class對(duì)象。
    struct objc_class  *cls = (struct objc_class *)(isa & 0xffffffff8);
    
   //3............................ 遍歷緩存哈希桶并查找緩存中的方法實(shí)現(xiàn)专筷。
    IMP  imp = NULL;
    //cmd與cache中的mask進(jìn)行與計(jì)算得到哈希桶中的索引弱贼,來查找方法是否已經(jīng)放入緩存cache哈希桶中。
    int index =  cls->cache.mask & op;
    while (true) {
        
        //如果緩存哈希桶中命中了對(duì)應(yīng)的方法實(shí)現(xiàn)磷蛹,則保存到imp中并退出循環(huán)吮旅。
        if (cls->cache.buckets[index].key == op) {
              imp = cls->cache.buckets[index].imp;
              break;
        }
        
        //方法實(shí)現(xiàn)并沒有被緩存,并且對(duì)應(yīng)的桶的數(shù)據(jù)是空的就退出循環(huán)
        if (cls->cache.buckets[index].key == NULL) {
             break;
        }
        
        //如果哈希桶中對(duì)應(yīng)的項(xiàng)已經(jīng)被占用但是又不是要執(zhí)行的方法味咳,則通過開地址法來繼續(xù)尋找緩存該方法的桶庇勃。
        if (index == 0) {
            index = cls->cache.mask;  //從尾部尋找
        }
        else {
            index--;   //索引減1繼續(xù)尋找。
        }
    } /*end while*/

   //4............................ 執(zhí)行方法實(shí)現(xiàn)或方法未命中緩存處理函數(shù)
    if (imp != NULL)
         return imp(receiver, op,  ...); //這里的... 是指?jìng)鬟f給objc_msgSend的OC方法中的參數(shù)槽驶。
    else
         return objc_msgSend_uncached(receiver, op, cls, ...);
}

/*
  方法未命中緩存處理函數(shù):objc_msgSend_uncached的C語(yǔ)言版本偽代碼實(shí)現(xiàn)责嚷,這個(gè)函數(shù)也是用匯編語(yǔ)言編寫。
*/
id objc_msgSend_uncached(id receiver, SEL op, struct objc_class *cls)
{
   //這個(gè)函數(shù)很簡(jiǎn)單就是直接調(diào)用了_class_lookupMethodAndLoadCache3 來查找方法并緩存到struct objc_class中的cache中掂铐,最后再返回IMP類型罕拂。
  IMP  imp =   _class_lookupMethodAndLoadCache3(receiver, op, cls);
  return imp(receiver, op, ....);
}

上面的代碼揍异,總結(jié)一下:
1.對(duì)象空值判斷,這個(gè)就是在OC中為什么給空對(duì)象發(fā)送消息爆班,不crash的原因衷掷。
2. 獲取或者構(gòu)造對(duì)象的isa數(shù)據(jù),通過isa查找類或者元類
3. 遍歷緩存哈希桶并查找緩存中的方法實(shí)現(xiàn)柿菩,通過cache查找是否命中緩存
4. 執(zhí)行方法實(shí)現(xiàn)或方法未命中緩存處理函數(shù)objc_msgSend_uncached

未命中緩存

三戚嗅、lookUpImpOrForward

lookUpImpOrForward是方法調(diào)用過程的核心類,方法的查找碗旅、類的初始化渡处、initialize都可能在這里面調(diào)用。

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

    runtimeLock.assertUnlocked();

    //1. 緩存查找
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    runtimeLock.lock();
    checkIsKnownClass(cls);
     //2. 類是否實(shí)現(xiàn)
    if (!cls->isRealized()) {
        realizeClass(cls);
    }
     //3. 類是否初始化
    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();
    }

    
 retry:    
    runtimeLock.assertLocked();

   
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // 4.方法列表查找,查找到以后暑劝,進(jìn)行緩存竭贩。
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // 5.父類方法列表查找,查找到進(jìn)行緩存醇份。
    {
        unsigned attempts = unreasonableClassCount();
        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.");
            }
            
            // Superclass cache.
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // Found the method in a superclass. Cache it in this class.
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    break;
                }
            }
            
            // Superclass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }

    // 6.如果還沒有查找到。進(jìn)入消息轉(zhuǎn)發(fā)resolveMethod方法

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        // 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;
    }

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlock();

    return imp;
}

lookUpImpOrForward方法有如下過程:
1. 緩存中查找方法
2. 類是否實(shí)現(xiàn)
3.類是否初始化
4.方法列表查找吼具,查找到以后僚纷,進(jìn)行緩存。
5.父類方法列表查找拗盒,查找到進(jìn)行緩存怖竭。
6.如果還沒有查找到。進(jìn)入消息轉(zhuǎn)發(fā)resolveMethod方法
這里的方法查找過程陡蝇,我在第一部分的方法調(diào)用過程中都有描述過痊臭,我重點(diǎn)說一下2和3,這兩部分是做什么登夫。

  • 類是否實(shí)現(xiàn)广匙,這一部分主要是判斷類是否是第一次調(diào)用,第一次調(diào)用的時(shí)候恼策,class_rw_t可能還沒有創(chuàng)建好鸦致,因?yàn)榉椒ㄊ谴嬖谶@里面的,所以要保證類已經(jīng)實(shí)現(xiàn)涣楷。
  • 類是否初始化分唾,這一部分主要是初始化類的一些參數(shù),包括isa指針狮斗,同時(shí)我們熟悉的Initialize方法也是在這里調(diào)用的鳍寂。

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

消息轉(zhuǎn)發(fā)是在運(yùn)行時(shí)進(jìn)行的情龄,大致分為三個(gè)階段:
第一階段是先檢查接收者迄汛,看是否能通過runtime動(dòng)態(tài)添加一個(gè)方法捍壤,來處理這個(gè)方法;
第二階段就是備援接收者鞍爱,看看有沒有對(duì)象可以響應(yīng)這個(gè)方法鹃觉。
第二階段就是把該消息的全部信息封裝到NSInvocation對(duì)象中,看哪個(gè)對(duì)象能否處理睹逃,如果還無法處理盗扇,則報(bào)錯(cuò)unrecognized selector sent to instance。

1.動(dòng)態(tài)方法解析

// 類方法專用
+ (BOOL)resolveClassMethod:(SEL)sel
// 對(duì)象方法專用
+ (BOOL)resolveInstanceMethod:(SEL)sel

2.備援接收者

- (id)forwardingTargetForSelector:(SEL)aSelector

3.完整消息轉(zhuǎn)發(fā)

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- (void)forwardInvocation:(NSInvocation *)anInvocation

在方法簽名的過程中沉填,注意簽名符號(hào):

*          代表  char * 
char BOOL  代表  c
:          代表  SEL 
^type      代表  type *
@          代表  NSObject * 或 id
^@         代表  NSError ** 
#          代表  NSObject 
v          代表  void
消息轉(zhuǎn)發(fā)過程.png

五疗隶、總結(jié)

方法的調(diào)用過程:
1.緩存查找
2.查找當(dāng)前類的緩存及方法。
3.查找父類的緩存及方法
4.消息轉(zhuǎn)發(fā)

參考:
objc4-750源碼
從源代碼看 ObjC 中消息的發(fā)送.md
深入解構(gòu)objc_msgSend函數(shù)的實(shí)現(xiàn)
iOS消息轉(zhuǎn)發(fā)機(jī)制實(shí)例
iOS的消息轉(zhuǎn)發(fā)機(jī)制詳解

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末翼闹,一起剝皮案震驚了整個(gè)濱河市斑鼻,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌猎荠,老刑警劉巖坚弱,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異关摇,居然都是意外死亡荒叶,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門输虱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來些楣,“玉大人,你說我怎么就攤上這事宪睹「甓荆” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵横堡,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我冠桃,道長(zhǎng)命贴,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任食听,我火速辦了婚禮胸蛛,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘樱报。我一直安慰自己葬项,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布迹蛤。 她就那樣靜靜地躺著民珍,像睡著了一般襟士。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上嚷量,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天陋桂,我揣著相機(jī)與錄音,去河邊找鬼蝶溶。 笑死嗜历,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的抖所。 我是一名探鬼主播梨州,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼田轧!你這毒婦竟也來了暴匠?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤涯鲁,失蹤者是張志新(化名)和其女友劉穎巷查,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體抹腿,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡岛请,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了警绩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片崇败。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖肩祥,靈堂內(nèi)的尸體忽然破棺而出后室,到底是詐尸還是另有隱情,我是刑警寧澤混狠,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布岸霹,位于F島的核電站,受9級(jí)特大地震影響将饺,放射性物質(zhì)發(fā)生泄漏贡避。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一予弧、第九天 我趴在偏房一處隱蔽的房頂上張望刮吧。 院中可真熱鬧,春花似錦掖蛤、人聲如沸杀捻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)致讥。三九已至仅仆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間拄踪,已是汗流浹背蝇恶。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留惶桐,地道東北人撮弧。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像姚糊,于是被迫代替她去往敵國(guó)和親贿衍。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

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

  • 關(guān)于OC中的消息發(fā)送的實(shí)現(xiàn)救恨,在去年也看過一次贸辈,當(dāng)時(shí)有點(diǎn)不太理解,但是今年再看卻很容易理解肠槽。 我想這跟知識(shí)體系的構(gòu)建...
    咖啡綠茶1991閱讀 934評(píng)論 0 1
  • 消息發(fā)送和轉(zhuǎn)發(fā)流程可以概括為:消息發(fā)送(Messaging)是 Runtime 通過 selector 快速查找 ...
    lylaut閱讀 1,821評(píng)論 2 3
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,089評(píng)論 1 32
  • 2019年1月3日擎淤,星期四,陰秸仙。 昨天忙活一天嘴拢,晚上喝多了點(diǎn)。 雇主是上次帝都那小伙寂纪。事不繁重席吴,安裝兩...
    距離負(fù)人閱讀 179評(píng)論 0 5
  • 那天我做了一個(gè)夢(mèng)。 那個(gè)小小的家里捞蛋,爺爺還坐在那張他專屬的板凳上孝冒,奶奶像平時(shí)一樣笨拙的忙進(jìn)忙出。 我似乎是一個(gè)局外...
    相佯閱讀 188評(píng)論 0 0