探秘Runtime - Runtime消息發(fā)送機(jī)制

該文章屬于劉小壯原創(chuàng),轉(zhuǎn)載請注明:劉小壯


方法調(diào)用

OC中方法調(diào)用是通過Runtime實現(xiàn)的彼水,Runtime進(jìn)行方法調(diào)用本質(zhì)上是發(fā)送消息条霜,通過objc_msgSend()函數(shù)進(jìn)行消息發(fā)送间雀。

例如下面的OC代碼會被轉(zhuǎn)換為Runtime代碼悔详。

原方法:[object testMethod]
轉(zhuǎn)換后的調(diào)用:objc_msgSend(object, @selector(testMethod));

發(fā)送消息的第二個參數(shù)是一個SEL類型的參數(shù),在項目里經(jīng)常會出現(xiàn)惹挟,不同的類定義了相同的方法茄螃,這樣就會有相同的SEL。那么問題就來了连锯,也是很多人博客里都問過的一個問題归苍,不同類的SEL是同一個嗎?

然而运怖,事實是通過我們的驗證拼弃,創(chuàng)建兩個不同的類,并定義兩個相同的方法摇展,通過@selector()獲取SEL并打印吻氧。我們發(fā)現(xiàn)SEL都是同一個對象,地址都是相同的咏连。由此證明盯孙,不同類的相同SEL是同一個對象。

@interface TestObject : NSObject
- (void)testMethod;
@end

@interface TestObject2 : NSObject
- (void)testMethod;
@end

// TestObject2實現(xiàn)文件也一樣
@implementation TestObject
- (void)testMethod {
    NSLog(@"TestObject testMethod %p", @selector(testMethod));
}
@end

// 結(jié)果:
TestObject testMethod 0x100000f81
TestObject2 testMethod 0x100000f81

Runtime中維護(hù)了一個SEL的表祟滴,這個表存儲SEL不按照類來存儲振惰,只要相同的SEL就會被看做一個,并存儲到表中垄懂。在項目加載時骑晶,會將所有方法都加載到這個表中,而動態(tài)生成的方法也會被加載到表中草慧。

隱藏參數(shù)

我們在方法內(nèi)部可以通過self獲取到當(dāng)前對象桶蛔,但是self又是從哪來的呢?

方法實現(xiàn)的本質(zhì)也是C函數(shù)冠蒋,C函數(shù)除了方法傳入的參數(shù)外羽圃,還會有兩個默認(rèn)參數(shù),這兩個參數(shù)在通過objc_msgSend()調(diào)用時也會傳入抖剿。這兩個參數(shù)在Runtime中并沒有聲明朽寞,而是在編譯時自動生成的。

objc_msgSend的聲明中可以看出這兩個隱藏參數(shù)的存在斩郎。

objc_msgSend(void /* id self, SEL op, ... */ )
  • self脑融,調(diào)用當(dāng)前方法的對象。
  • _cmd缩宜,當(dāng)前被調(diào)用方法的SEL肘迎。

雖然這兩個參數(shù)在調(diào)用和實現(xiàn)方法中都沒有明確聲明甥温,但是我們?nèi)匀豢梢允褂盟m憫?yīng)對象就是self妓布,被調(diào)用方法的selector_cmd姻蚓。

- (void)method {
    id  target = getTheReceiver();
    SEL method = getTheMethod();
 
    if ( target == self || method == _cmd )
        return nil;
    return [target performSelector:method];
}

函數(shù)調(diào)用

一個對象被創(chuàng)建后,自身的類及其父類一直到NSObject類的部分匣沼,都會包含在對象的內(nèi)存中狰挡,例如其父類的實例變量。當(dāng)通過[super class]的方式調(diào)用其父類的方法時释涛,會創(chuàng)建一個結(jié)構(gòu)體加叁。

struct objc_super { id receiver; Class class; };

