神經(jīng)病院Objective-C Runtime住院第二天——消息發(fā)送與轉(zhuǎn)發(fā)

前言

現(xiàn)在越來(lái)越多的app都使用了JSPatch實(shí)現(xiàn)app熱修復(fù)魔市,而JSPatch 能做到通過(guò) JS 調(diào)用和改寫(xiě) OC 方法最根本的原因是 Objective-C 是動(dòng)態(tài)語(yǔ)言主届,OC 上所有方法的調(diào)用/類的生成都通過(guò) Objective-C Runtime 在運(yùn)行時(shí)進(jìn)行,我們可以通過(guò)類名/方法名反射得到相應(yīng)的類和方法待德,也可以替換某個(gè)類的方法為新的實(shí)現(xiàn)君丁,理論上你可以在運(yùn)行時(shí)通過(guò)類名/方法名調(diào)用到任何 OC 方法,替換任何類的實(shí)現(xiàn)以及新增任意類将宪。今天就來(lái)詳細(xì)解析一下OC中runtime最為吸引人的地方绘闷。

目錄

  • 1.objc_msgSend函數(shù)簡(jiǎn)介
  • 2.消息發(fā)送Messaging階段—objc_msgSend源碼解析
  • 3.消息轉(zhuǎn)發(fā)Message Forwarding階段
  • 4.forwardInvocation的例子
  • 5.入院考試
  • 6.Runtime中的優(yōu)化

一.objc_msgSend函數(shù)簡(jiǎn)介

最初接觸到OC Runtime,一定是從[receiver message]這里開(kāi)始的较坛。[receiver message]會(huì)被編譯器轉(zhuǎn)化為:


id objc_msgSend ( id self, SEL op, ... );

這是一個(gè)可變參數(shù)函數(shù)印蔗。第二個(gè)參數(shù)類型是SEL。SEL在OC中是selector方法選擇器丑勤。


typedef struct objc_selector *SEL;

objc_selector是一個(gè)映射到方法的C字符串华嘹。需要注意的是@selector()選擇子只與函數(shù)名有關(guān)。不同類中相同名字的方法所對(duì)應(yīng)的方法選擇器是相同的法竞,即使方法名字相同而變量類型不同也會(huì)導(dǎo)致它們具有相同的方法選擇器耙厚。由于這點(diǎn)特性,也導(dǎo)致了OC不支持函數(shù)重載岔霸。

在receiver拿到對(duì)應(yīng)的selector之后薛躬,如果自己無(wú)法執(zhí)行這個(gè)方法,那么該條消息要被轉(zhuǎn)發(fā)秉剑》汉溃或者臨時(shí)動(dòng)態(tài)的添加方法實(shí)現(xiàn)。如果轉(zhuǎn)發(fā)到最后依舊沒(méi)法處理侦鹏,程序就會(huì)崩潰诡曙。

所以編譯期僅僅是確定了要發(fā)送消息,而消息如何處理是要運(yùn)行期需要解決的事情略水。

objc_msgSend函數(shù)究竟會(huì)干什么事情呢价卤?從這篇「objc_msgSend() Tour」文章里面可以得到一個(gè)比較詳細(xì)的結(jié)論。

  1. Check for ignored selectors (GC) and short-circuit.
  2. Check for nil target.
    If nil & nil receiver handler configured, jump to handler
    If nil & no handler (default), cleanup and return.
  3. Search the class’s method cache for the method IMP(use hash to find&store method in cache)
    -1. If found, jump to it.
    -2. Not found: lookup the method IMP in the class itself corresponding its hierarchy chain.
    If found, load it into cache and jump to it.
    If not found, jump to forwarding mechanism.

總結(jié)一下objc_msgSend會(huì)做一下幾件事情:
1.檢測(cè)這個(gè) selector是不是要忽略的渊涝。
2.檢查target是不是為nil慎璧。

如果這里有相應(yīng)的nil的處理函數(shù)床嫌,就跳轉(zhuǎn)到相應(yīng)的函數(shù)中。
如果沒(méi)有處理nil的函數(shù)胸私,就自動(dòng)清理現(xiàn)場(chǎng)并返回厌处。這一點(diǎn)就是為何在OC中給nil發(fā)送消息不會(huì)崩潰的原因。

3.確定不是給nil發(fā)消息之后岁疼,在該class的緩存中查找方法對(duì)應(yīng)的IMP實(shí)現(xiàn)阔涉。

如果找到,就跳轉(zhuǎn)進(jìn)去執(zhí)行捷绒。
如果沒(méi)有找到瑰排,就在方法分發(fā)表里面繼續(xù)查找,一直找到NSObject為止暖侨。

4.如果還沒(méi)有找到椭住,那就需要開(kāi)始消息轉(zhuǎn)發(fā)階段了。至此字逗,發(fā)送消息Messaging階段完成京郑。這一階段主要完成的是通過(guò)select()快速查找IMP的過(guò)程。

二. 消息發(fā)送Messaging階段—objc_msgSend源碼解析

在這篇文章**Obj-C Optimization: **The faster objc_msgSend中看到了這樣一段C版本的objc_msgSend的源碼扳肛。

#include <objc/objc-runtime.h>

id  c_objc_msgSend( struct objc_class /* ahem */ *self, SEL _cmd, ...)
{
   struct objc_class    *cls;
   struct objc_cache    *cache;
   unsigned int         hash;
   struct objc_method   *method;   
   unsigned int         index;
   
   if( self)
   {
      cls   = self->isa;
      cache = cls->cache;
      hash  = cache->mask;
      index = (unsigned int) _cmd & hash;
      
      do
      {
         method = cache->buckets[ index];
         if( ! method)
            goto recache;
         index = (index + 1) & cache->mask;
      }
      while( method->method_name != _cmd);
      return( (*method->method_imp)( (id) self, _cmd));
   }
   return( (id) self);

recache:
   /* ... */
   return( 0);
}

該源碼中有一個(gè)do-while循環(huán)傻挂,這個(gè)循環(huán)就是上一章里面提到的在方法分發(fā)表里面查找method的過(guò)程。

不過(guò)在obj4-680里面的objc-msg-x86_64.s文件中實(shí)現(xiàn)是一段匯編代碼挖息。



/********************************************************************
 *
 * id objc_msgSend(id self, SEL _cmd,...);
 *
 ********************************************************************/
 
 .data
 .align 3
 .globl _objc_debug_taggedpointer_classes
