用代碼理解ObjC中的發(fā)送消息和消息轉(zhuǎn)發(fā)

今天我們主要以看代碼寫代碼的形式聊聊消ObjC中的發(fā)送消息和消息轉(zhuǎn)發(fā)。
當(dāng)我們向一個(gè)對(duì)象(實(shí)例對(duì)象、類對(duì)象)發(fā)送一條消息時(shí),對(duì)象可能是處理不了的,結(jié)果就是程序發(fā)生crash。當(dāng)然,通過(guò)消息轉(zhuǎn)發(fā)可以預(yù)防crash。現(xiàn)在我們就帶著幾個(gè)困惑:消息發(fā)送和處理的機(jī)制是什么樣的?消息轉(zhuǎn)發(fā)執(zhí)行的時(shí)機(jī)和包含的步驟是什么樣的?(為什么實(shí)際步驟是2步而不是很多人認(rèn)為的3步)?消息轉(zhuǎn)發(fā)的一些細(xì)節(jié)是什么樣的?下面是我分析一些開源代碼并通過(guò)自己的代碼實(shí)踐,得出的自己的一些理解和心得。

 id null = [NSNull null];
 [null setObject:@2 forKey:@"2"];

 2017-12-08 10:40:34.678705+0800 test[8809:225907] -[NSNull setObject:forKey:]: 
unrecognized selector sent to instance 0x10bc2def0

嘗試?yán)斫忾_源代碼

發(fā)送消息
 void/id objc_msgSend(void /* id self, SEL op, ... */ )   //返回值為結(jié)構(gòu)體及浮點(diǎn)數(shù)時(shí)方法名有所不同_stret / _fpret
/*
 * Sends a message with a simple return value to an instance of a class.
 * 
 * @param self A pointer to the instance of the class that is to receive the message.
 * @param op The selector of the method that handles the message.
 * @param ... 
 *   A variable argument list containing the arguments to the method.
 * 
 * @return The return value of the method.

objc_msgSend的實(shí)現(xiàn)在objc-msg-x86.64.s文件中的匯編代碼如下:

id objc_msgSend(id self, SEL _cmd,...)
/********************************************************************
 *
 * id objc_msgSend(id self, SEL _cmd,...);
 *
 ********************************************************************/

    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

上文中的一些宏如下:

GetIsaFast
.macro GetIsaFast
.if $0 != STRET
    testb   $$1, %a1b
    PN
    jnz LGetIsaSlow_f
    movq    $$0x00007ffffffffff8, %r11
    andq    (%a1), %r11
.else
    testb   $$1, %a2b
    PN
    jnz LGetIsaSlow_f
    movq    $$0x00007ffffffffff8, %r11
    andq    (%a2), %r11
.endif
LGetIsaDone:    
.endmacro
NilTest
.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

CacheLookup
.macro  CacheLookup
.if $0 != STRET  &&  $0 != SUPER_STRET  &&  $0 != SUPER2_STRET
    movq    %a2, %r10       // r10 = _cmd
.else
    movq    %a3, %r10       // r10 = _cmd
.endif
    andl    24(%r11), %r10d     // r10 = _cmd & class->cache.mask
    shlq    $$4, %r10       // r10 = offset = (_cmd & mask)<<4
    addq    16(%r11), %r10      // r10 = class->cache.buckets + offset

.if $0 != STRET  &&  $0 != SUPER_STRET  &&  $0 != SUPER2_STRET
    cmpq    (%r10), %a2     // if (bucket->sel != _cmd)
.else
    cmpq    (%r10), %a3     // if (bucket->sel != _cmd)
.endif
    jne     1f          //     scan more
    // CacheHit must always be preceded by a not-taken `jne` instruction
    CacheHit $0         // call or return imp

1:
    // loop
    cmpq    $$1, (%r10)
    jbe 3f          // if (bucket->sel <= 1) wrap or miss

    addq    $$16, %r10      // bucket++