super的調(diào)用會被轉(zhuǎn)化為objc_msgSendSuper()的調(diào)用,并在其內(nèi)部調(diào)用objc_msgSend()函數(shù)唇撬。有一點需要注意它匕,盡管是通過[super class]的方式調(diào)用的,但傳入的receiver對象仍然是self窖认,返回結(jié)果也是selfclass豫柬。由此可知,當(dāng)前對象無論調(diào)用任何方法耀态,receiver都是當(dāng)前對象轮傍。

objc_msgSend(objc_super->receiver, @selector(class))

objc_msg.s中,存在多個版本的objc_msgSend函數(shù)首装。內(nèi)部實現(xiàn)邏輯大體一致,都是通過匯編實現(xiàn)的杭跪,只是根據(jù)不同的情況有不同的調(diào)用仙逻。

objc_msgSend
objc_msgSend_fpret
objc_msgSend_fp2ret
objc_msgSend_stret
objc_msgSendSuper
objc_msgSendSuper_stret
objc_msgSendSuper2
objc_msgSendSuper2_stret

在上面源碼中,帶有super的會在外界傳入一個objc_super的結(jié)構(gòu)體對象涧尿。stret表示返回的是struct類型系奉,super2objc_msgSendSuper()的一種實現(xiàn)方式,不對外暴露姑廉。

struct objc_super {
    id  receiver;
    Class   class;
};

fp則表示返回一個long double的浮點型缺亮,而fp2則返回一個complex long double的復(fù)雜浮點型,其他float桥言、double的普通浮點型都用objc_msgSend萌踱。除了上面這些情況外,其他都通過objc_msgSend()調(diào)用号阿。

消息發(fā)送流程

當(dāng)一個對象被創(chuàng)建時并鸵,系統(tǒng)會為其分配內(nèi)存,并完成默認(rèn)的初始化工作扔涧,例如對實例變量進(jìn)行初始化园担。對象第一個變量是指向其類對象的指針-isa届谈,isa指針可以訪問其類對象,并且通過其類對象擁有訪問其所有繼承者鏈中的類弯汰。

調(diào)用順序

isa指針不是語言的一部分艰山,主要為Runtime機(jī)制提供服務(wù)。

當(dāng)對象接收到一條消息時咏闪,消息函數(shù)隨著對象isa指針到類的結(jié)構(gòu)體中曙搬,在method list中查找方法selector。如果在本類中找不到對應(yīng)的selector汤踏,則objc_msgSend會向其父類的method list中查找selector织鲸,如果還不能找到則沿著繼承關(guān)系一直向上查找,直到找到NSObject類溪胶。

Runtimeselector查找的過程做了優(yōu)化搂擦,為類的結(jié)構(gòu)體中增加了cache字段,每個類都有獨立的cache哗脖,在一個selector被調(diào)用后就會加入到cache中瀑踢。在每次搜索方法列表之前,都會先檢查cache中有沒有才避,如果沒有才調(diào)用方法列表橱夭,這樣會提高方法的查找效率。

如果通過OC代碼的調(diào)用都會走消息發(fā)送的階段桑逝,如果不想要消息發(fā)送的過程棘劣,可以獲取到方法的函數(shù)指針直接調(diào)用。通過NSObjectmethodForSelector:方法可以獲取到函數(shù)指針楞遏,獲取到指針后需要對指針進(jìn)行類型轉(zhuǎn)換茬暇,轉(zhuǎn)換為和調(diào)用函數(shù)相符的函數(shù)指針,然后發(fā)起調(diào)用即可寡喝。

void (*setter)(id, SEL, BOOL);
int i;
 