_objc_debug_taggedpointer_classes:
 .fill 16, 8, 0

 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

 
 ENTRY _objc_msgSend_fixup
 int3
 END_ENTRY _objc_msgSend_fixup

 
 STATIC_ENTRY _objc_msgSend_fixedup
 // Load _cmd from the message_ref
 movq 8(%a2), %a2
 jmp _objc_msgSend
 END_ENTRY _objc_msgSend_fixedup

來(lái)分析一下這段匯編代碼金拒。

乍一看,如果從LCacheMiss:這里上下分開(kāi)套腹,可以很明顯的看到objc_msgSend就干了兩件事情—— CacheLookup 和 MethodTableLookup绪抛。



/////////////////////////////////////////////////////////////////////
//
// NilTest return-type
//
// Takes: $0 = NORMAL or FPRET or FP2RET or STRET
//  %a1 or %a2 (STRET) = receiver
//
// On exit:  Loads non-nil receiver in %a1 or %a2 (STRET), or returns zero.
//
/////////////////////////////////////////////////////////////////////

.macro NilTest
.if $0 == SUPER  ||  $0 == SUPER_STRET
 error super dispatch does not test for nil
.endif

.if $0 != STRET
 testq %a1, %a1
.else
 testq %a2, %a2
.endif
 PN
 jz LNilTestSlow_f
.endmacro

NilTest是用來(lái)檢測(cè)是否為nil的。傳入?yún)?shù)有4種电禀,NORMAL / FPRET / FP2RET / STRET幢码。

objc_msgSend 傳入的參數(shù)是NilTest NORMAL
objc_msgSend_fpret 傳入的參數(shù)是NilTest FPRET
objc_msgSend_fp2ret 傳入的參數(shù)是NilTest FP2RET
objc_msgSend_stret 傳入的參數(shù)是NilTest STRET

如果檢測(cè)方法的接受者是nil,那么系統(tǒng)會(huì)自動(dòng)clean并且return尖飞。

GetIsaFast宏可以快速地獲取到對(duì)象的 isa 指針地址(放到 r11
寄存器症副,r10會(huì)被重寫(xiě);在 arm 架構(gòu)上是直接賦值到 r9)



.macro CacheLookup
 
 ldrh r12, [r9, #CACHE_MASK] // r12 = mask
 ldr r9, [r9, #CACHE] // r9 = buckets
.if $0 == STRET  ||  $0 == SUPER_STRET
 and r12, r12, r2  // r12 = index = SEL & mask
.else
 and r12, r12, r1  // r12 = index = SEL & mask
.endif
 add r9, r9, r12, LSL #3 // r9 = bucket = buckets+index*8
 ldr r12, [r9]  // r12 = bucket->sel
2:
.if $0 == STRET  ||  $0 == SUPER_STRET
 teq r12, r2
.else
 teq r12, r1
.endif
 bne 1f
 CacheHit $0
1: 
 cmp r12, #1
 blo LCacheMiss_f  // if (bucket->sel == 0) cache miss
 it eq   // if (bucket->sel == 1) cache wrap
 ldreq r9, [r9, #4]  // bucket->imp is before first bucket
 ldr r12, [r9, #8]!  // r12 = (++bucket)->sel
 b 2b

.endmacro

r12里面存的是方法method政基,r9里面是cache贞铣。r1,r2是SEL沮明。在這個(gè)CacheLookup函數(shù)中辕坝,不斷的通過(guò)SEL與cache中的bucket->sel進(jìn)行比較,如果r12 = = 0荐健,則跳轉(zhuǎn)到LCacheMiss_f標(biāo)記去繼續(xù)執(zhí)行酱畅。如果r12找到了,r12 = =1琳袄,即在cache中找到了相應(yīng)的SEL,則直接執(zhí)行該IMP(放在r10中)纺酸。

程序跳到LCacheMiss窖逗,就說(shuō)明cache中無(wú)緩存,未命中緩存餐蔬。這個(gè)時(shí)候就要開(kāi)始下一階段MethodTableLookup的查找了滑负。


/////////////////////////////////////////////////////////////////////
//
// MethodTableLookup classRegister, selectorRegister
//
// Takes: $0 = class to search (a1 or a2 or r10 ONLY)
//  $1 = selector to search for (a2 or a3 ONLY)
//   r11 = class to search
//
// On exit: imp in %r11
//
/////////////////////////////////////////////////////////////////////
.macro MethodTableLookup

 MESSENGER_END_SLOW
 
 SaveRegisters

 // _class_lookupMethodAndLoadCache3(receiver, selector, class)

 movq $0, %a1
 movq $1, %a2
 movq %r11, %a3
 call __class_lookupMethodAndLoadCache3

 // IMP is now in %rax
 movq %rax, %r11

 RestoreRegisters

.endmacro

MethodTableLookup 可以算是個(gè)接口層宏,主要用于保存環(huán)境與準(zhǔn)備參數(shù)用含,來(lái)調(diào)用 __class_lookupMethodAndLoadCache3函數(shù)(在objc-class.mm中)。具體是把receiver, selector, class三個(gè)參數(shù)傳給$0帮匾,$1啄骇,r11,然后再去調(diào)用lookupMethodAndLoadCache3方法瘟斜。最后會(huì)將 IMP 返回(從 r11 挪到 rax)缸夹。最后在 objc_msgSend中調(diào)用 IMP。



/***********************************************************************
* _class_lookupMethodAndLoadCache.
* Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp().
* This lookup avoids optimistic cache scan because the dispatcher 
* already tried that.
**********************************************************************/
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{        
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

__class_lookupMethodAndLoadCache3函數(shù)也是個(gè)接口層(C編寫(xiě))螺句,此函數(shù)提供相應(yīng)參數(shù)配置虽惭,實(shí)際功能在lookUpImpOrForward函數(shù)中。

再來(lái)看看lookUpImpOrForward函數(shù)實(shí)現(xiàn)


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

    /*
    中間是查找過(guò)程蛇尚,詳細(xì)解析見(jiàn)下芽唇。
    */

    // paranoia: look for ignored selectors with non-ignored implementations
    assert(!(ignoreSelector(sel)  &&  imp != (IMP)&_objc_ignored_method));

    // paranoia: never let uncached leak out
    assert(imp != _objc_msgSend_uncached_impcache);

    return imp;
}

接下來(lái)一行行的解析。


    runtimeLock.assertUnlocked();

runtimeLock.assertUnlocked(); 這個(gè)是加一個(gè)讀寫(xiě)鎖取劫,保證線程安全匆笤。


    // Optimistic cache lookup
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

lookUpImpOrForward第5個(gè)新參是是否找到cache的布爾量,如果傳入的是YES谱邪,那么就會(huì)調(diào)用cache_getImp方法去找到緩存里面的IMP炮捧。


/********************************************************************
 * IMP cache_getImp(Class cls, SEL sel)
 *
 * On entry: a1 = class whose cache is to be searched
 *  a2 = selector to search for
 *
 * If found, returns method implementation.
 * If not found, returns NULL.
 ********************************************************************/

 STATIC_ENTRY _cache_getImp

// do lookup
 movq %a1, %r11  // move class to r11 for CacheLookup
 CacheLookup GETIMP  // returns IMP on success

LCacheMiss:
// cache miss, return nil
 xorl %eax, %eax
 ret

LGetImpExit:
 END_ENTRY  _cache_getImp


cache_getImp會(huì)把找到的IMP放在r11中。


    if (!cls->isRealized()) {
        rwlock_writer_t lock(runtimeLock);
        realizeClass(cls);
    }

調(diào)用realizeClass方法是申請(qǐng)class_rw_t的可讀寫(xiě)空間惦银。


    if (initialize  &&  !cls->isInitialized()) {
        _class_initialize (_class_getNonMetaClass(cls, inst));
    }

_class_initialize是類初始化的過(guò)程咆课。


retry:
    runtimeLock.read();

runtimeLock.read();這里加了一個(gè)讀鎖。因?yàn)樵谶\(yùn)行時(shí)中會(huì)動(dòng)態(tài)的添加方法扯俱,為了保證線程安全书蚪,所以要加鎖。從這里開(kāi)始蘸吓,下面會(huì)出現(xiàn)5處goto done的地方善炫,和一處goto retry。


 done:
    runtimeLock.unlockRead();

在done的地方库继,會(huì)完成IMP的查找箩艺,于是可以打開(kāi)讀鎖窜醉。


    // Ignore GC selectors
    if (ignoreSelector(sel)) {
        imp = _objc_ignored_method;
        cache_fill(cls, sel, imp, inst);
        goto done;
    }

緊接著GC selectors是為了忽略macOS中GC垃圾回收機(jī)制用到的方法,iOS則沒(méi)有這一步艺谆。如果忽略榨惰,則進(jìn)行cache_fill,然后跳轉(zhuǎn)到goto done那里去静汤。


void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
    mutex_locker_t lock(cacheUpdateLock);
    cache_fill_nolock(cls, sel, imp, receiver);
#else
    _collecting_in_critical();
    return;
#endif
}


static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
    cacheUpdateLock.assertLocked();

    // Never cache before +initialize is done
    if (!cls->isInitialized()) return;

    // Make sure the entry wasn't added to the cache by some other thread 
    // before we grabbed the cacheUpdateLock.
    if (cache_getImp(cls, sel)) return;

    cache_t *cache = getCache(cls);
    cache_key_t key = getKey(sel);

    // Use the cache as-is if it is less than 3/4 full
    mask_t newOccupied = cache->occupied() + 1;
    mask_t capacity = cache->capacity();
    if (cache->isConstantEmptyCache()) {
        // Cache is read-only. Replace it.
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        // Cache is too full. Expand it.
        cache->expand();
    }
    bucket_t *bucket = cache->find(key, receiver);
    if (bucket->key() == 0) cache->incrementOccupied();
    bucket->set(key, imp);
}

在cache_fill中還會(huì)去調(diào)用cache_fill_nolock函數(shù)琅催,如果緩存中的內(nèi)容大于容量的 3/4就會(huì)擴(kuò)充緩存,使緩存的大小翻倍虫给。找到第一個(gè)空的 bucket_t藤抡,以 (SEL, IMP)的形式填充進(jìn)去。


    // Try this class's cache.

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

如果不忽略抹估,則再次嘗試從類的cache中獲取IMP缠黍,如果獲取到,然后也會(huì)跳轉(zhuǎn)到goto done去药蜻。



    // Try this class's method lists.

    meth = getMethodNoSuper_nolock(cls, sel);
    if (meth) {
        log_and_fill_cache(cls, meth->imp, sel, inst, cls);
        imp = meth->imp;
        goto done;
    }

如果在cache緩存中獲取失敗瓷式,則再去類方法列表里面進(jìn)行查找。找到后跳轉(zhuǎn)到goto done语泽。


    // Try superclass caches and method lists.

    curClass = cls;
    while ((curClass = curClass->superclass)) {
        // 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 {
                // Found a forward:: entry in a superclass.
                // Stop searching, but don't cache yet; call method 
                // resolver for this class first.
                break;
            }
        }

