深刻淺出 Runtime(三):消息機制

Runtime 系列文章

深刻淺出 Runtime(一):初識
深刻淺出 Runtime(二):數據結構
深刻淺出 Runtime(三):消息機制
深刻淺出 Runtime(四):super 的本質
深刻淺出 Runtime(五):具體應用
深刻淺出 Runtime(六):相關面試題面試

1. objc_msgSend 方法調用流程

  • OC中的方法調用,其實都是轉換爲objc_msgSend()函數的調用(不包括[super message])。
void objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
複製代碼
  • receiver(方法調用者/消息接收者)發送一條消息(SEL方法名)
    參數 1 : receiver
    參數 2 : SEL
    參數 三、四、5... : SEL方法的參數
  • objc_msgSend()的執行流程能夠分爲 3 大階段:
    消息發送
    動態方法解析
    消息轉發

 

2. 消息發送

「消息發送」流程

消息發送流程

源碼分析

在前面的文章說過,Runtime 是一個用C、彙編編寫的運行時庫, 在底層彙編裏面若是須要調用 C 函數的話,蘋果會爲其加一個下劃線_, 因此查看objc_msgSend函數的實現,須要搜索_objc_msgSend(objc-msg-arm64.s(objc4))。編程

// objc-msg-arm64.s(objc4)
    /* _objc_msgSend 函數實現 */
    // ⚠️彙編程序入口格式爲:ENTRY + 函數名
	ENTRY _objc_msgSend

    // ⚠️若是 receiver 爲 nil 或者 tagged pointer,執行 LNilOrTagged,不然繼續往下執行
	cmp	x0, #0 // nil check and tagged pointer check
	b.le	LNilOrTagged   

    // ⚠️經過 isa 找到 class/meta-class
	ldr	x13, [x0]		// x13 = isa
	and	x16, x13, #ISA_MASK // x16 = class 
LGetIsaDone:
    // ⚠️進入 cache 緩存查找,傳的參數爲 NORMAL
    // CacheLookup 宏,用於在緩存中查找 SEL 對應方法實現
	CacheLookup NORMAL		// calls imp or objc_msgSend_uncached 

LNilOrTagged:
    // ⚠️若是 receiver 爲 nil,執行 LReturnZero,結束 objc_msgSend
	b.eq	LReturnZero		// nil check 
    // ⚠️若是 receiver 爲 tagged pointer,則執行其它
    ......
	b	LGetIsaDone

LReturnZero:
    ret  // 返回

    // ⚠️彙編中,函數的結束格式爲:ENTRY + 函數名
	END_ENTRY _objc_msgSend