setter = (void (*)(id, SEL, BOOL))[target
    methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
    setter(targetList[i], @selector(setFilled:), YES);

實現(xiàn)原理

Runtime中糙俗,objc_msgSend函數(shù)也是開源的,但其是通過匯編代碼實現(xiàn)的预鬓,arm64架構(gòu)代碼可以在objc-msg-arm64.s中找到巧骚。在Runtime中,很多執(zhí)行頻率比較高的函數(shù)格二,都是用匯編寫的劈彪。

objc_msgSend并不是完全開源的,在_class_lookupMethodAndLoadCache3函數(shù)中已經(jīng)獲取到Class參數(shù)了蟋定。所以在下面中有一個肯定是對象中獲取isa_t的過程粉臊,從方法命名和注釋來看,應(yīng)該是GetIsaFast匯編命令驶兜。如果這樣的話扼仲,就可以從消息發(fā)送到調(diào)用流程銜接起來了远寸。

ENTRY   _objc_msgSend
    MESSENGER_START

    NilTest NORMAL

    GetIsaFast NORMAL       // r11 = self->isa
    CacheLookup NORMAL      // calls IMP on success

    NilTestSupport  NORMAL

    GetIsaSupport      NORMAL

// cache miss: go search the method lists
LCacheMiss:
    // isa still in r11
    MethodTableLookup %a1, %a2  // r11 = IMP
    cmp %r11, %r11      // set eq (nonstret) for forwarding
    jmp *%r11           // goto *imp

    END_ENTRY   _objc_msgSend
  • MESSENGER_START:消息開始執(zhí)行。
  • NilTest:判斷接收消息的對象是否為nil屠凶,如果為nil則直接返回驰后,這就是對nil發(fā)送消息無效的原因。
  • GetIsaFast:快速獲取到isa指向的對象矗愧,是一個類對象或元類對象灶芝。
  • CacheLookup:從ache list中獲取緩存selector,如果查到則調(diào)用其對應(yīng)的IMP唉韭。
  • LCacheMiss:緩存沒有命中夜涕,則執(zhí)行此條匯編下面的方法。
  • MethodTableLookup:如果緩存中沒有找到属愤,則從method list中查找女器。

cache_t

如果每次進(jìn)行方法調(diào)用時,都按照對象模型來進(jìn)行方法列表的查找住诸,這樣是很消耗時間的驾胆。Runtime為了優(yōu)化調(diào)用時間,在objc_class中添加了一個cache_t類型的cache字段贱呐,通過緩存來優(yōu)化調(diào)用時間丧诺。

在執(zhí)行objc_msgSend函數(shù)的消息發(fā)送過程中,同一個方法第一次調(diào)用是沒有緩存的奄薇,但調(diào)用之后就會存在緩存驳阎,之后的調(diào)用就直接調(diào)用緩存。所以方法的調(diào)用馁蒂,可以分為有緩存和無緩存兩種搞隐,這兩種情況下的調(diào)用堆棧是不同的。

首先是從緩存中查找IMP远搪,但是由于cache3調(diào)用lookUpImpOrForward函數(shù)時,已經(jīng)查找過cache了逢捺,所以傳入的是NO谁鳍,不進(jìn)入查找cahce的代碼塊中。

struct cache_t {
    // 存儲被緩存方法的哈希表
    struct bucket_t *_buckets;
    // 占用的總大小
    mask_t _mask;
    // 已使用大小
    mask_t _occupied;
}

struct bucket_t {
    cache_key_t _key;
    IMP _imp;
};

當(dāng)給一個對象發(fā)送消息時劫瞳,Runtime會沿著isa找到對應(yīng)的類對象倘潜,但并不會立刻查找method_list,而是先查找cache_list志于,如果有緩存的話優(yōu)先查找緩存涮因,沒有再查找方法列表。

這是Runtime對查找method的優(yōu)化伺绽,理論上來說在cache中的method被訪問的頻率會更高养泡。cache_listcache_t定義嗜湃,內(nèi)部有一個bucket_t的數(shù)組,數(shù)組中保存IMPkey澜掩,通過key找到對應(yīng)的IMP并調(diào)用购披。具體源碼可以查看objc-cache.mm

如果類對象沒有被初始化肩榕,并且lookUpImpOrForward函數(shù)的initialize參數(shù)為YES刚陡,則表示需要對該類進(jìn)行創(chuàng)建。函數(shù)內(nèi)部主要是一些基礎(chǔ)的初始化操作株汉,而且會遞歸檢查父類筐乳,如果父類未初始化,則先初始化其父類對象乔妈。

    STATIC_ENTRY _cache_getImp

    mov r9, r0
    CacheLookup NORMAL
    // cache hit, IMP in r12
    mov r0, r12
    bx  lr          // return imp
    
    CacheLookup2 GETIMP
    // cache miss, return nil
    mov r0, #0
    bx  lr

    END_ENTRY _cache_getImp

下面會進(jìn)入cache_getImp的代碼中蝙云,然而這個函數(shù)不是開源的,但是有一部分源碼可以看到褒翰,是通過匯編寫的贮懈。其內(nèi)部調(diào)用了CacheLookupCacheLookup2兩個函數(shù),這兩個函數(shù)也都是匯編寫的优训。

經(jīng)過第一次調(diào)用后朵你,就會存在緩存。進(jìn)入objc_msgSend后會調(diào)用CacheLookup命令揣非,如果找到緩存則直接調(diào)用抡医。但是Runtime并不是完全開源的,內(nèi)部很多實現(xiàn)我們依然看不到早敬,CacheLookup命令內(nèi)部也一樣忌傻,只能看到調(diào)用完命令后就開始執(zhí)行我們的方法了。

CacheLookup NORMAL, CALL

源碼分析

在上面objc_msgSend匯編實現(xiàn)中搞监,存在一個MethodTableLookup的匯編調(diào)用水孩。在這條匯編調(diào)用中,調(diào)用了查找方法列表的C函數(shù)琐驴。下面是精簡版代碼俘种。

.macro MethodTableLookup
    
  // 調(diào)用MethodTableLookup并在內(nèi)部執(zhí)行cache3函數(shù)(C函數(shù))
    blx __class_lookupMethodAndLoadCache3
    mov r12, r0         // r12 = IMP

.endmacro

MethodTableLookup中通過調(diào)用_class_lookupMethodAndLoadCache3函數(shù),來查找方法列表绝淡。函數(shù)內(nèi)部是通過lookUpImpOrForward函數(shù)實現(xiàn)的宙刘,在調(diào)用時cache字段傳入NO,表示不需要查找緩存了牢酵,因為在cache3函數(shù)上面已經(jīng)通過匯編查找過了悬包。

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    // 通過cache3內(nèi)部調(diào)用lookUpImpOrForward函數(shù)
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

lookUpImpOrForward函數(shù)是支持多線程的,所以內(nèi)部會有很多鎖操作馍乙。其內(nèi)部有一個rwlock_t類型的runtimeLock變量布近,有runtimeLock控制讀寫鎖垫释。其內(nèi)部有很多邏輯代碼,這里把函數(shù)內(nèi)部實現(xiàn)做了精簡吊输,把核心代碼貼到下面饶号。

通過類對象的isRealized函數(shù),判斷當(dāng)前類是否被實現(xiàn)季蚂,如果沒有被實現(xiàn)茫船,則通過realizeClass函數(shù)實現(xiàn)該類。在realizeClass函數(shù)中扭屁,會設(shè)置version算谈、rwsuperClass等一些信息料滥。

// 執(zhí)行查找imp和轉(zhuǎn)發(fā)的代碼
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // 如果cache是YES然眼,則從緩存中查找IMP。如果是從cache3函數(shù)進(jìn)來葵腹,則不會執(zhí)行cache_getImp()函數(shù)
    if (cache) {
        // 通過cache_getImp函數(shù)查找IMP高每,查找到則返回IMP并結(jié)束調(diào)用
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    runtimeLock.read();

    // 判斷類是否已經(jīng)被創(chuàng)建,如果沒有被創(chuàng)建践宴,則將類實例化
    if (!cls->isRealized()) {
        // 對類進(jìn)行實例化操作
        realizeClass(cls);
    }

    // 第一次調(diào)用當(dāng)前類的話鲸匿,執(zhí)行initialize的代碼
    if (initialize  &&  !cls->isInitialized()) {
        // 對類進(jìn)行初始化,并開辟內(nèi)存空間
        _class_initialize (_class_getNonMetaClass(cls, inst));
    }
    
 retry:    
    runtimeLock.assertReading();

    // 嘗試獲取這個類的緩存
    imp = cache_getImp(cls, sel);
    if (imp) goto done;
    
    {
        // 如果沒有從cache中查找到阻肩,則從方法列表中獲取Method
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            // 如果獲取到對應(yīng)的Method带欢,則加入緩存并從Method獲取IMP
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    {
        unsigned attempts = unreasonableClassCount();
        // 循環(huán)獲取這個類的緩存IMP 或 方法列表的IMP
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // 獲取父類緩存的IMP
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // 如果發(fā)現(xiàn)父類的方法,并且不再緩存中烤惊,在下面的函數(shù)中緩存方法
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    break;
                }
            }
            
            // 在父類的方法列表中乔煞,獲取method_t對象。如果找到則緩存查找到的IMP
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }

    // 如果沒有找到柒室,則嘗試動態(tài)方法解析
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        triedResolver = YES;
        goto retry;
    }

    // 如果沒有IMP被發(fā)現(xiàn)渡贾,并且動態(tài)方法解析也沒有處理,則進(jìn)入消息轉(zhuǎn)發(fā)階段
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();

    return imp;
}