2:  
.if $0 != STRET  &&  $0 != SUPER_STRET  &&  $0 != SUPER2_STRET
    cmpq    (%r10), %a2     // if (bucket->sel != _cmd)
.else
    cmpq    (%r10), %a3     // if (bucket->sel != _cmd)
.endif
    jne     1b          //     scan more
    // CacheHit must always be preceded by a not-taken `jne` instruction
    CacheHit $0         // call or return imp

3:
    // wrap or miss
    jb  LCacheMiss_f        // if (bucket->sel < 1) cache miss
    // wrap
    movq    8(%r10), %r10       // bucket->imp is really first bucket
    jmp     2f

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

1:
    // loop
    cmpq    $$1, (%r10)
    jbe 3f          // if (bucket->sel <= 1) wrap or miss

    addq    $$16, %r10      // bucket++
2:  
.if $0 != STRET  &&  $0 != SUPER_STRET  &&  $0 != SUPER2_STRET
    cmpq    (%r10), %a2     // if (bucket->sel != _cmd)
.else
    cmpq    (%r10), %a3     // if (bucket->sel != _cmd)
.endif
    jne     1b          //     scan more
    // CacheHit must always be preceded by a not-taken `jne` instruction
    CacheHit $0         // call or return imp

3:
    // double wrap or miss
    jmp LCacheMiss_f

.endmacro
MethodTableLookup
.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

使用開源代碼里最底層的runtime api,可以把上述過(guò)程下盡可能的逐行寫成如下偽代碼,如下

id objc_msgSend(id self, SEL _cmd,...)
id objc_msgSend(id self, SEL _cmd,...) {
  ① if (!self) return nil; 
  ② Class cls = self->getIsa();
     IMP imp = nil;
  ③ imp = cache_getImp(cls, sel);   
     if (imp) return imp;  
  ④ imp = _class_lookupMethodAndLoadCache3(self, _cmd, cls); 
 return imp;
}

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{        
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}  // 跳過(guò)了“樂(lè)觀的”無(wú)鎖的查找cache過(guò)程

NilTest宏,判斷對(duì)象是否為nil,若為 nil,直接返回 nil。
GetIsaFast宏快速獲取到對(duì)象的 isa 指針地址(不同處理器架構(gòu)存放的位置不同)
CacheLookup_cache_getImp(Class cls, SEL sel)包含并調(diào)用了這塊代碼。嘗試尋找sel對(duì)應(yīng)的IMP,有可能返回_objc_msgForward_impcache(?下文會(huì)講到。
MethodTableLookup最終調(diào)用了lookUpImpOrForward方法,嘗試找method_array_t里所有method_list_t中的包含sel的method_t的IMP。有可能返回_objc_msgForward_impcache(?下文會(huì)講到)。
此外,我們可以猜測(cè)ObjC中IMP的定義為
typedef id (*IMP)(...)或者id (*IMP)(id object, SEL sel,...) (返回值也可能為結(jié)構(gòu)體或浮點(diǎn)數(shù))。

IMP lookUpImpOrForward(Class cls, SEL sel, id inst,vbool initialize, bool cache, bool resolver) 實(shí)現(xiàn)
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    Class curClass;
    IMP methodPC = nil;
    Method meth;
    bool triedResolver = NO;

    methodListLock.assertUnlocked();

    // Optimistic cache lookup
    if (cache) {
        methodPC = _cache_getImp(cls, sel);
        if (methodPC) return methodPC;    
    }

    // Check for freed class
    if (cls == _class_getFreedObjectClass())
        return (IMP) _freedHandler;

    // Check for +initialize
    if (initialize  &&  !cls->isInitialized()) {
        _class_initialize (_class_getNonMetaClass(cls, inst));
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }

    // The lock is held to make method-lookup + cache-fill atomic 
    // with respect to method addition. Otherwise, a category could 
    // be added but ignored indefinitely because the cache was re-filled 
    // with the old value after the cache flush on behalf of the category.
 retry:
    methodListLock.lock();

    // Ignore GC selectors
    if (ignoreSelector(sel)) {
        methodPC = _cache_addIgnoredEntry(cls, sel);
        goto done;
    }

    // Try this class's cache.

    methodPC = _cache_getImp(cls, sel);
    if (methodPC) goto done;

    // Try this class's method lists.

    meth = _class_getMethodNoSuper_nolock(cls, sel);
    if (meth) {
        log_and_fill_cache(cls, cls, meth, sel);
        methodPC = method_getImplementation(meth);
        goto done;
    }

    // Try superclass caches and method lists.

    curClass = cls;
    while ((curClass = curClass->superclass)) {
        // Superclass cache.
        meth = _cache_getMethod(curClass, sel, _objc_msgForward_impcache);
        if (meth) {
            if (meth != (Method)1) {
                // Found the method in a superclass. Cache it in this class.
                log_and_fill_cache(cls, curClass, meth, sel);
                methodPC = method_getImplementation(meth);
                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;
            }
        }

        // Superclass method list.
        meth = _class_getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            log_and_fill_cache(cls, curClass, meth, sel);
            methodPC = method_getImplementation(meth);
            goto done;
        }
    }

    // No implementation found. Try method resolver once.

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

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

    _cache_addForwardEntry(cls, sel);
    methodPC = _objc_msgForward_impcache;

 done:
    methodListLock.unlock();

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

    return methodPC;
}