.macro CacheLookup
    // ⚠️根據 SEL 去哈希表 buckets 中查找方法
	// x1 = SEL, x16 = isa
	ldp	x10, x11, [x16, #CACHE] // x10 = buckets, x11 = occupied|mask
	and	w12, w1, w11		// x12 = _cmd & mask
	add	x12, x10, x12, LSL #4 // x12 = buckets + ((_cmd & mask)<<4) ldp x9, x17, [x12] // {x9, x17} = *bucket // ⚠️緩存命中,進行 CacheHit 操做 1: cmp x9, x1 // if (bucket->sel != _cmd)
	b.ne	2f			// scan more
	CacheHit $0			// call or return imp
    // ⚠️緩存中沒有找到,進行 CheckMiss 操做
2:	// not hit: x12 = not-hit bucket
	CheckMiss $0			// miss if bucket->sel == 0
	cmp	x12, x10		// wrap if bucket == buckets
	b.eq	3f
	ldp	x9, x17, [x12, #-16]! // {x9, x17} = *--bucket
	b	1b			// loop
3:	// wrap: x12 = first bucket, w11 = mask
	add	x12, x12, w11, UXTW #4 // x12 = buckets+(mask<<4) // Clone scanning loop to miss instead of hang when cache is corrupt. // The slow path may detect any corruption and halt later. ldp x9, x17, [x12] // {x9, x17} = *bucket 1: cmp x9, x1 // if (bucket->sel != _cmd)
	b.ne	2f			// scan more
	CacheHit $0			// call or return imp 
2:	// not hit: x12 = not-hit bucket
	CheckMiss $0			// miss if bucket->sel == 0
	cmp	x12, x10		// wrap if bucket == buckets
	b.eq	3f
	ldp	x9, x17, [x12, #-16]! // {x9, x17} = *--bucket
	b	1b			// loop
3:	// double wrap
	JumpMiss $0	
.endmacro


// CacheLookup NORMAL|GETIMP|LOOKUP
#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2
.macro CacheHit
.if $0 == NORMAL  // ⚠️CacheLookup 傳的參數是 NORMAL
	MESSENGER_END_FAST
	br	x17			// call imp // ⚠️執行函數
.elseif $0 == GETIMP
	mov	x0, x17			// return imp
	ret
.elseif $0 == LOOKUP
	ret				// return imp via x17
.else
.abort oops
.endif
.endmacro


.macro CheckMiss
	// miss if bucket->sel == 0
.if $0 == GETIMP
	cbz	x9, LGetImpMiss
.elseif $0 == NORMAL  // ⚠️CacheLookup 傳的參數是 NORMAL
	cbz	x9, __objc_msgSend_uncached  // ⚠️執行 __objc_msgSend_uncached
.elseif $0 == LOOKUP
	cbz	x9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro


.macro JumpMiss
.if $0 == GETIMP
	b	LGetImpMiss
.elseif $0 == NORMAL
	b	__objc_msgSend_uncached
.elseif $0 == LOOKUP
	b	__objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro



    // ⚠️__objc_msgSend_uncached
    // ⚠️緩存中沒有找到方法的實現,接下來去 MethodTableLookup 類的方法列表中查找
	STATIC_ENTRY __objc_msgSend_uncached
	MethodTableLookup NORMAL
	END_ENTRY __objc_msgSend_uncached

.macro MethodTableLookup
	blx	__class_lookupMethodAndLoadCache3  // ⚠️執行C函數 _class_lookupMethodAndLoadCache3
.endmacro
複製代碼

相反,經過彙編中函數名找對應 C 函數實現時,須要去掉一個下劃線_。數組

// objc-runtime-new.mm(objc4)
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    // ⚠️注意傳參,因爲以前已經經過彙編去緩存中查找方法,因此這裏不會再次到緩存中查找
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;  // triedResolver 標記用於 動態方法解析

    runtimeLock.assertUnlocked();

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

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search 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.

    runtimeLock.read();

    if (!cls->isRealized()) {  // ⚠️若是 receiverClass(消息接受者類) 還未實現,就進行 realize 操做
        // Drop the read-lock and acquire the write-lock.
        // realizeClass() checks isRealized() again to prevent
        // a race while the lock is down.
        runtimeLock.unlockRead();
        runtimeLock.write();

        realizeClass(cls);

        runtimeLock.unlockWrite();
        runtimeLock.read();
    }

    // ⚠️若是 receiverClass 須要初始化且還未初始化,就進行初始化操做
    // 這裏插入一個 +initialize 方法的知識點
    // 調用 _class_initialize(cls),該函數中會遞歸遍歷父類,判斷父類是否存在且還未初始化 _class_initialize(cls->superclass)
    // 調用 callInitialize(cls) ,給 cls 發送一條 initialize 消息((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize)
    // 因此 +initialize 方法會在類第一次接收到消息時調用
    // 調用方式:objc_msgSend()
    // 調用順序:先調用父類的 +initialize,再調用子類的 +initialize (先初始化父類,再初始化子類,每一個類只會初始化1次)
    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
        // 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
    }


// ⚠️⚠️⚠️核心 
 retry:    
    runtimeLock.assertReading();

    // ⚠️去 receiverClass 的 cache 中查找方法,若是找到 imp 就直接調用
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // ⚠️去 receiverClass 的 class_rw_t 中的方法列表查找方法,若是找到 imp 就調用並將該方法緩存到 receiverClass 的 cache 中
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);  // ⚠️去目標類的方法列表中查找方法實現
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);  // ⚠️緩存方法
            imp = meth->imp;
            goto done;
        }
    }

    // ⚠️逐級查找父類的緩存和方法列表,若是找到 imp 就調用並將該方法緩存到 receiverClass 的 cache 中
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // Superclass cache.
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // Found the method in a superclass. Cache it in this class.
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    // 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.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }


    // ⚠️進入「動態方法解析」階段
    // No implementation found. Try method resolver once.
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // 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;
    }


    // ⚠️進入「消息轉發」階段
    // No implementation found, and method resolver didn't help. 
    // Use forwarding.
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);


 done:
    runtimeLock.unlockRead();

    return imp;
}
複製代碼