在方法第一次調(diào)用時雄右,可以通過cache_getImp函數(shù)查找到緩存的IMP剥啤。但如果是第一次調(diào)用,就查不到緩存的IMP不脯,就會進(jìn)入到getMethodNoSuper_nolock函數(shù)中執(zhí)行。下面是getMethod函數(shù)的關(guān)鍵代碼刻诊。

getMethodNoSuper_nolock(Class cls, SEL sel) {
    // 根據(jù)for循環(huán)防楷,從methodList列表中,從頭開始遍歷则涯,每次遍歷后向后移動一位地址复局。
    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
        // 對sel參數(shù)和method_t做匹配冲簿,如果匹配上則返回。
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

當(dāng)調(diào)用一個對象的方法時亿昏,查找對象的方法峦剔,本質(zhì)上就是遍歷對象isa所指向類的方法列表,并用調(diào)用方法的SEL和遍歷的method_t結(jié)構(gòu)體的name字段做對比角钩,如果相等則將IMP函數(shù)指針返回吝沫。

// 根據(jù)傳入的SEL,查找對應(yīng)的method_t結(jié)構(gòu)體
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
    
    if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        for (auto& meth : *mlist) {
            // SEL本質(zhì)上就是字符串递礼,查找的過程就是進(jìn)行字符串對比
            if (meth.name == sel) return &meth;
        }
    }

    if (mlist->isFixedUp()) {
        for (auto& meth : *mlist) {
            if (meth.name == sel) {
                _objc_fatal("linear search worked when binary search did not");
            }
        }
    }

    return nil;
}