通過(guò)關(guān)鍵點(diǎn)簡(jiǎn)述這個(gè)函數(shù)的查找過(guò)程.
執(zhí)行起點(diǎn)a
*起點(diǎn)a 方法列表加鎖(查詢讀取和動(dòng)態(tài)添加修改方法實(shí)現(xiàn)互斥),嘗試忽略GC sel

  1. cache_t中尋找sel對(duì)應(yīng)的IMP,如果找到,直接返回, 可能直接返回_objc_msgForward_impcache;
  2. 在所有方法列表中(自身,categorys)使用二分法或遍歷逐一尋找以name屬性值為sel的method_t(Method),如果找到,以sel為鍵把method存入cache_t, 直接執(zhí)行mehtod里的IMP;
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;
        }
    }

    return nil;
}
  1. 循環(huán)父類直到NSObject(父類為nil),通過(guò)_cache_getMethod方法(返回1,IMP或nil)在父類的cache_t尋找以sel為鍵的method_t, 如果此時(shí)method_t不為1(imp屬性為_objc_msgForward_impcache時(shí)method為1),證明父類有執(zhí)行該方法的記錄,加入自己的緩存,直接調(diào)用,若為1,停止尋找。然后在父類的所有方法列表里繼續(xù)尋找,如果找到IMP,加入自己的緩存并執(zhí)行。
  2. 如果沒(méi)有找到,嘗試調(diào)用自身的_class_resolveMethod動(dòng)態(tài)為類對(duì)象或元類對(duì)象里添加方法實(shí)現(xiàn)。如果成功添加了method,記錄已經(jīng)添加過(guò),重新從起點(diǎn)a出發(fā)執(zhí)行;
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);
        }
    }
}

如果這時(shí)候還沒(méi)找到sel對(duì)應(yīng)的IMP imp, 把_objc_msgForward_impcache當(dāng)做sel的實(shí)現(xiàn)一塊加入到緩存中,并返回_objc_msgForward_impcache。這也意味著,如果下次再收到該sel消息,將從緩存中直接返回_objc_msgForward_impcache

void _cache_addForwardEntry(Class cls, SEL sel) //藏
{
   cache_entry *smt;
 
   smt = (cache_entry *)malloc(sizeof(cache_entry));
   smt->name = sel;
   smt->imp = _objc_msgForward_impcache;
   if (! _cache_fill(cls, (Method)smt, sel)) {  // fixme hack
       // Entry not added to cache. Don't leak the method struct.
       free(smt);
   }
}

