神經病院Objective-C Runtime住院次日—消息發送與轉發

前言

如今愈來愈多的app都使用了JSPatch實現app熱修復,而JSPatch 能作到經過 JS 調用和改寫 OC 方法最根本的緣由是 Objective-C 是動態語言,OC 上全部方法的調用/類的生成都經過 Objective-C Runtime 在運行時進行,咱們能夠經過類名/方法名反射獲得相應的類和方法,也能夠替換某個類的方法爲新的實現,理論上你能夠在運行時經過類名/方法名調用到任何 OC 方法,替換任何類的實現以及新增任意類。今天就來詳細解析一下OC中runtime最爲吸引人的地方。html

目錄

  • 1.objc_msgSend函數簡介
  • 2.消息發送Messaging階段—objc_msgSend源碼解析
  • 3.消息轉發Message Forwarding階段
  • 4.forwardInvocation的例子
  • 5.入院考試
  • 6.Runtime中的優化

一.objc_msgSend函數簡介

最初接觸到OC Runtime,必定是從[receiver message]這裏開始的。[receiver message]會被編譯器轉化爲:編程

id objc_msgSend ( id self, SEL op, ... );複製代碼

這是一個可變參數函數。第二個參數類型是SEL。SEL在OC中是selector方法選擇器。vim

typedef struct objc_selector *SEL;複製代碼

objc_selector是一個映射到方法的C字符串。須要注意的是@selector()選擇子只與函數名有關。不一樣類中相同名字的方法所對應的方法選擇器是相同的,即便方法名字相同而變量類型不一樣也會致使它們具備相同的方法選擇器。因爲這點特性,也致使了OC不支持函數重載。數組

在receiver拿到對應的selector以後,若是本身沒法執行這個方法,那麼該條消息要被轉發。或者臨時動態的添加方法實現。若是轉發到最後依舊無法處理,程序就會崩潰。緩存

因此編譯期僅僅是肯定了要發送消息,而消息如何處理是要運行期須要解決的事情。安全

objc_msgSend函數究竟會幹什麼事情呢?從這篇「objc_msgSend() Tour」文章裏面能夠獲得一個比較詳細的結論。架構

  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.

總結一下objc_msgSend會作一下幾件事情: 1.檢測這個 selector是否是要忽略的。 2.檢查target是否是爲nil。app

若是這裏有相應的nil的處理函數,就跳轉到相應的函數中。 若是沒有處理nil的函數,就自動清理現場並返回。這一點就是爲什麼在OC中給nil發送消息不會崩潰的緣由。框架

3.肯定不是給nil發消息以後,在該class的緩存中查找方法對應的IMP實現。less

若是找到,就跳轉進去執行。 若是沒有找到,就在方法分發表裏面繼續查找,一直找到NSObject爲止。

4.若是尚未找到,那就須要開始消息轉發階段了。至此,發送消息Messaging階段完成。這一階段主要完成的是經過select()快速查找IMP的過程。

二. 消息發送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);
}複製代碼

該源碼中有一個do-while循環,這個循環就是上一章裏面提到的在方法分發表裏面查找method的過程。

不過在obj4-680裏面的objc-msg-x86_64.s文件中實現是一段彙編代碼。

/********************************************************************
 *
 * 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複製代碼

來分析一下這段彙編代碼。

乍一看,若是從LCacheMiss:這裏上下分開,能夠很明顯的看到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是用來檢測是否爲nil的。傳入參數有4種,NORMAL / FPRET / FP2RET / STRET。

objc_msgSend 傳入的參數是NilTest NORMAL objc_msgSend_fpret 傳入的參數是NilTest FPRET objc_msgSend_fp2ret 傳入的參數是NilTest FP2RET objc_msgSend_stret 傳入的參數是NilTest STRET

若是檢測方法的接受者是nil,那麼系統會自動clean而且return。

GetIsaFast宏能夠快速地獲取到對象的 isa 指針地址(放到 r11 寄存器,r10會被重寫;在 arm 架構上是直接賦值到 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。在這個CacheLookup函數中,不斷的經過SEL與cache中的bucket->sel進行比較,若是r12 = = 0,則跳轉到LCacheMiss_f標記去繼續執行。若是r12找到了,r12 = =1,即在cache中找到了相應的SEL,則直接執行該IMP(放在r10中)。

程序跳到LCacheMiss,就說明cache中無緩存,未命中緩存。這個時候就要開始下一階段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 能夠算是個接口層宏,主要用於保存環境與準備參數,來調用 __class_lookupMethodAndLoadCache3函數(在objc-class.mm中)。具體是把receiver, selector, class三個參數傳給$0,$1,r11,而後再去調用lookupMethodAndLoadCache3方法。最後會將 IMP 返回(從 r11 挪到 rax)。最後在 objc_msgSend中調用 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函數也是個接口層(C編寫),此函數提供相應參數配置,實際功能在lookUpImpOrForward函數中。

再來看看lookUpImpOrForward函數實現

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

    /* 中間是查找過程,詳細解析見下。 */

    // 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;
}複製代碼