咱們來看一下getMethodNoSuper_nolock(cls, sel)是怎麼從類中查找方法實現的緩存

  • 若是方法列表是通過排序的,則進行二分查找;
  • 若是方法列表沒有進行排序,則進行線性遍歷查找。
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)
    {
    // ⚠️核心函數 search_method_list()
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }
    return nil;
}


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


static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    assert(list);

    const method_t * const first = &list->first;
    const method_t *base = first;
    const method_t *probe;
    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    // ⚠️count >>= 1 二分查找
    for (count = list->count; count != 0; count >>= 1) {
        probe = base + (count >> 1);
        
        uintptr_t probeValue = (uintptr_t)probe->name;
        
        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                probe--;
            }
            return (method_t *)probe;
        }
        
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}
複製代碼

咱們來看一下log_and_fill_cache(cls, meth->imp, sel, inst, cls)是怎麼緩存方法的bash

/*********************************************************************** * log_and_fill_cache * Log this method call. If the logger permits it, fill the method cache. * cls is the method whose cache should be filled. * implementer is the class that owns the implementation in question. **********************************************************************/
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (objcMsgLogEnabled) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cache_fill (cls, sel, imp, receiver);
}

#if TARGET_OS_WIN32 || TARGET_OS_EMBEDDED
# define SUPPORT_MESSAGE_LOGGING 0
#else
# define SUPPORT_MESSAGE_LOGGING 1
#endif
複製代碼

cache_fill()函數實現已經在上一篇文章中寫到。
關於緩存查找流程和更多cache_t的知識,能夠查看:
深刻淺出 Runtime(二):數據結構數據結構

 

2. 動態方法解析

「動態方法解析」流程

動態方法解析流程

  • 若是「消息發送」階段未找到方法的實現,進行一次「動態方法解析」;
  • 「動態方法解析」後,會再次進入「消息發送」流程, 從「去 receiverClass 的 cache 中查找方法」這一步開始執行。
  • 咱們能夠根據方法類型(實例方法 or 類方法)重寫如下方法
    +(BOOL)resolveInstanceMethod:(SEL)sel
    +(BOOL)resolveClassMethod:(SEL)sel
    在方法中調用如下方法來動態添加方法的實現
    BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
  • 示例代碼以下,咱們分別調用了 HTPerson 的eat實例方法和類方法,而 HTPerson.m 文件中並無這兩個方法的對應實現,咱們爲這兩個方法動態添加了實現,輸出結果以下。
// main.m
#import <Foundation/Foundation.h>
#import "HTPerson.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [[HTPerson new] eat];
        [HTPerson eat];   
    }
    return 0;
}
@end

// HTPerson.h
#import <Foundation/Foundation.h>
@interface HTPerson : NSObject
- (void)eat;  // 沒有對應實現
- (void)sleep;
+ (void)eat;  // 沒有對應實現
+ (void)sleep;
@end

// HTPerson.m
#import "HTPerson.h"
#import <objc/runtime.h>
@implementation HTPerson
- (void)sleep
{
    NSLog(@"%s",__func__);
}
+ (void)sleep
{
    NSLog(@"%s",__func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(eat)) {
        
        // 獲取其它方法, Method 就是指向 method_t 結構體的指針
        Method method = class_getInstanceMethod(self, @selector(sleep));
        /* 參數1:給哪一個類添加 參數2:給哪一個方法添加 參數3:方法的實現地址 參數4:方法的編碼類型 */
        class_addMethod(self,  // 實例方法存放在類對象中,因此這裏要傳入類對象
                        sel,
                        method_getImplementation(method),
                        method_getTypeEncoding(method)
                        );
        // 返回 YES 表明有動態添加方法實現
        // 從源碼來看,該返回值只是用來打印解析結果相關信息,並不影響動態方法解析的結果
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

+ (BOOL)resolveClassMethod:(SEL)sel
{
    if (sel == @selector(eat)) {
        
        Method method = class_getClassMethod(object_getClass(self), @selector(sleep));

        class_addMethod(object_getClass(self),  // 類方法存放在元類對象中,因此這裏要傳入元類對象
                        sel,
                        method_getImplementation(method),
                        method_getTypeEncoding(method)
                        );
        return YES;
    }
    return [super resolveClassMethod:sel];
}

@end

複製代碼

-[HTPerson sleep]
+[HTPerson sleep]app

源碼分析

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

 retry:    
    ......

    // ⚠️若是「消息發送」階段未找到方法的實現,進行一次「動態方法解析」
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);  // ⚠️核心函數
        runtimeLock.read();
        // 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;  // ⚠️標記triedResolver爲YES
        goto retry;  // ⚠️再次進入消息發送,從「去 receiverClass 的 cache 中查找方法」這一步開始
    }

    // ⚠️進入「消息轉發」階段
    ......
}