_objc_msgForward_impcache是什么?

上文中已經(jīng)可以看出,當(dāng)某種類型的對(duì)象第一次處理SEL sel消息過(guò)程中,無(wú)論如何也找不到對(duì)應(yīng)的IMP imp時(shí),便使得_objc_msgForward_impcache作為sel對(duì)應(yīng)的imp計(jì)入緩存(下一次直接從緩存中返回)并返回。沒(méi)錯(cuò),它就是消息轉(zhuǎn)發(fā)的函數(shù)指針,也就是說(shuō),無(wú)法順利找到該類sel對(duì)應(yīng)的實(shí)現(xiàn)imp時(shí),將執(zhí)行消息轉(zhuǎn)發(fā)對(duì)應(yīng)的imp。從上面也可以看出,嚴(yán)格意義上來(lái)講,_class_resolveMethod 因?yàn)椴⒉皇?code>_objc_msgForward_impcache觸發(fā)的,并不能算作消息轉(zhuǎn)發(fā)的后續(xù)步驟; 消息轉(zhuǎn)發(fā)后,該種對(duì)象/類對(duì)象再次處理到同名消息,將直接進(jìn)行消息轉(zhuǎn)發(fā)(從cache_t中拿到sel對(duì)應(yīng)的imp, 即_objc_msgForward_impcache)。

/********************************************************************
*
* id _objc_msgForward(id self, SEL _cmd,...);
*
* _objc_msgForward and _objc_msgForward_stret are the externally-callable
*   functions returned by things like method_getImplementation().
* _objc_msgForward_impcache is the function pointer actually stored in
*   method caches.
*
********************************************************************/

    .non_lazy_symbol_pointer
L_forward_handler:
    .indirect_symbol __objc_forward_handler
    .long 0
L_forward_stret_handler:
    .indirect_symbol __objc_forward_stret_handler
    .long 0

    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-struct return version

    call    1f
1:  popl    %edx
    movl    L_forward_handler-1b(%edx), %edx
    jmp *(%edx)

    END_ENTRY   __objc_msgForward


    ENTRY   __objc_msgForward_stret
    // Struct return version

    call    1f
1:  popl    %edx
    movl    L_forward_stret_handler-1b(%edx), %edx
    jmp *(%edx)

    END_ENTRY   __objc_msgForward_stret

從源碼中可以看出,_objc_msgForward_impcache 只是個(gè)內(nèi)部的函數(shù)指針,會(huì)根據(jù)根據(jù)此時(shí) CPU 的狀態(tài)寄存器的內(nèi)容來(lái)繼續(xù)執(zhí)行 _objc_msgForward或者_objc_msgForward_stret, 這兩個(gè)才是真正的調(diào)用的消息轉(zhuǎn)發(fā)的函數(shù);且,對(duì)應(yīng)的處理過(guò)程在_forward_handler或_forward_stret_handler里。在開源代碼里,我們找到了一個(gè)默認(rèn)的handler實(shí)現(xiàn)。貌似輸出了我們熟悉的 unrecognized selector sent to instance *,但真的會(huì)執(zhí)行這樣薄弱的東西嗎?

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

代碼實(shí)踐

以下是一段會(huì)發(fā)生crash的代碼;

- (void)viewDidLoad {
  [super viewDidLoad];
  id obj = [ASClassB new];
  
  [obj performSelector:@selector(exampleInvoke:) withObject:@"1"];
  //[obj performSelector:@selector(exampleInvoke:) withObject:@"1"];
}

我們?cè)谠撐恢么蛏蠑帱c(diǎn);

屏幕快照 2017-12-10 下午6.33.19.png

調(diào)試欄執(zhí)行call (void)instrumentObjcMessageSends(YES), 繼續(xù)

屏幕快照 2017-12-10 下午6.34.01.png