接下來一行行的解析。

runtimeLock.assertUnlocked();複製代碼

runtimeLock.assertUnlocked(); 這個是加一個讀寫鎖,保證線程安全。

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

lookUpImpOrForward第5個新參是是否找到cache的布爾量,若是傳入的是YES,那麼就會調用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會把找到的IMP放在r11中。

if (!cls->isRealized()) {
        rwlock_writer_t lock(runtimeLock);
        realizeClass(cls);
    }複製代碼

調用realizeClass方法是申請class_rw_t的可讀寫空間。

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

_class_initialize是類初始化的過程。

retry:
    runtimeLock.read();複製代碼

runtimeLock.read();這裏加了一個讀鎖。由於在運行時中會動態的添加方法,爲了保證線程安全,因此要加鎖。從這裏開始,下面會出現5處goto done的地方,和一處goto retry。

done:
    runtimeLock.unlockRead();複製代碼

在done的地方,會完成IMP的查找,因而能夠打開讀鎖。

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

緊接着GC selectors是爲了忽略macOS中GC垃圾回收機制用到的方法,iOS則沒有這一步。若是忽略,則進行cache_fill,而後跳轉到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中還會去調用cache_fill_nolock函數,若是緩存中的內容大於容量的 3/4就會擴充緩存,使緩存的大小翻倍。找到第一個空的 bucket_t,以 (SEL, IMP)的形式填充進去。

// Try this class's cache.

    imp = cache_getImp(cls, sel);
    if (imp) goto done;複製代碼

若是不忽略,則再次嘗試從類的cache中獲取IMP,若是獲取到,而後也會跳轉到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緩存中獲取失敗,則再去類方法列表裏面進行查找。找到後跳轉到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;
            }
        }複製代碼

若是以上嘗試都失敗了,接下來就會循環嘗試父類的緩存和方法列表。一直找到NSObject爲止。由於NSObject的superclass爲nil,才跳出循環。

若是在父類中找到了該方法method的IMP,接下來就應該把這個方法cache回本身的緩存中。fill完以後跳轉goto done語句。

// 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;
        }
    }複製代碼

若是沒有在父類的cache中找到IMP,繼續在父類的方法列表裏面查找。若是找到,跳轉goto done語句。

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的查找過程。在getMethodNoSuper_nolock方法中,會遍歷一次methodList鏈表,從begin一直遍歷到end。遍歷過程當中會調用search_method_list函數。

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函數中,會去判斷當前methodList是否有序,若是有序,會調用findMethodInSortedMethodList方法,這個方法裏面的實現是一個二分搜索,具體代碼就不貼了。若是非有序,就調用線性的傻瓜式遍歷搜索。

// 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尚未找到,那麼就會開始嘗試_class_resolveMethod方法。注意,這些須要打開讀鎖,由於開發者可能會在這裏動態增長方法實現,因此不須要緩存結果。此處雖然鎖被打開,可能會出現線程問題,因此在執行完_class_resolveMethod方法以後,會goto retry,從新執行一遍以前查找的過程。