// objc-class.mm(objc4)
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    // ⚠️判斷是 class 對象仍是 meta-class 對象
    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);
        }
    }
}

/*********************************************************************** * _class_resolveInstanceMethod * Call +resolveInstanceMethod, looking for a method to be added to class cls. * cls may be a metaclass or a non-meta class. * Does not check if the method already exists. **********************************************************************/
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    // ⚠️查看 receiverClass 的 meta-class 對象的方法列表裏面是否有 SEL_resolveInstanceMethod 函數 imp
    // ⚠️也就是看咱們是否實現了 +(BOOL)resolveInstanceMethod:(SEL)sel 方法
    // ⚠️這裏必定會找到該方法實現,由於 NSObject 中有實現
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // ⚠️若是沒找到,說明程序異常,直接返回
        // Resolver not implemented.
        return;
    }

    // ⚠️若是找到了,經過 objc_msgSend 給對象發送一條 SEL_resolveInstanceMethod 消息
    // ⚠️即調用一下 +(BOOL)resolveInstanceMethod:(SEL)sel 方法
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // ⚠️下面是解析結果的一些打印信息
    ......
}

/*********************************************************************** * _class_resolveClassMethod * Call +resolveClassMethod, looking for a method to be added to class cls. * cls should be a metaclass. * Does not check if the method already exists. **********************************************************************/
static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    assert(cls->isMetaClass());
    // ⚠️查看 receiverClass 的 meta-class 對象的方法列表裏面是否有 SEL_resolveClassMethod 函數 imp
    // ⚠️也就是看咱們是否實現了 +(BOOL)resolveClassMethod:(SEL)sel 方法
    // ⚠️這裏必定會找到該方法實現,由於 NSObject 中有實現
    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // ⚠️若是沒找到,說明程序異常,直接返回
        // Resolver not implemented.
        return;
    }
    // ⚠️若是找到了,經過 objc_msgSend 給對象發送一條 SEL_resolveClassMethod 消息
    // ⚠️即調用一下 +(BOOL)resolveClassMethod:(SEL)sel 方法
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(_class_getNonMetaClass(cls, inst),   // 該函數返回值是類對象,而非元類對象
                        SEL_resolveClassMethod, sel);

    // ⚠️下面是解析結果的一些打印信息
    ......
}
複製代碼

 

3. 消息轉發

「消息轉發」流程

消息轉發流程

  • 若是「消息發送」階段未找到方法的實現,且經過「動態方法解析」沒有解決, 就進入「消息轉發」階段;
  • 「消息轉發」階段分兩步進行:Fast forwarding 和 Normal forwarding,顧名思義,第一步速度要比第二步快;
  • Fast forwarding:將消息轉發給一個其它 OC 對象(找一個備用接收者), 咱們能夠重寫如下方法,返回一個!= receiver的對象,來完成這一步驟; +/- (id)forwardingTargetForSelector:(SEL)sel
  • Normal forwarding:實現一個完整的消息轉發過程, 若是上一步沒能解決未知消息,能夠重寫如下兩個方法啓動完整的消息轉發。

    ① 第一個方法:咱們須要在該方法中返回一個適合該未知消息的方法簽名(方法簽名就是對返回值類型、參數類型的描述,可使用 Type Encodings 編碼,關於 Type Encodings 能夠閱讀個人上一篇 blog 深刻淺出 Runtime(二):數據結構)。
    +/- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    Runtime 會根據這個方法簽名,建立一個NSInvocation對象(NSInvocation封裝了未知消息的所有內容,包括:方法調用者 target、方法名 selector、方法參數 argument 等),而後調用第二個方法並將該NSInvocation對象做爲參數傳入。

    ② 第二個方法:咱們能夠在該方法中:將未知消息轉發給其它對象;改變未知消息的內容(如方法名、方法參數)再轉發給其它對象;甚至能夠定義任何邏輯。
    +/- (void)forwardInvocation:(NSInvocation *)invocation
    若是第一個方法中沒有返回方法簽名,或者咱們沒有重寫第二個方法,系統就會認爲咱們完全不想處理這個消息了,這時候就會調用+/- (void)doesNotRecognizeSelector:(SEL)sel方法並拋出經典的 crash:unrecognized selector sent to instance/class,結束 objc_msgSend 的所有流程。

  • 下面咱們來看一下這幾個代碼的默認實現