在private/tmp文件夾中找到msgSends開頭的文件,便知道所有的發(fā)送的消息和對(duì)象的日志。(下圖截取了一部分)
屏幕快照 2017-12-10 下午6.43.51.png

可以看到,通過(guò)performSelector:向ASClassA發(fā)送exampleInvoke:消息后,陸續(xù)調(diào)用了resolveInstanceMethod:``forwardingTargetForSelector:``methodSignatureForSelector:``class``doesNotRecognizeSelector:方法。

// Replaced by CF (throws an NSException)
- (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p", 
                object_getClassName(self), sel_getName(sel), self);
}

通過(guò)源碼發(fā)現(xiàn),是doesNotRecognizeSelector:拋出異常終止了程序并給出了提示!可以猜測(cè),實(shí)際上那個(gè)名字為default的handler并沒(méi)有執(zhí)行。那么如何驗(yàn)證上述消息轉(zhuǎn)發(fā)過(guò)程呢,很簡(jiǎn)單,我們可以寫一層層的簡(jiǎn)單的消息轉(zhuǎn)發(fā)來(lái)防止crash。

forwardingTargetForSelector:
#import "ASClassB.h"
#import "ASClassA.h"
#import <objc/runtime.h>

@implementation ASClassB

- (id)forwardingTargetForSelector:(SEL)aSelector {
  if (aSelector == @selector(exampleInvoke:)) {
    return [ASClassA new];
  }
  return [super forwardingTargetForSelector:aSelector];
}
@end

@implementation ASClassA

- (void)exampleInvoke:(NSString *)text {
  NSLog(@"ASClassA receive exampleIncoke:");
}
@end

我們重寫了ASClassB的forwardingTargetForSelector:方法,嘗試把消息轉(zhuǎn)發(fā)給實(shí)際上已經(jīng)實(shí)現(xiàn)了exampleInvoke:的ASClass類的一個(gè)對(duì)象。和上文調(diào)試步驟一樣,我們對(duì)objA執(zhí)行兩次方法。

屏幕快照 2017-12-10 下午7.12.32.png

執(zhí)行結(jié)果:
屏幕快照 2017-12-10 下午7.18.19.png

第一次:

- ASClassB NSObject performSelector:withObject:
+ ASClassB NSObject resolveInstanceMethod:
+ ASClassB NSObject resolveInstanceMethod:
- ASClassB ASClassB forwardingTargetForSelector:
- ASClassB ASClassB forwardingTargetForSelector:
+ ASClassA NSObject initialize
+ ASClassA NSObject new
- ASClassA NSObject init
- ASClassA ASClassA exampleInvoke:

第二次:

- ASClassB NSObject performSelector:withObject:
- ASClassB ASClassB forwardingTargetForSelector:
- ASClassB ASClassB forwardingTargetForSelector:
+ ASClassA NSObject new
- ASClassA NSObject init
- ASClassA ASClassA exampleInvoke:

可以發(fā)現(xiàn),第一點(diǎn),沒(méi)有執(zhí)行methodSignatureForSelector:方法,因?yàn)?code>forwardingTargetForSelector:方法已經(jīng)返回了能正確處理消息的對(duì)象;第二點(diǎn),obj第二次收到exampleInvoke:消息時(shí),直接進(jìn)行進(jìn)行了消息轉(zhuǎn)發(fā)。原因正是上文中提到的首次未找到sel對(duì)應(yīng)的imp時(shí),直接把消息轉(zhuǎn)發(fā)的imp和sel一塊放在了類對(duì)象/元對(duì)象的cache_t中。

methodSignatureForSelector: & forwardInvocation:

實(shí)測(cè),在未重寫forwardingTargetForSelector:或該方法提供對(duì)象不能處理該消息時(shí)(返回nil無(wú)效),便會(huì)陸續(xù)執(zhí)行methodSignatureForSelector:forwardInvocation:方法。