/*********************************************************************** * _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);
        }
    }
}複製代碼

這個函數首先判斷是不是meta-class類,若是不是元類,就執行_class_resolveInstanceMethod,若是是元類,執行_class_resolveClassMethod。這裏有一個lookUpImpOrNil的函數調用。

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;
}複製代碼

在這個函數實現中,還會去調用lookUpImpOrForward去查找有沒有傳入的sel的實現,可是返回值還會返回nil。在imp == _objc_msgForward_impcache會返回nil。_objc_msgForward_impcache是一個標記,這個標記用來表示在父類的緩存中中止繼續查找。

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的實現中,若是lookUpImpOrNil返回nil,就表明在父類中的緩存中找到,因而須要再調用一次_class_resolveInstanceMethod方法。保證給sel添加上了對應的IMP。

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

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);複製代碼

回到lookUpImpOrForward方法中,若是也沒有找到IMP的實現,那麼method resolver也沒用了,只能進入消息轉發階段。進入這個階段以前,imp變成_objc_msgForward_impcache。最後再加入緩存中。

三. 消息轉發Message Forwarding階段

到了轉發階段,會調用id _objc_msgForward(id self, SEL _cmd,...)方法。在objc-msg-x86_64.s中有其彙編的實現。

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複製代碼

在執行_objc_msgForward以後會調用__objc_forward_handler函數。

// 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中會有一個objc_defaultForwardHandler,看源碼實現咱們能夠看到熟悉的語句。當咱們給一個對象發送一個沒有實現的方法的時候,若是其父類也沒有這個方法,則會崩潰,報錯信息相似於這樣:unrecognized selector sent to instance,而後接着會跳出一些堆棧信息。這些信息就是從這裏而來。

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
}複製代碼

要設置轉發只要重寫_objc_forward_handler方法便可。在objc_setForwardHandler方法中,能夠設置ForwardHandler。

可是當你想要弄清objc_setForwardHandler調用棧的狀況的時候,你會發現打印不出來入口。由於蘋果在這裏作了點手腳。關於objc_setForwardHandler的調用,以及以後的消息轉發調用棧的問題,須要用到逆向的知識。推薦你們看這兩篇文章就會明白其中的原理。

Objective-C 消息發送與轉發機制原理
Hmmm, What’s that Selector?

仍是回到消息轉發上面來。當前的SEL沒法找到相應的IMP的時候,開發者能夠經過重寫- (id)forwardingTargetForSelector:(SEL)aSelector方法來「偷樑換柱」,把消息的接受者換成一個能夠處理該消息的對象。

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

固然也能夠替換類方法,那就要重寫 + (id)forwardingTargetForSelector:(SEL)aSelector方法,返回值是一個類對象。

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

這一步是替消息找備援接收者,若是這一步返回的是nil,那麼補救措施就徹底的失效了,Runtime系統會向對象發送methodSignatureForSelector:消息,並取到返回的方法簽名用於生成NSInvocation對象。爲接下來的完整的消息轉發生成一個 NSMethodSignature對象。NSMethodSignature 對象會被包裝成 NSInvocation 對象,forwardInvocation: 方法裏就能夠對 NSInvocation 進行處理了。

接下來未識別的方法崩潰以前,系統會作一次完整的消息轉發。

咱們只須要重寫下面這個方法,就能夠自定義咱們本身的轉發邏輯了。

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

實現此方法以後,若發現某調用不該由本類處理,則會調用超類的同名方法。如此,繼承體系中的每一個類都有機會處理該方法調用的請求,一直到NSObject根類。若是到NSObject也不能處理該條消息,那麼就是再無挽救措施了,只能拋出「doesNotRecognizeSelector」異常了。

至此,消息發送和轉發的過程都清楚明白了。

四. forwardInvocation的例子

這裏我想舉一個好玩的例子,來講明一下forwardInvocation的使用方法。

這個例子中咱們會利用runtime消息轉發機制建立一個動態代理。利用這個動態代理來轉發消息。這裏咱們會用到兩個基類的另一個神祕的類,NSProxy。

NSProxy類和NSObject同爲OC裏面的基類,可是NSProxy類是一種抽象的基類,沒法直接實例化,可用於實現代理模式。它經過實現一組通過簡化的方法,代替目標對象捕捉和處理全部的消息。NSProxy類也一樣實現了NSObject的協議聲明的方法,並且它有兩個必須實現的方法。

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

另外還須要說明的是,NSProxy類的子類必須聲明並實現至少一個init方法,這樣才能符合OC中建立和初始化對象的慣例。Foundation框架裏面也含有多個NSProxy類的具體實現類。

  • NSDistantObject類:定義其餘應用程序或線程中對象的代理類。
  • NSProtocolChecker類:定義對象,使用這話對象能夠限定哪些消息可以發送給另一個對象。

接下來就來看看下面這個好玩的例子。

#import <Foundation/Foundation.h>

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

定義一個student類,裏面隨便給兩個方法。

#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複製代碼

在兩個方法實現裏面增長log信息,這是爲了一會打印的時候方便知道調用了哪一個方法。

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

@interface AspectProxy : NSProxy

/** 經過NSProxy實例轉發消息的真正對象 */
@property(strong) id proxyTarget;
/** 可以實現橫切功能的類(遵照Invoker協議)的實例 */
@property(strong) id<Invoker> invoker;
/** 定義了哪些消息會調用橫切功能 */
@property(readonly) NSMutableArray *selectors;

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

@end複製代碼