getMethod函數(shù)中惨险,主要是對Classmethods方法列表進(jìn)行查找和匹配。類的方法列表都在Classclass_data_bits_t中脊髓,通過data()函數(shù)從bits中獲取到class_rw_t的結(jié)構(gòu)體辫愉,然后獲取到方法列表methods,并遍歷方法列表将硝。

如果從當(dāng)前類中獲取不到對應(yīng)的IMP恭朗,則進(jìn)入循環(huán)中。循環(huán)是從當(dāng)前類出發(fā)依疼,沿著繼承者鏈的關(guān)系痰腮,一直向根類查找,直到找到對應(yīng)的IMP實現(xiàn)涛贯。

查找步驟和上面也一樣诽嘉,先通過cache_getImp函數(shù)查找父類的緩存,如果找到則調(diào)用對應(yīng)的實現(xiàn)弟翘。如果沒找到緩存虫腋,表示第一次調(diào)用父類的方法,則調(diào)用getMethodNoSuper_nolock函數(shù)從方法列表中獲取實現(xiàn)稀余。

for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
{
    imp = cache_getImp(curClass, sel);
    if (imp) {
        if (imp != (IMP)_objc_msgForward_impcache) {
            log_and_fill_cache(cls, imp, sel, inst, curClass);
            goto done;
        }
    }
    
    Method meth = getMethodNoSuper_nolock(curClass, sel);
    if (meth) {
        log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
        imp = meth->imp;
        goto done;
    }
}