#import "ASClassB.h"
#import "ASClassA.h"
#import <objc/runtime.h>

@implementation ASClassB

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
  if (aSelector == @selector(exampleInvoke:)) {
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
  }
  return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
  if (anInvocation.selector == @selector(exampleInvoke:)) {
     [anInvocation invokeWithTarget:[ASClassA new]];
  } else {
    [super forwardInvocation:anInvocation];
  }
}anInvocation invokeWithTarget:[ASClassA new]];
}
@end

這個(gè)簡(jiǎn)單的demo可以實(shí)現(xiàn)正確的消息轉(zhuǎn)發(fā)。通過(guò)重寫methodSignatureForSelector:方法返回一個(gè)可用的方法簽名,通過(guò)forwardInvocation:將incovation(后面介紹)完成一個(gè)完整的發(fā)送消息過(guò)程。我們甚至可以重寫這兩個(gè)方法完成所有未知消息的消息轉(zhuǎn)發(fā), 不再crash。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
  [anInvocation invokeWithTarget:nil];
#if DEBUG
  NSLog(@"[%@ %@] unrecognized selector sent to instance %@", self.class, NSStringFromSelector(anInvocation.selector), self);
  [NSException raise:@"UnrecognizedSelector" format:@"[%@ %@] unrecognized selector sent to instance %@", self.class, NSStringFromSelector(anInvocation.selector), self];
#endif
}

后來(lái)我們也看到了forwardInvocation:的調(diào)用過(guò)程

- ASClassB ASClassB forwardInvocation:
+ NSInvocation NSInvocation _invocationWithMethodSignature:frame:
+ NSInvocation NSObject alloc
- NSMethodSignature NSObject retain
- NSMethodSignature NSMethodSignature frameLength
- NSMethodSignature NSMethodSignature _frameDescriptor
- NSMethodSignature NSMethodSignature frameLength
- NSMethodSignature NSMethodSignature _frameDescriptor
- NSInvocation NSObject autorelease
- ASClassB ASClassB forwardInvocation:
- NSInvocation NSInvocation invokeWithTarget:
- NSInvocation NSInvocation setArgument:atIndex:
- NSMethodSignature NSMethodSignature numberOfArguments
- NSMethodSignature NSMethodSignature _frameDescriptor
- NSMethodSignature NSMethodSignature _argInfo:
- NSMethodSignature NSMethodSignature _frameDescriptor
- NSInvocation NSInvocation invoke