+ (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    _objc_fatal("+[NSObject methodSignatureForSelector:] "
                "not available without CoreFoundation");
}
+ (void)forwardInvocation:(NSInvocation *)invocation {
    [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}
+ (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("+[%s %s]: unrecognized selector sent to instance %p", 
                class_getName(self), sel_getName(sel), self);
}
複製代碼
  • Fast forwarding 示例代碼以下:
    咱們調用了 HTPerson 的eat實例方法,而 HTPerson.m 文件中並無該方法的對應實現,HTDog.m 中有同名方法的實現,咱們將消息轉發給 HTDog 的實例對象,輸出結果以下。
// main.m
#import <Foundation/Foundation.h>
#import "HTPerson.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [[HTPerson new] eat];        
    }
    return 0;
}
@end

// HTPerson.h
#import <Foundation/Foundation.h>
@interface HTPerson : NSObject
- (void)eat;  // 沒有對應實現
@end

// HTPerson.m
#import "HTPerson.h"
#import "HTDog.h"
@implementation HTPerson
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(eat)) {
        return [HTDog new];  // 將 eat 消息轉發給 HTDog 的實例對象
// return [HTDog class]; // 還能夠將 eat 消息轉發給 HTDog 的類對象
    }
    return [super forwardingTargetForSelector:aSelector];
}

@end

// HTDog.m
#import "HTDog.h"
@implementation HTDog
- (void)eat
{
    NSLog(@"%s",__func__);
}
+ (void)eat
{
    NSLog(@"%s",__func__);
}
@end
複製代碼

-[HTDog eat]框架

  • Normal forwarding 示例代碼及輸出結果以下:
// HTPerson.m
#import "HTPerson.h"
#import "HTDog.h"

@implementation HTPerson

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(eat)) {
        
        return [[HTDog new] methodSignatureForSelector:aSelector];
        //return [NSMethodSignature signatureWithObjCTypes:"v@i"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    // 將未知消息轉發給其它對象
    [anInvocation invokeWithTarget:[HTDog new]];
    
    // 改變未知消息的內容(如方法名、方法參數)再轉發給其它對象
    /* anInvocation.selector = @selector(sleep); anInvocation.target = [HTDog new]; int age; [anInvocation getArgument:&age atIndex:2]; // 參數順序:target、selector、other arguments [anInvocation setArgument:&age atIndex:2]; // 參數的個數由上個方法返回的方法簽名決定,要注意數組越界問題 [anInvocation invoke]; int ret; [anInvocation getReturnValue:&age]; // 獲取返回值 */
    
    // 定義任何邏輯,如:只打印一句話
    /* NSLog(@"好好學習"); */
}

@end
複製代碼

-[HTDog eat]ide

源碼分析

// objc-runtime-new.mm(objc4)
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    ......

    // ⚠️若是「消息發送」階段未找到方法的實現,且經過「動態方法解析」沒有解決
    // ⚠️進入「消息轉發」階段
    imp = (IMP)_objc_msgForward_impcache;  // 進入彙編
    cache_fill(cls, sel, imp, inst);       // 緩存方法

    ......
}
複製代碼
// objc-msg-arm64.s(objc4)
	STATIC_ENTRY __objc_msgForward_impcache
	b	__objc_msgForward
	END_ENTRY __objc_msgForward_impcache
	
	ENTRY __objc_msgForward
	adrp	x17, __objc_forward_handler@PAGE   // ⚠️執行C函數 _objc_forward_handler
	ldr	x17, [x17, __objc_forward_handler@PAGEOFF]
	br	x17
	END_ENTRY __objc_msgForward