定義一個AspectProxy類,這個類專門用來轉發消息的。

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

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

// 爲目標對象中被調用的方法返回一個NSMethodSignature實例
// 運行時系統要求在執行標準轉發時實現這個方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    return [self.proxyTarget methodSignatureForSelector:sel];
}

/** * 當調用目標方法的選擇器與在AspectProxy對象中註冊的選擇器匹配時,forwardInvocation:會 * 調用目標對象中的方法,並根據條件語句的判斷結果調用AOP(面向切面編程)功能 */
- (void)forwardInvocation:(NSInvocation *)invocation{
    // 在調用目標方法前執行橫切功能
    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];
        }
    }

    // 調用目標方法
    [invocation invokeWithTarget:self.proxyTarget];

    // 在調用目標方法後執行橫切功能
    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];
        }
    }
}複製代碼

接着咱們定義一個代理協議

#import <Foundation/Foundation.h>

@protocol Invoker <NSObject>

@required
// 在調用對象中的方法前執行對功能的橫切
- (void)preInvoke:(NSInvocation *)inv withTarget:(id)target;
@optional
// 在調用對象中的方法後執行對功能的橫切
- (void)postInvoke:(NSInvocation *)inv withTarget:(id)target;

@end複製代碼

最後還須要一個遵照協議的類

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

@interface AuditingInvoker : NSObject<Invoker>//遵照Invoker協議
@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複製代碼

在這個遵循代理類裏面咱們只實現協議裏面的兩個方法。

寫出測試代碼

#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];

        // 設置代理中註冊的選擇器數組
        NSValue *selValue1 = [NSValue valueWithPointer:@selector(study:andRead:)];
        NSArray *selValues = @[selValue1];
        // 建立AuditingInvoker
        AuditingInvoker *invoker = [[AuditingInvoker alloc] init];
        // 建立Student對象的代理studentProxy
        id studentProxy = [[AspectProxy alloc] initWithObject:student selectors:selValues andInvoker:invoker];

        // 使用指定的選擇器向該代理髮送消息---例子1
        [studentProxy study:@"Computer" andRead:@"Algorithm"];

        // 使用還未註冊到代理中的其餘選擇器,向這個代理髮送消息!---例子2
        [studentProxy study:@"mathematics" :@"higher mathematics"];

        // 爲這個代理註冊一個選擇器並再次向其發送消息---例子3
        [studentProxy registerSelector:@selector(study::)];
        [studentProxy study:@"mathematics" :@"higher mathematics"];
    }
    return 0;
}複製代碼

這裏有3個例子。裏面會分別輸出什麼呢?

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中會輸出3句話。調用Student對象的代理中的study:andRead:方法,會使該代理調用AuditingInvoker對象中的preInvoker:方法、真正目標(Student對象)中的study:andRead:方法,以及AuditingInvoker對象中的postInvoker:方法。一個方法的調用,調用起了3個方法。緣由是study:andRead:方法是經過Student對象的代理註冊的;

例子2就只會輸出1句話。調用Student對象代理中的study::方法,由於該方法還未經過這個代理註冊,因此程序僅會將調用該方法的消息轉發給Student對象,而不會調用AuditorInvoker方法。

例子3又會輸出3句話了。由於study::經過這個代理進行了註冊,而後程序再次調用它,在此次調用過程當中,程序會調用AuditingInvoker對象中的AOP方法和真正目標(Student對象)中的study::方法。

這個例子就實現了一個簡單的AOP(Aspect Oriented Programming)面向切面編程。咱們把一切功能"切"出去,與其餘部分分開,這樣能夠提升程序的模塊化程度。AOP能解耦也能動態組裝,能夠經過預編譯方式和運行期動態代理實如今不修改源代碼的狀況下給程序動態統一添加功能。好比上面的例子三,咱們經過把方法註冊到動態代理類中,因而就實現了該類也能處理方法的功能。

五. 入院考試

下面的代碼會?Compile Error / Runtime Crash / NSLog…?

@interface NSObject (Sark)
 + (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;
}複製代碼

這道有兩處難點,難點一是給NSObject增長了一個分類,分類聲明的是一個加號的類方法,而實現中是一個減號的實例方法。在main中去NSObject去調用了這個foo方法,會編譯錯誤,仍是會Crash呢?

難點二是會輸出什麼內容呢?

先來看難點一,這裏會牽扯到Category的知識。推薦文章仍是美團的這篇經典的深刻理解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在初始化的時候,會去加載map_images,map_images最終會調用objc-runtime-new.mm裏面的_read_images方法。_read_images方法裏面會去初始化內存中的map, 這個時候將會load全部的類,協議還有Category。NSOBject的+load方法就是這個時候調用的。