如果沒有找到方法實現(xiàn)悦冀,則會進(jìn)入動態(tài)方法決議的步驟。在if語句中會判斷傳入的resolver參數(shù)是否為YES睛琳,并且會判斷是否已經(jīng)有過動態(tài)決議盒蟆,因為下面是goto retry,所以這段代碼可能會執(zhí)行多次师骗。

if (resolver  &&  !triedResolver) {
    _class_resolveMethod(cls, sel, inst);
    triedResolver = YES;
    goto retry;
}

如果滿足條件并且是第一次進(jìn)行動態(tài)方法決議历等,則進(jìn)入if語句中調(diào)用_class_resolveMethod函數(shù)。動態(tài)方法決議有兩種辟癌,_class_resolveClassMethod類方法決議和_class_resolveInstanceMethod實例方法決議寒屯。

BOOL (*msg)(Class, SEL, SEL) = (__typeof__(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

在這兩個動態(tài)方法決議的函數(shù)實現(xiàn)中,本質(zhì)上都是通過objc_msgSend函數(shù),調(diào)用NSObject中定義的resolveInstanceMethod:resolveClassMethod:兩個方法寡夹。

可以在這兩個方法中動態(tài)添加方法处面,添加方法實現(xiàn)后,會在下面執(zhí)行goto retry菩掏,然后再次進(jìn)入方法查找的過程中魂角。從triedResolver參數(shù)可以看出,動態(tài)方法決議的機(jī)會只有一次智绸,如果這次再沒有找到野揪,則進(jìn)入消息轉(zhuǎn)發(fā)流程。

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

如果經(jīng)過上面這些步驟传于,還是沒有找到方法實現(xiàn)的話囱挑,則進(jìn)入動態(tài)消息轉(zhuǎn)發(fā)中。在動態(tài)消息轉(zhuǎn)發(fā)中沼溜,還可以對沒有實現(xiàn)的方法做一些彌補(bǔ)措施平挑。

下面是通過objc_msgSend函數(shù)發(fā)送一條消息后,所經(jīng)過的調(diào)用堆棧系草,調(diào)用順序是從上到下的通熄。

CacheLookup NORMAL, CALL
__objc_msgSend_uncached
MethodTableLookup NORMAL
_class_lookupMethodAndLoadCache3
lookUpImpOrForward

調(diào)用總結(jié)

在調(diào)用objc_msgSend函數(shù)后,會有一系列復(fù)雜的判斷邏輯找都,總結(jié)如下唇辨。

  1. 判斷當(dāng)前調(diào)用的SEL是否需要忽略,例如Mac OS中的垃圾處理機(jī)制啟動的話能耻,則忽略retain赏枚、release等方法,并返回一個_objc_ignored_methodIMP晓猛,用來標(biāo)記忽略饿幅。
  2. 判斷接收消息的對象是否為nil,因為在OC中對nil發(fā)消息是無效的戒职,這是因為在調(diào)用時就通過判斷條件過濾掉了栗恩。
  3. 從方法的緩存列表中查找,通過cache_getImp函數(shù)進(jìn)行查找洪燥,如果找到緩存則直接返回IMP磕秤。
  4. 查找當(dāng)前類的method list,查找是否有對應(yīng)的SEL捧韵,如果有則獲取到Method對象市咆,并從Method對象中獲取IMP,并返回IMP(這步查找結(jié)果是Method對象)再来。
  5. 如果在當(dāng)前類中沒有找到SEL床绪,則去父類中查找。首先查找cache list,如果緩存中沒有則查找method list癞己,并以此類推直到查找到NSObject為止。
  6. 如果在類的繼承體系中梭伐,始終沒有查找到對應(yīng)的SEL痹雅,則進(jìn)入動態(tài)方法解析中『叮可以在resolveInstanceMethodresolveClassMethod兩個方法中動態(tài)添加實現(xiàn)绩社。
  7. 動態(tài)消息解析如果沒有做出響應(yīng),則進(jìn)入動態(tài)消息轉(zhuǎn)發(fā)階段赂苗。此時可以在動態(tài)消息轉(zhuǎn)發(fā)階段做一些處理愉耙,否則就會Crash

整體分析

總體可以被分為三部分:

  1. 剛調(diào)用objc_msgSend函數(shù)后拌滋,內(nèi)部的一些處理邏輯朴沿。
  2. 復(fù)雜的查找IMP的過程,會涉及到cache listmethod list等败砂。
  3. 進(jìn)入消息轉(zhuǎn)發(fā)階段赌渣。

cache list中找不到方法的情況下,會通過MethodTableLookup宏定義從類的方法列表中昌犹,查找對應(yīng)的方法坚芜。在MethodTableLookup中本質(zhì)上也是調(diào)用_class_lookupMethodAndLoadCache3函數(shù),只是在傳參時cache字段傳NO斜姥,表示不從cache list中查找鸿竖。

cache3函數(shù)中,是直接調(diào)用的lookUpImpOrForward函數(shù)铸敏,這個函數(shù)內(nèi)部實現(xiàn)很復(fù)雜缚忧,可以看一下Runtime Analyze。在這個里面直接搜lookUpImpOrForward函數(shù)名即可搞坝,可以詳細(xì)看一下內(nèi)部實現(xiàn)邏輯搔谴。


簡書由于排版的問題,閱讀體驗并不好桩撮,布局敦第、圖片顯示、代碼等很多問題店量。所以建議到我Github上芜果,下載Runtime PDF合集。把所有Runtime文章總計九篇融师,都寫在這個PDF中右钾,而且左側(cè)有目錄,方便閱讀。

Runtime PDF

下載地址:Runtime PDF
麻煩各位大佬點個贊舀射,謝謝窘茁!??

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌棒妨,老刑警劉巖芳肌,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門框冀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人敏簿,你說我怎么就攤上這事明也。” “怎么了极谊?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵诡右,是天一觀的道長。 經(jīng)常有香客問我轻猖,道長帆吻,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任咙边,我火速辦了婚禮猜煮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘败许。我一直安慰自己王带,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布市殷。 她就那樣靜靜地躺著愕撰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪醋寝。 梳的紋絲不亂的頭發(fā)上搞挣,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天,我揣著相機(jī)與錄音音羞,去河邊找鬼囱桨。 笑死,一個胖子當(dāng)著我的面吹牛嗅绰,可吹牛的內(nèi)容都是我干的舍肠。 我是一名探鬼主播搀继,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼翠语!你這毒婦竟也來了叽躯?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤肌括,失蹤者是張志新(化名)和其女友劉穎险毁,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體们童,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年鲸鹦,在試婚紗的時候發(fā)現(xiàn)自己被綠了慧库。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡馋嗜,死狀恐怖齐板,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情葛菇,我是刑警寧澤甘磨,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站眯停,受9級特大地震影響济舆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜莺债,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一滋觉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧齐邦,春花似錦椎侠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至丐吓,卻和暖如春浅悉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背汰蜘。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工仇冯, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人族操。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓苛坚,卻偏偏與公主長得像比被,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子泼舱,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,976評論 2 355