如果以上嘗試都失敗了贸典,接下來(lái)就會(huì)循環(huán)嘗試父類的緩存和方法列表。一直找到NSObject為止踱卵。因?yàn)镹SObject的superclass為nil廊驼,才跳出循環(huán)。

如果在父類中找到了該方法method的IMP惋砂,接下來(lái)就應(yīng)該把這個(gè)方法cache回自己的緩存中蔬充。fill完之后跳轉(zhuǎn)goto done語(yǔ)句。


        // Superclass method list.
        meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
            imp = meth->imp;
            goto done;
        }
    }


如果沒(méi)有在父類的cache中找到IMP班利,繼續(xù)在父類的方法列表里面查找饥漫。如果找到,跳轉(zhuǎn)goto done語(yǔ)句罗标。


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;
}

這里可以解析一下method的查找過(guò)程庸队。在getMethodNoSuper_nolock方法中,會(huì)遍歷一次methodList鏈表闯割,從begin一直遍歷到end彻消。遍歷過(guò)程中會(huì)調(diào)用search_method_list函數(shù)。


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 {
        // Linear search of unsorted method list
        for (auto& meth : *mlist) {
            if (meth.name == sel) return &meth;
        }
    }

#if DEBUG
    // sanity-check negative results
    if (mlist->isFixedUp()) {
        for (auto& meth : *mlist) {
            if (meth.name == sel) {
                _objc_fatal("linear search worked when binary search did not");
            }
        }
    }
#endif

    return nil;
}


在search_method_list函數(shù)中宙拉,會(huì)去判斷當(dāng)前methodList是否有序宾尚,如果有序,會(huì)調(diào)用findMethodInSortedMethodList方法,這個(gè)方法里面的實(shí)現(xiàn)是一個(gè)二分搜索煌贴,具體代碼就不貼了御板。如果非有序,就調(diào)用線性的傻瓜式遍歷搜索牛郑。



    // No implementation found. Try method resolver once.

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