// 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);
            }
        }
    }
}複製代碼

在這個加載中,for循環中會反覆調用_getObjc2CategoryList 方法,這個方法的具體實現是:

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

最後一個參數__objc_catlist就是編譯器剛剛生成的category數組。

加載完全部的category以後,就開始處理這些類別。大致思路仍是分爲2類來分開處理。

if (cat->instanceMethods || cat->protocols || cat->instanceProperties){
}複製代碼

第一類是實例方法

if (cat->classMethods || cat->protocols /* || cat->classProperties */) {
}複製代碼

第二類是類方法。

處理完以後的結果
1)、把category的實例方法、協議以及屬性添加到類上 2)、把category的類方法和協議添加到類的metaclass上

這兩種狀況裏面的處理方式都差很少,先去調用addUnattachedCategoryForClass函數,申請內存,分配空間。remethodizeClass這個方法裏面會調用attachCategories方法。

attachCategories方法代碼就不貼了,有興趣的能夠本身去看看。這個方法裏面會用頭插法,把新加的方法從頭插入方法鏈表中。而且最後還會flushCaches。

這也就是爲何咱們能夠在Category裏面覆蓋原有的方法的緣由,由於頭插法,新的方法在鏈表的前面,會優先被遍歷到。

以上就是Category加載時候的流程。

再回到這道題目上面來,在加載NSObject的Category中,在編譯期會提示咱們沒有實現+(void)foo的方法,由於在.m文件中並無找到+的方法,而是一個-號的方法,因此會提示。

可是在實際加載Category的時候,會把-(void)foo加載進去,因爲是實例方法,因此會放在NSObject的實例方法鏈表裏面。

根據第二章分析的objc_msgSend源碼實現,咱們能夠知道:

在調用[NSObject foo]的時候,會先在NSObject的meta-class中去查找foo方法的IMP,未找到,繼續在superClass中去查找,NSObject的meta-class的superClass就是自己NSObject,因而又回到NSObject的類方法中查找foo方法,因而乎找到了,執行foo方法,輸出

IMP: -[NSObject(Sark) foo]複製代碼

在調用[[NSObject new] foo]的時候,會先生成一個NSObject的對象,用這個NSObject實例對象再去調用foo方法的時候,會去NSObject的類方法裏面去查找,找到,因而也會輸出

IMP: -[NSObject(Sark) foo]複製代碼

因此上面這題,不會Compile Error ,更不會 Runtime Crash ,會輸出兩個相同的結果。

六. Runtime中的優化

關於Runtime系統中,有3種地方進行了優化。

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

在消息發送過程當中,查找IMP的過程,會優先查找緩存。這個緩存會存儲最近使用過的方法都緩存起來。這個cache和CPU裏面的cache的工做方式有點相似。原理是調用的方法有可能常常會被調用。若是沒有這個緩存,直接去類方法的方法鏈表裏面去查找,查詢效率實在過低。因此查找IMP會優先搜索飯方法緩存,若是沒有找到,接着會在虛函數表中尋找IMP。若是找到了,就會把這個IMP存儲到緩存中備用。

基於這個設計,使Runtime系統能可以執行快速高效的方法查詢操做。

2.虛函數表

虛函數表也稱爲分派表,是編程語言中經常使用的動態綁定支持機制。在OC的Runtime運行時系統庫實現了一種自定義的虛函數表分派機制。這個表是專門用來提升性能和靈活性的。這個虛函數表是用來存儲IMP類型的數組。每一個object-class都有這樣一個指向虛函數表的指針。

3.dyld共享緩存

在咱們的程序中,必定會有不少自定義類,而這些類中,不少SEL是重名的,好比alloc,init等等。Runtime系統須要爲每個方法給定一個SEL指針,而後爲每次調用個各個方法更新元數據,以獲取惟一值。這個過程是在應用程序啓動的時候完成。爲了提升這一部分的執行效率,Runtime會經過dyld共享緩存實現選擇器的惟一性。

dyld是一種系統服務,用於定位和加載動態庫。它含有共享緩存,可以使多個進程共用這些動態庫。dyld共享緩存中含有一個選擇器表,從而能使運行時系統可以經過使用緩存訪問共享庫和自定義類的選擇器。

關於dyld的知識能夠看看這篇文章dyld: Dynamic Linking On OS X

未完待續,請你們多多指教。

相關文章
相關標籤/搜索