提到幾個(gè)點(diǎn),invokeWithTarget:在這里,是可以轉(zhuǎn)發(fā)給nil的,畢竟nil收到任何消息后會(huì)直接返回nil。然后注意到,在這里的invocation調(diào)用過(guò)程,此處的methodSignaturetypes只需設(shè)成"v@:"或"v@"(如果不取SEL),相當(dāng)于`- (id)m;只要不在anInvocation里取和設(shè)方法參數(shù),并不會(huì)發(fā)生數(shù)組越界,也不會(huì)影響多個(gè)變量傳遞給新的target,系統(tǒng)執(zhí)行時(shí)應(yīng)該把參數(shù)放置在了一個(gè)更高效的位置,incocation取時(shí)也只相當(dāng)于一個(gè)懶加載的getter; 另外,NSNull+NullSafe擴(kuò)展采用了遍歷所有類來(lái)尋找能響應(yīng)未知消息的類對(duì)象來(lái)轉(zhuǎn)發(fā)消息,并做了緩存優(yōu)化。

簡(jiǎn)單講下 NSMethodSignature & NSInvocation

NSMethodSignature

A record of the type information for the return value and parameters of a method. 官方文檔定義:一個(gè)對(duì)于方法返回值和參數(shù)的記錄。

Method m = class_getInstanceMethod(NSString.class, @selector(initWithFormat:));
const char *c = method_getTypeEncoding(m);
NSMethodSignature* sg = [[NSString new] methodSignatureForSelector:@selector(initWithFormat:)];

輸出c和m, 得到:

(lldb) po c
"@24@0:8@16"

(lldb) po sg
<NSMethodSignature: 0x600000273880>
    number of arguments = 3
    frame size = 224
    is special struct return? NO
    return value: -------- -------- -------- --------
        type encoding (@) '@'
        flags {isObject}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 0: -------- -------- -------- --------
        type encoding (@) '@'
        flags {isObject}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 1: -------- -------- -------- --------
        type encoding (:) ':'
        flags {}
        modifiers {}
        frame {offset = 8, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 2: -------- -------- -------- --------
        type encoding (@) '@'
        flags {isObject}
        modifiers {}
        frame {offset = 16, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}

c = "@24@0:8@16", 數(shù)字代表著相對(duì)于地址的偏移量,由下邊的sg可以看出,第一位@代表返回值(實(shí)際是argument -1),第二位 argument 0是id self, argument 1是SEL sel, argument 2是id arg。為什么會(huì)這樣,我們接下來(lái)會(huì)驗(yàn)證,這仿佛又與id objc_msgSend(id self, SEL op, ... */ )的參數(shù)順序是一致的...可以認(rèn)為方法簽名就是個(gè)方法的模板記錄。關(guān)于type encoding,有以下資料:

#define _C_ID       '@'
#define _C_CLASS    '#'
#define _C_SEL      ':'
#define _C_CHR      'c'
#define _C_UCHR     'C'
#define _C_SHT      's'
#define _C_USHT     'S'
#define _C_INT      'i'
#define _C_UINT     'I'
#define _C_LNG      'l'
#define _C_ULNG     'L'
#define _C_LNG_LNG  'q'
#define _C_ULNG_LNG 'Q'
#define _C_FLT      'f'
#define _C_DBL      'd'
#define _C_BFLD     'b'
#define _C_BOOL     'B'
#define _C_VOID     'v'
#define _C_UNDEF    '?'
#define _C_PTR      '^'
#define _C_CHARPTR  '*'
#define _C_ATOM     '%'
#define _C_ARY_B    '['
#define _C_ARY_E    ']'
#define _C_UNION_B  '('
#define _C_UNION_E  ')'
#define _C_STRUCT_B '{'
#define _C_STRUCT_E '}'
#define _C_VECTOR   '!'
#define _C_CONST    'r'

總之這些不同字符代表不同類型啦。例如':'代表SEL,證明了argument 1確實(shí)是sel,@代表'id'等。例如-(BOOL)isKindOfClass:(Class)cls;的type encoding為"B@:#"。

NSInvocation。

An Objective-C message rendered as an object.
呈現(xiàn)為對(duì)象的消息,可以存儲(chǔ)消息的所有配置和直接調(diào)用給任意對(duì)象(真tm是萬(wàn)物皆對(duì)象啊)。
輸出上文中得到的anInvocation:


//type: @v:@

id obj = [ASClassB new];
[obj performSelector:@selector(exampleInvoke:) withObject:@"1"];

----------------------------------------
id x;
id y;
id z;
[anInvocation getArgument:&x atIndex:0];
[anInvocation getArgument:&y atIndex:1];
[anInvocation getArgument:&z atIndex:2];
---------------------------------------- 

(lldb) po anInvocation
<NSInvocation: 0x604000460780>
return value: {v} void
target: {@} 0x6040000036e0
selector: {:} exampleInvoke:
argument 2: {@} 0x10e8ec340

(lldb) po x
<ASClassB: 0x60400000eb10>

(lldb) po anInvocation.selector
"exampleInvoke:"

(lldb) po NSStringFromSelector(y)
exampleInvoke:

(lldb) po z
1

(lldb) po anInvocation.methodSignature
<NSMethodSignature: 0x604000464c40>
    number of arguments = 3
    frame size = 224
    is special struct return? NO
    return value: -------- -------- -------- --------
        type encoding (v) 'v'
        flags {}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0}
        memory {offset = 0, size = 0}
    argument 0: -------- -------- -------- --------
        type encoding (@) '@'
        flags {isObject}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 1: -------- -------- -------- --------
        type encoding (:) ':'
        flags {}
        modifiers {}
        frame {offset = 8, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 2: -------- -------- -------- --------
        type encoding (@) '@'
        flags {isObject}
        modifiers {}
        frame {offset = 16, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}

由此可以看出上文描述方法簽名前幾位位置代表的意義是完全正確的。
此外我們也可以自己手動(dòng)構(gòu)建invocation,實(shí)現(xiàn)多參數(shù)方法的動(dòng)態(tài)執(zhí)行。總之這個(gè)類很強(qiáng)大,后續(xù)文章我們還會(huì)提到。

NSString *text = @"string";
SEL sel = @selector(stringByAppendingString:);
NSMethodSignature *sg = [text methodSignatureForSelector:sel];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sg];
invocation.target = text;
invocation.selector = sel;
id p = @"SS";
[invocation setArgument:&p atIndex:2];
id r;
[invocation invoke];
if (sg.methodReturnLength) {
  [invocation getReturnValue:&r];
}
-----------------------------------------------
(lldb) po r
stringSS

(lldb) 

和上面分析的一樣,方法的參數(shù)index從2開始。

嘗試手動(dòng)觸發(fā)消息轉(zhuǎn)發(fā)

前面我們已經(jīng)知道,如果method的imp為__objc_msgForward, 將直接觸發(fā)消息轉(zhuǎn)發(fā)。
下面我們直接替換ASClassA的@selector(print)的實(shí)現(xiàn)為__objc_msgForward,然后替換該類@selector(forwardInvocation:)對(duì)應(yīng)的imp為我們自己實(shí)現(xiàn)的函數(shù)。

@implementation ASClassA
- (void)print {
  NSLog(@"ASClassA print");
}
void forward(id obj, SEL sel, NSInvocation *invo) {
  if (invo.selector == @selector(print)) {
    NSLog(@"hahhahahahhaha");
  }
}

- (void)viewDidLoad {
  [super viewDidLoad];
  class_replaceMethod(ASClassA.class, @selector(print), _objc_msgForward, "v@:");
  
  class_replaceMethod(ASClassA.class, @selector(forwardInvocation:), (IMP)forward,"v@:@");
  ASClassA *obj = [ASClassA new];
  [obj performSelector:@selector(print)];
}

結(jié)果為:

(lldb) call (void)instrumentObjcMessageSends(YES)
2017-12-10 23:20:47.625463+0800 test[12136:765892] hahhahahahhaha
(lldb) 

執(zhí)行過(guò)程為:

 ASClassA NSObject performSelector:
- ASClassA ASClassA print
- ASClassA NSObject forwardingTargetForSelector:
- ASClassA NSObject forwardingTargetForSelector:
- ASClassA NSObject methodSignatureForSelector:
- ASClassA NSObject methodSignatureForSelector:
...
- ASClassA ASClassA forwardInvocation:

print方法直接跳到了我們的自定義函數(shù)代碼實(shí)現(xiàn)上,消息轉(zhuǎn)發(fā)成功。上述只是一個(gè)簡(jiǎn)單的例子,如果自定義的函數(shù)里根據(jù)每個(gè)invocation的SEL名字動(dòng)態(tài)化新建一個(gè)包含完整代碼完全不同的invocation,功能將會(huì)異常強(qiáng)大。實(shí)際上JSPatch的某些核心部分也正是使用了這種方式直接替換掉某些類里的方法實(shí)現(xiàn)。

謝謝觀看!!如有問(wèn)題請(qǐng)多指教!!

參考文獻(xiàn)

https://github.com/RetVal/objc-runtime
https://github.com/opensource-apple/objc4
https://developer.apple.com/documentation
可以參考的反編譯代碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容