複製代碼
// objc-runtime.mm(objc4)
// 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);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
複製代碼

能夠看到_objc_forward_handler是一個函數指針,指向objc_defaultForwardHandler(),該函數只是打印信息。因爲蘋果沒有對此開源,咱們沒法再深刻探索關於「消息轉發」的詳細執行邏輯。函數

咱們知道,若是調用一個沒有實現的方法,而且沒有進行「動態方法解析」和「消息轉發」處理,會報經典的 crash:unrecognized selector sent to instance/class。咱們查看 crash 打印信息的函數調用棧,以下,能夠看的系統調用了一個叫___forwarding___的函數。

該函數是 CoreFoundation 框架中的,蘋果對此函數還沒有開源,咱們能夠打斷點進入該函數的彙編實現。

如下是從網上找到的___forewarding___的 C 語言僞代碼實現。

// 僞代碼
int __forwarding__(void *frameStackPointer, int isStret) {
    id receiver = *(id *)frameStackPointer;
    SEL sel = *(SEL *)(frameStackPointer + 8);
    const char *selName = sel_getName(sel);
    Class receiverClass = object_getClass(receiver);

    // ⚠️⚠️⚠️調用 forwardingTargetForSelector:
    if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
        id forwardingTarget = [receiver forwardingTargetForSelector:sel];
        // ⚠️判斷該方法是否返回了一個對象且該對象 != receiver
        if (forwardingTarget && forwardingTarget != receiver) {
            if (isStret == 1) {
                int ret;
                objc_msgSend_stret(&ret,forwardingTarget, sel, ...);
                return ret;
            }
            //⚠️objc_msgSend(返回值, sel, ...);
            return objc_msgSend(forwardingTarget, sel, ...);
        }
    }

    // 殭屍對象
    const char *className = class_getName(receiverClass);
    const char *zombiePrefix = "_NSZombie_";
    size_t prefixLen = strlen(zombiePrefix); // 0xa
    if (strncmp(className, zombiePrefix, prefixLen) == 0) {
        CFLog(kCFLogLevelError,
              @"*** -[%s %s]: message sent to deallocated instance %p",
              className + prefixLen,
              selName,
              receiver);
        <breakpoint-interrupt>
    }

    // ⚠️⚠️⚠️調用 methodSignatureForSelector 獲取方法簽名後再調用 forwardInvocation
    if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
        // ⚠️調用 methodSignatureForSelector 獲取方法簽名
        NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
        // ⚠️判斷返回值是否爲 nil
        if (methodSignature) {
            BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct;
            if (signatureIsStret != isStret) {
                CFLog(kCFLogLevelWarning ,
                      @"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'. Signature thinks it does%s return a struct, and compiler thinks it does%s.",
                      selName,
                      signatureIsStret ? "" : not,
                      isStret ? "" : not);
            }
            if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {

                // ⚠️根據方法簽名建立一個 NSInvocation 對象
                NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];

                // ⚠️調用 forwardInvocation
                [receiver forwardInvocation:invocation];

                void *returnValue = NULL;
                [invocation getReturnValue:&value];
                return returnValue;
            } else {
                CFLog(kCFLogLevelWarning ,
                      @"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message",
                      receiver,
                      className);
                return 0;
            }
        }
    }

    SEL *registeredSel = sel_getUid(selName);

    // selector 是否已經在 Runtime 註冊過
    if (sel != registeredSel) {
        CFLog(kCFLogLevelWarning ,
              @"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort",
              sel,
              selName,
              registeredSel);
    } // ⚠️⚠️⚠️調用 doesNotRecognizeSelector 
    else if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {

        [receiver doesNotRecognizeSelector:sel];
    }
    else {
        CFLog(kCFLogLevelWarning ,
              @"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort",
              receiver,
              className);
    }

    // The point of no return.
    kill(getpid(), 9);
}
複製代碼

 

總結

至此,objc_msgSend方法調用流程就已經講解結束了。 下面來作一個小總結。

objc_msgSend 執行流程圖

objc_msgSend流程
相關文章
相關標籤/搜索