如果父類找到NSObject還沒(méi)有找到怠肋,那么就會(huì)開(kāi)始嘗試_class_resolveMethod方法。注意淹朋,這些需要打開(kāi)讀鎖笙各,因?yàn)殚_(kāi)發(fā)者可能會(huì)在這里動(dòng)態(tài)增加方法實(shí)現(xiàn),所以不需要緩存結(jié)果础芍。此處雖然鎖被打開(kāi)杈抢,可能會(huì)出現(xiàn)線程問(wèn)題,所以在執(zhí)行完_class_resolveMethod方法之后仑性,會(huì)goto retry春感,重新執(zhí)行一遍之前查找的過(guò)程。


/***********************************************************************
* _class_resolveMethod
* Call +resolveClassMethod or +resolveInstanceMethod.
* Returns nothing; any result would be potentially out-of-date already.
* Does not check if the method already exists.
**********************************************************************/
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}


這個(gè)函數(shù)首先判斷是否是meta-class類虏缸,如果不是元類,就執(zhí)行_class_resolveInstanceMethod嫩实,如果是元類刽辙,執(zhí)行_class_resolveClassMethod。這里有一個(gè)lookUpImpOrNil的函數(shù)調(diào)用甲献。



IMP lookUpImpOrNil(Class cls, SEL sel, id inst, 
                   bool initialize, bool cache, bool resolver)
{
    IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
    if (imp == _objc_msgForward_impcache) return nil;
    else return imp;
}

在這個(gè)函數(shù)實(shí)現(xiàn)中宰缤,還會(huì)去調(diào)用lookUpImpOrForward去查找有沒(méi)有傳入的sel的實(shí)現(xiàn),但是返回值還會(huì)返回nil晃洒。在imp == _objc_msgForward_impcache會(huì)返回nil慨灭。_objc_msgForward_impcache是一個(gè)標(biāo)記,這個(gè)標(biāo)記用來(lái)表示在父類的緩存中停止繼續(xù)查找球及。


IMP class_getMethodImplementation(Class cls, SEL sel)
{
    IMP imp;

    if (!cls  ||  !sel) return nil;

    imp = lookUpImpOrNil(cls, sel, nil, 
                         YES/*initialize*/, YES/*cache*/, YES/*resolver*/);

    // Translate forwarding function to C-callable external version
    if (!imp) {
        return _objc_msgForward;
    }

    return imp;
}

再回到_class_resolveMethod的實(shí)現(xiàn)中氧骤,如果lookUpImpOrNil返回nil,就代表在父類中的緩存中找到吃引,于是需要再調(diào)用一次_class_resolveInstanceMethod方法筹陵。保證給sel添加上了對(duì)應(yīng)的IMP。


    // No implementation found, and method resolver didn't help. 
    // Use forwarding.

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

回到lookUpImpOrForward方法中镊尺,如果也沒(méi)有找到IMP的實(shí)現(xiàn)朦佩,那么method resolver也沒(méi)用了,只能進(jìn)入消息轉(zhuǎn)發(fā)階段庐氮。進(jìn)入這個(gè)階段之前语稠,imp變成_objc_msgForward_impcache。最后再加入緩存中弄砍。_objc_msgForward_impcache 第二個(gè)作用就是作為方法實(shí)現(xiàn)的 imp 仙畦,調(diào)用轉(zhuǎn)發(fā)流程输涕。

三. 消息轉(zhuǎn)發(fā)Message Forwarding階段

到了轉(zhuǎn)發(fā)階段,會(huì)調(diào)用id _objc_msgForward(id self, SEL _cmd,...)方法议泵。在objc-msg-x86_64.s中有其匯編的實(shí)現(xiàn)占贫。


 STATIC_ENTRY __objc_msgForward_impcache
 // Method cache version

 // THIS IS NOT A CALLABLE C FUNCTION
 // Out-of-band condition register is NE for stret, EQ otherwise.

 MESSENGER_START
 nop
 MESSENGER_END_SLOW
 
 jne __objc_msgForward_stret
 jmp __objc_msgForward

 END_ENTRY __objc_msgForward_impcache
 
 
 ENTRY __objc_msgForward
 // Non-stret version

 movq __objc_forward_handler(%rip), %r11
 jmp *%r11

 END_ENTRY __objc_msgForward

在執(zhí)行_objc_msgForward之后會(huì)調(diào)用__objc_forward_handler函數(shù)。



// Default forward handler halts the process.
__attribute__((noreturn)) void 
objc_defaultForwardHandler(id self, SEL sel)
{
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}

在最新的Objc2.0中會(huì)有一個(gè)objc_defaultForwardHandler先口,看源碼實(shí)現(xiàn)我們可以看到熟悉的語(yǔ)句型奥。當(dāng)我們給一個(gè)對(duì)象發(fā)送一個(gè)沒(méi)有實(shí)現(xiàn)的方法的時(shí)候,如果其父類也沒(méi)有這個(gè)方法碉京,則會(huì)崩潰厢汹,報(bào)錯(cuò)信息類似于這樣:unrecognized selector sent to instance,然后接著會(huì)跳出一些堆棧信息谐宙。這些信息就是從這里而來(lái)烫葬。



void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

#if SUPPORT_STRET
struct stret { int i[100]; };
__attribute__((noreturn)) struct stret 
objc_defaultForwardStretHandler(id self, SEL sel)
{
    objc_defaultForwardHandler(self, sel);
}
void *_objc_forward_stret_handler = (void*)objc_defaultForwardStretHandler;
#endif

#endif

void objc_setForwardHandler(void *fwd, void *fwd_stret)
{
    _objc_forward_handler = fwd;
#if SUPPORT_STRET
    _objc_forward_stret_handler = fwd_stret;
#endif
}

要設(shè)置轉(zhuǎn)發(fā)只要重寫(xiě)_objc_forward_handler方法即可。在objc_setForwardHandler方法中凡蜻,可以設(shè)置ForwardHandler搭综。

但是當(dāng)你想要弄清objc_setForwardHandler調(diào)用棧的情況的時(shí)候,你會(huì)發(fā)現(xiàn)打印不出來(lái)入口划栓。因?yàn)樘O(píng)果在這里做了點(diǎn)手腳兑巾。關(guān)于objc_setForwardHandler的調(diào)用,以及之后的消息轉(zhuǎn)發(fā)調(diào)用棧的問(wèn)題忠荞,需要用到逆向的知識(shí)蒋歌。推薦大家看這兩篇文章就會(huì)明白其中的原理。

Objective-C 消息發(fā)送與轉(zhuǎn)發(fā)機(jī)制原理
Hmmm, What’s that Selector?

還是回到消息轉(zhuǎn)發(fā)上面來(lái)委煤。當(dāng)前的SEL無(wú)法找到相應(yīng)的IMP的時(shí)候堂油,開(kāi)發(fā)者可以通過(guò)重寫(xiě)- (id)forwardingTargetForSelector:(SEL)aSelector方法來(lái)“偷梁換柱”,把消息的接受者換成一個(gè)可以處理該消息的對(duì)象碧绞。



- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if(aSelector == @selector(Method:)){
        return otherObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

當(dāng)然也可以替換類方法府框,那就要重寫(xiě) + (id)forwardingTargetForSelector:(SEL)aSelector方法,返回值是一個(gè)類對(duì)象讥邻。



+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if(aSelector == @selector(xxx)) {
        return NSClassFromString(@"Class name");
    }
    return [super forwardingTargetForSelector:aSelector];
}

這一步是替消息找備援接收者寓免,如果這一步返回的是nil,那么補(bǔ)救措施就完全的失效了计维,Runtime系統(tǒng)會(huì)向?qū)ο蟀l(fā)送methodSignatureForSelector:消息袜香,并取到返回的方法簽名用于生成NSInvocation對(duì)象。為接下來(lái)的完整的消息轉(zhuǎn)發(fā)生成一個(gè) NSMethodSignature對(duì)象鲫惶。NSMethodSignature 對(duì)象會(huì)被包裝成 NSInvocation 對(duì)象蜈首,forwardInvocation: 方法里就可以對(duì) NSInvocation 進(jìn)行處理了。

接下來(lái)未識(shí)別的方法崩潰之前,系統(tǒng)會(huì)做一次完整的消息轉(zhuǎn)發(fā)欢策。

我們只需要重寫(xiě)下面這個(gè)方法吆寨,就可以自定義我們自己的轉(zhuǎn)發(fā)邏輯了。


- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
         [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}

實(shí)現(xiàn)此方法之后踩寇,若發(fā)現(xiàn)某調(diào)用不應(yīng)由本類處理啄清,則會(huì)調(diào)用超類的同名方法。如此俺孙,繼承體系中的每個(gè)類都有機(jī)會(huì)處理該方法調(diào)用的請(qǐng)求辣卒,一直到NSObject根類。如果到NSObject也不能處理該條消息睛榄,那么就是再無(wú)挽救措施了荣茫,只能拋出“doesNotRecognizeSelector”異常了。

至此场靴,消息發(fā)送和轉(zhuǎn)發(fā)的過(guò)程都清楚明白了啡莉。

四. forwardInvocation的例子

這里我想舉一個(gè)好玩的例子,來(lái)說(shuō)明一下forwardInvocation的使用方法旨剥。

這個(gè)例子中我們會(huì)利用runtime消息轉(zhuǎn)發(fā)機(jī)制創(chuàng)建一個(gè)動(dòng)態(tài)代理咧欣。利用這個(gè)動(dòng)態(tài)代理來(lái)轉(zhuǎn)發(fā)消息。這里我們會(huì)用到兩個(gè)基類的另外一個(gè)神秘的類轨帜,NSProxy魄咕。

NSProxy類和NSObject同為OC里面的基類,但是NSProxy類是一種抽象的基類阵谚,無(wú)法直接實(shí)例化,可用于實(shí)現(xiàn)代理模式烟具。它通過(guò)實(shí)現(xiàn)一組經(jīng)過(guò)簡(jiǎn)化的方法梢什,代替目標(biāo)對(duì)象捕捉和處理所有的消息。NSProxy類也同樣實(shí)現(xiàn)了NSObject的協(xié)議聲明的方法朝聋,而且它有兩個(gè)必須實(shí)現(xiàn)的方法嗡午。


- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available");

另外還需要說(shuō)明的是,NSProxy類的子類必須聲明并實(shí)現(xiàn)至少一個(gè)init方法冀痕,這樣才能符合OC中創(chuàng)建和初始化對(duì)象的慣例荔睹。Foundation框架里面也含有多個(gè)NSProxy類的具體實(shí)現(xiàn)類。

  • NSDistantObject類:定義其他應(yīng)用程序或線程中對(duì)象的代理類言蛇。
  • NSProtocolChecker類:定義對(duì)象僻他,使用這話對(duì)象可以限定哪些消息能夠發(fā)送給另外一個(gè)對(duì)象。

接下來(lái)就來(lái)看看下面這個(gè)好玩的例子腊尚。


#import <Foundation/Foundation.h>

@interface Student : NSObject
-(void)study:(NSString *)subject andRead:(NSString *)bookName;
-(void)study:(NSString *)subject :(NSString *)bookName;
@end

定義一個(gè)student類吨拗,里面隨便給兩個(gè)方法。


#import "Student.h"
#import <objc/runtime.h>

@implementation Student

-(void)study:(NSString *)subject :(NSString *)bookName
{
    NSLog(@"Invorking method on %@ object with selector %@",[self class],NSStringFromSelector(_cmd));
}

-(void)study:(NSString *)subject andRead:(NSString *)bookName
{
    NSLog(@"Invorking method on %@ object with selector %@",[self class],NSStringFromSelector(_cmd));
}
@end

在兩個(gè)方法實(shí)現(xiàn)里面增加log信息,這是為了一會(huì)打印的時(shí)候方便知道調(diào)用了哪個(gè)方法劝篷。


#import <Foundation/Foundation.h>
#import "Invoker.h"

@interface AspectProxy : NSProxy

/** 通過(guò)NSProxy實(shí)例轉(zhuǎn)發(fā)消息的真正對(duì)象 */
@property(strong) id proxyTarget;
/** 能夠?qū)崿F(xiàn)橫切功能的類(遵守Invoker協(xié)議)的實(shí)例 */
@property(strong) id<Invoker> invoker;
/** 定義了哪些消息會(huì)調(diào)用橫切功能 */
@property(readonly) NSMutableArray *selectors;

// AspectProxy類實(shí)例的初始化方法
- (id)initWithObject:(id)object andInvoker:(id<Invoker>)invoker;
- (id)initWithObject:(id)object selectors:(NSArray *)selectors andInvoker:(id<Invoker>)invoker;
// 向當(dāng)前的選擇器列表中添加選擇器
- (void)registerSelector:(SEL)selector;

@end

定義一個(gè)AspectProxy類哨鸭,這個(gè)類專門(mén)用來(lái)轉(zhuǎn)發(fā)消息的。


#import "AspectProxy.h"

@implementation AspectProxy

- (id)initWithObject:(id)object selectors:(NSArray *)selectors andInvoker:(id<Invoker>)invoker{
    _proxyTarget = object;
    _invoker = invoker;
    _selectors = [selectors mutableCopy];
    
    return self;
}

- (id)initWithObject:(id)object andInvoker:(id<Invoker>)invoker{
    return [self initWithObject:object selectors:nil andInvoker:invoker];
}

// 添加另外一個(gè)選擇器
- (void)registerSelector:(SEL)selector{
    NSValue *selValue = [NSValue valueWithPointer:selector];
    [self.selectors addObject:selValue];
}

// 為目標(biāo)對(duì)象中被調(diào)用的方法返回一個(gè)NSMethodSignature實(shí)例
// 運(yùn)行時(shí)系統(tǒng)要求在執(zhí)行標(biāo)準(zhǔn)轉(zhuǎn)發(fā)時(shí)實(shí)現(xiàn)這個(gè)方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    return [self.proxyTarget methodSignatureForSelector:sel];
}

/**
 *  當(dāng)調(diào)用目標(biāo)方法的選擇器與在AspectProxy對(duì)象中注冊(cè)的選擇器匹配時(shí)娇妓,forwardInvocation:會(huì)
 *  調(diào)用目標(biāo)對(duì)象中的方法像鸡,并根據(jù)條件語(yǔ)句的判斷結(jié)果調(diào)用AOP(面向切面編程)功能
 */
- (void)forwardInvocation:(NSInvocation *)invocation{
    // 在調(diào)用目標(biāo)方法前執(zhí)行橫切功能
    if ([self.invoker respondsToSelector:@selector(preInvoke:withTarget:)]) {
        if (self.selectors != nil) {
            SEL methodSel = [invocation selector];
            for (NSValue *selValue in self.selectors) {
                if (methodSel == [selValue pointerValue]) {
                    [[self invoker] preInvoke:invocation withTarget:self.proxyTarget];
                    break;
                }
            }
        }else{
            [[self invoker] preInvoke:invocation withTarget:self.proxyTarget];
        }
    }
    
    // 調(diào)用目標(biāo)方法
    [invocation invokeWithTarget:self.proxyTarget];
    
    // 在調(diào)用目標(biāo)方法后執(zhí)行橫切功能
    if ([self.invoker respondsToSelector:@selector(postInvoke:withTarget:)]) {
        if (self.selectors != nil) {
            SEL methodSel = [invocation selector];
            for (NSValue *selValue in self.selectors) {
                if (methodSel == [selValue pointerValue]) {
                    [[self invoker] postInvoke:invocation withTarget:self.proxyTarget];
                    break;
                }
            }
        }else{
            [[self invoker] postInvoke:invocation withTarget:self.proxyTarget];
        }
    }
}

接著我們定義一個(gè)代理協(xié)議


#import <Foundation/Foundation.h>

@protocol Invoker <NSObject>

@required
// 在調(diào)用對(duì)象中的方法前執(zhí)行對(duì)功能的橫切
- (void)preInvoke:(NSInvocation *)inv withTarget:(id)target;
@optional
// 在調(diào)用對(duì)象中的方法后執(zhí)行對(duì)功能的橫切
- (void)postInvoke:(NSInvocation *)inv withTarget:(id)target;

@end

最后還需要一個(gè)遵守協(xié)議的類


#import <Foundation/Foundation.h>
#import "Invoker.h"

@interface AuditingInvoker : NSObject<Invoker>//遵守Invoker協(xié)議
@end


#import "AuditingInvoker.h"

@implementation AuditingInvoker

- (void)preInvoke:(NSInvocation *)inv withTarget:(id)target{
    NSLog(@"before sending message with selector %@ to %@ object", NSStringFromSelector([inv selector]),[target className]);
}
- (void)postInvoke:(NSInvocation *)inv withTarget:(id)target{
    NSLog(@"after sending message with selector %@ to %@ object", NSStringFromSelector([inv selector]),[target className]);

}
@end

在這個(gè)遵循代理類里面我們只實(shí)現(xiàn)協(xié)議里面的兩個(gè)方法。

寫(xiě)出測(cè)試代碼


#import <Foundation/Foundation.h>
#import "AspectProxy.h"
#import "AuditingInvoker.h"
#import "Student.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        id student = [[Student alloc] init];

        // 設(shè)置代理中注冊(cè)的選擇器數(shù)組
        NSValue *selValue1 = [NSValue valueWithPointer:@selector(study:andRead:)];
        NSArray *selValues = @[selValue1];
        // 創(chuàng)建AuditingInvoker
        AuditingInvoker *invoker = [[AuditingInvoker alloc] init];
        // 創(chuàng)建Student對(duì)象的代理studentProxy
        id studentProxy = [[AspectProxy alloc] initWithObject:student selectors:selValues andInvoker:invoker];
        
        // 使用指定的選擇器向該代理發(fā)送消息---例子1
        [studentProxy study:@"Computer" andRead:@"Algorithm"];
        
        // 使用還未注冊(cè)到代理中的其他選擇器哈恰,向這個(gè)代理發(fā)送消息只估!---例子2
        [studentProxy study:@"mathematics" :@"higher mathematics"];
        
        // 為這個(gè)代理注冊(cè)一個(gè)選擇器并再次向其發(fā)送消息---例子3
        [studentProxy registerSelector:@selector(study::)];
        [studentProxy study:@"mathematics" :@"higher mathematics"];
    }
    return 0;
}

這里有3個(gè)例子。里面會(huì)分別輸出什么呢蕊蝗?



before sending message with selector study:andRead: to Student object
Invorking method on Student object with selector study:andRead:
after sending message with selector study:andRead: to Student object

Invorking method on Student object with selector study::

before sending message with selector study:: to Student object
Invorking method on Student object with selector study::
after sending message with selector study:: to Student object

例子1中會(huì)輸出3句話仅乓。調(diào)用Student對(duì)象的代理中的study:andRead:方法,會(huì)使該代理調(diào)用AuditingInvoker對(duì)象中的preInvoker:方法蓬戚、真正目標(biāo)(Student對(duì)象)中的study:andRead:方法夸楣,以及AuditingInvoker對(duì)象中的postInvoker:方法。一個(gè)方法的調(diào)用子漩,調(diào)用起了3個(gè)方法豫喧。原因是study:andRead:方法是通過(guò)Student對(duì)象的代理注冊(cè)的;

例子2就只會(huì)輸出1句話幢泼。調(diào)用Student對(duì)象代理中的study::方法紧显,因?yàn)樵摲椒ㄟ€未通過(guò)這個(gè)代理注冊(cè),所以程序僅會(huì)將調(diào)用該方法的消息轉(zhuǎn)發(fā)給Student對(duì)象缕棵,而不會(huì)調(diào)用AuditorInvoker方法孵班。

例子3又會(huì)輸出3句話了。因?yàn)閟tudy::通過(guò)這個(gè)代理進(jìn)行了注冊(cè)招驴,然后程序再次調(diào)用它篙程,在這次調(diào)用過(guò)程中,程序會(huì)調(diào)用AuditingInvoker對(duì)象中的AOP方法和真正目標(biāo)(Student對(duì)象)中的study::方法别厘。

這個(gè)例子就實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的AOP(Aspect Oriented Programming)面向切面編程虱饿。我們把一切功能"切"出去,與其他部分分開(kāi)触趴,這樣可以提高程序的模塊化程度氮发。AOP能解耦也能動(dòng)態(tài)組裝,可以通過(guò)預(yù)編譯方式和運(yùn)行期動(dòng)態(tài)代理實(shí)現(xiàn)在不修改源代碼的情況下給程序動(dòng)態(tài)統(tǒng)一添加功能冗懦。比如上面的例子三爽冕,我們通過(guò)把方法注冊(cè)到動(dòng)態(tài)代理類中,于是就實(shí)現(xiàn)了該類也能處理方法的功能披蕉。

五. 入院考試

下面的代碼會(huì)扇售?Compile Error / Runtime Crash / NSLog…?

 @interface NSObject (Sark)
 + (void)foo;
 - (void)foo;
 @end
 @implementation NSObject (Sark)
 - (void)foo
 {
    NSLog(@"IMP: -[NSObject(Sark) foo]");
 }
 @end
 int main(int argc, const char * argv[]) {
  @autoreleasepool {
      [NSObject foo];
      [[NSObject new] foo];
}
return 0;
}

這道有兩處難點(diǎn)前塔,難點(diǎn)一是給NSObject增加了一個(gè)分類,分類聲明的是一個(gè)加號(hào)的類方法承冰,而實(shí)現(xiàn)中是一個(gè)減號(hào)的實(shí)例方法华弓。在main中去NSObject去調(diào)用了這個(gè)foo方法,會(huì)編譯錯(cuò)誤困乒,還是會(huì)Crash呢寂屏?

難點(diǎn)二是會(huì)輸出什么內(nèi)容呢?

先來(lái)看難點(diǎn)一娜搂,這里會(huì)牽扯到Category的知識(shí)迁霎。推薦文章還是美團(tuán)的這篇經(jīng)典的深入理解Objective-C:Category


void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    lock_init();
    exception_init();
    
    // Register for unmap first, in case some +load unmaps something
    _dyld_register_func_for_remove_image(&unmap_image);
    dyld_register_image_state_change_handler(dyld_image_state_bound,
                                             1/*batch*/, &map_images);
    dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images);
}

OC在初始化的時(shí)候,會(huì)去加載map_images百宇,map_images最終會(huì)調(diào)用objc-runtime-new.mm里面的_read_images方法考廉。_read_images方法里面會(huì)去初始化內(nèi)存中的map, 這個(gè)時(shí)候?qū)?huì)load所有的類,協(xié)議還有Category携御。NSOBject的+load方法就是這個(gè)時(shí)候調(diào)用的昌粤。


// Discover categories.
for (EACH_HEADER) {
    category_t **catlist =
    _getObjc2CategoryList(hi, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = catlist[i];
        class_t *cls = remapClass(cat->cls);
        
        if (!cls) {
            // Category's target class is missing (probably weak-linked).
            // Disavow any knowledge of this category.
            catlist[i] = NULL;
            if (PrintConnecting) {
                _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                             "missing weak-linked target class",
                             cat->name, cat);
            }
            continue;
        }
        
        // Process this category.
        // First, register the category with its target class.
        // Then, rebuild the class's method lists (etc) if
        // the class is realized.
        BOOL classExists = NO;
        if (cat->instanceMethods ||  cat->protocols
            ||  cat->instanceProperties)
        {
            addUnattachedCategoryForClass(cat, cls, hi);
            if (isRealized(cls)) {
                remethodizeClass(cls);
                classExists = YES;
            }
            if (PrintConnecting) {
                _objc_inform("CLASS: found category -%s(%s) %s",
                             getName(cls), cat->name,
                             classExists ? "on existing class" : "");
            }
        }
        
        if (cat->classMethods  ||  cat->protocols
            /* ||  cat->classProperties */)
        {
            addUnattachedCategoryForClass(cat, cls->isa, hi);
            if (isRealized(cls->isa)) {
                remethodizeClass(cls->isa);
            }
            if (PrintConnecting) {
                _objc_inform("CLASS: found category +%s(%s)",
                             getName(cls), cat->name);
            }
        }
    }
}

在這個(gè)加載中,for循環(huán)中會(huì)反復(fù)調(diào)用_getObjc2CategoryList
方法啄刹,這個(gè)方法的具體實(shí)現(xiàn)是:


//      function name                 content type     section name
GETSECT(_getObjc2CategoryList,        category_t *,    "__objc_catlist");

最后一個(gè)參數(shù)__objc_catlist就是編譯器剛剛生成的category數(shù)組涮坐。

加載完所有的category之后,就開(kāi)始處理這些類別誓军。大體思路還是分為2類來(lái)分開(kāi)處理袱讹。


if (cat->instanceMethods || cat->protocols || cat->instanceProperties){
}

第一類是實(shí)例方法



if (cat->classMethods || cat->protocols /* || cat->classProperties */) {
}

第二類是類方法。

處理完之后的結(jié)果
1)昵时、把category的實(shí)例方法捷雕、協(xié)議以及屬性添加到類上
2)、把category的類方法和協(xié)議添加到類的metaclass上

這兩種情況里面的處理方式都差不多壹甥,先去調(diào)用addUnattachedCategoryForClass函數(shù)救巷,申請(qǐng)內(nèi)存,分配空間盹廷。remethodizeClass這個(gè)方法里面會(huì)調(diào)用attachCategories方法征绸。

attachCategories方法代碼就不貼了久橙,有興趣的可以自己去看看漓雅。這個(gè)方法里面會(huì)用頭插法缸棵,把新加的方法從頭插入方法鏈表中。并且最后還會(huì)flushCaches。

這也就是為什么我們可以在Category里面覆蓋原有的方法的原因伯复,因?yàn)轭^插法,新的方法在鏈表的前面您没,會(huì)優(yōu)先被遍歷到温算。

以上就是Category加載時(shí)候的流程她肯。

再回到這道題目上面來(lái),在加載NSObject的Category中鹰贵,在編譯期會(huì)提示我們沒(méi)有實(shí)現(xiàn)+(void)foo的方法晴氨,因?yàn)樵?m文件中并沒(méi)有找到+的方法,而是一個(gè)-號(hào)的方法碉输,所以會(huì)提示籽前。

但是在實(shí)際加載Category的時(shí)候,會(huì)把-(void)foo加載進(jìn)去敷钾,由于是實(shí)例方法枝哄,所以會(huì)放在NSObject的實(shí)例方法鏈表里面。

根據(jù)第二章分析的objc_msgSend源碼實(shí)現(xiàn)阻荒,我們可以知道:

在調(diào)用[NSObject foo]的時(shí)候挠锥,會(huì)先在NSObject的meta-class中去查找foo方法的IMP,未找到侨赡,繼續(xù)在superClass中去查找蓖租,NSObject的meta-class的superClass就是本身NSObject,于是又回到NSObject的類方法中查找foo方法辆毡,于是乎找到了菜秦,執(zhí)行foo方法,輸出


IMP: -[NSObject(Sark) foo]

在調(diào)用[[NSObject new] foo]的時(shí)候舶掖,會(huì)先生成一個(gè)NSObject的對(duì)象球昨,用這個(gè)NSObject實(shí)例對(duì)象再去調(diào)用foo方法的時(shí)候,會(huì)去NSObject的實(shí)例方法里面去查找眨攘,找到主慰,于是也會(huì)輸出


IMP: -[NSObject(Sark) foo]

所以上面這題,不會(huì)Compile Error 鲫售,更不會(huì) Runtime Crash 共螺,會(huì)輸出兩個(gè)相同的結(jié)果。

六. Runtime中的優(yōu)化

關(guān)于Runtime系統(tǒng)中藐不,有3種地方進(jìn)行了優(yōu)化。

  • 1.方法列表的緩存
  • 2.虛函數(shù)表vTable
  • 3.dyld共享緩存
1.方法列表的緩存

在消息發(fā)送過(guò)程中秦效,查找IMP的過(guò)程雏蛮,會(huì)優(yōu)先查找緩存。這個(gè)緩存會(huì)存儲(chǔ)最近使用過(guò)的方法都緩存起來(lái)阱州。這個(gè)cache和CPU里面的cache的工作方式有點(diǎn)類似挑秉。原理是調(diào)用的方法有可能經(jīng)常會(huì)被調(diào)用。如果沒(méi)有這個(gè)緩存苔货,直接去類方法的方法鏈表里面去查找犀概,查詢效率實(shí)在太低立哑。所以查找IMP會(huì)優(yōu)先搜索飯方法緩存,如果沒(méi)有找到姻灶,接著會(huì)在虛函數(shù)表中尋找IMP铛绰。如果找到了,就會(huì)把這個(gè)IMP存儲(chǔ)到緩存中備用产喉。

基于這個(gè)設(shè)計(jì)至耻,使Runtime系統(tǒng)能能夠執(zhí)行快速高效的方法查詢操作。

2.虛函數(shù)表

虛函數(shù)表也稱為分派表镊叁,是編程語(yǔ)言中常用的動(dòng)態(tài)綁定支持機(jī)制尘颓。在OC的Runtime運(yùn)行時(shí)系統(tǒng)庫(kù)實(shí)現(xiàn)了一種自定義的虛函數(shù)表分派機(jī)制。這個(gè)表是專門(mén)用來(lái)提高性能和靈活性的晦譬。這個(gè)虛函數(shù)表是用來(lái)存儲(chǔ)IMP類型的數(shù)組疤苹。每個(gè)object-class都有這樣一個(gè)指向虛函數(shù)表的指針。

3.dyld共享緩存

在我們的程序中敛腌,一定會(huì)有很多自定義類卧土,而這些類中,很多SEL是重名的像樊,比如alloc尤莺,init等等。Runtime系統(tǒng)需要為每一個(gè)方法給定一個(gè)SEL指針生棍,然后為每次調(diào)用個(gè)各個(gè)方法更新元數(shù)據(jù)颤霎,以獲取唯一值。這個(gè)過(guò)程是在應(yīng)用程序啟動(dòng)的時(shí)候完成涂滴。為了提高這一部分的執(zhí)行效率友酱,Runtime會(huì)通過(guò)dyld共享緩存實(shí)現(xiàn)選擇器的唯一性。

dyld是一種系統(tǒng)服務(wù)柔纵,用于定位和加載動(dòng)態(tài)庫(kù)缔杉。它含有共享緩存,能夠使多個(gè)進(jìn)程共用這些動(dòng)態(tài)庫(kù)搁料。dyld共享緩存中含有一個(gè)選擇器表或详,從而能使運(yùn)行時(shí)系統(tǒng)能夠通過(guò)使用緩存訪問(wèn)共享庫(kù)和自定義類的選擇器。

關(guān)于dyld的知識(shí)可以看看這篇文章dyld: Dynamic Linking On OS X

未完待續(xù)郭计,請(qǐng)大家多多指教霸琴。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市拣宏,隨后出現(xiàn)的幾起案子沈贝,更是在濱河造成了極大的恐慌杠人,老刑警劉巖勋乾,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宋下,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡辑莫,警方通過(guò)查閱死者的電腦和手機(jī)学歧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)各吨,“玉大人枝笨,你說(shuō)我怎么就攤上這事〗已眩” “怎么了横浑?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)屉更。 經(jīng)常有香客問(wèn)我徙融,道長(zhǎng),這世上最難降的妖魔是什么瑰谜? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任欺冀,我火速辦了婚禮,結(jié)果婚禮上萨脑,老公的妹妹穿的比我還像新娘隐轩。我一直安慰自己,他們只是感情好渤早,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布职车。 她就那樣靜靜地躺著,像睡著了一般鹊杖。 火紅的嫁衣襯著肌膚如雪提鸟。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天仅淑,我揣著相機(jī)與錄音称勋,去河邊找鬼。 笑死涯竟,一個(gè)胖子當(dāng)著我的面吹牛赡鲜,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播庐船,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼银酬,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了筐钟?” 一聲冷哼從身側(cè)響起揩瞪,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎篓冲,沒(méi)想到半個(gè)月后李破,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體宠哄,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年嗤攻,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了毛嫉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡妇菱,死狀恐怖承粤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情闯团,我是刑警寧澤辛臊,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站房交,受9級(jí)特大地震影響浪讳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜涌萤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一淹遵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧负溪,春花似錦透揣、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至崖堤,卻和暖如春侍咱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背密幔。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工楔脯, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人胯甩。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓昧廷,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親偎箫。 傳聞我的和親對(duì)象是個(gè)殘疾皇子木柬,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

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