objc_msgSend流程分析

背景

如果大家用clang编译编译过oc的代码,你会发现oc的所有方法调用最终都都转换成了objc_msgSend,而它内部是如何实现的呢,对于我们开发过程中经常遇到的unrecognized selector sent to instance这个方法未实现异常又是如何出现的呢,今天我们就来剖析一下整个流程吧~

先总结一下objc_msgSend整体流程的四个阶段:

  • 快速查找
  • 慢速查找
  • 动态方法决议
  • 消息转发

下面我们将从这四个阶段还逐步分析其具体流程和实现。

一、快速查找

首先看objc_msgSend这个名字大概就能猜到它是属于runtime中的方法,所以在runtime源码层面搜索objc_msgSend,发现其是使用汇编语言实现的,并且根据不同的架构有不同的实现,这里我们直接看ios的架构objc-msg-arm64.s中的实现:

#if SUPPORT_TAGGED_POINTERS
    .data
    .align 3
    .globl _objc_debug_taggedpointer_classes
_objc_debug_taggedpointer_classes:
    .fill 16, 8, 0
    .globl _objc_debug_taggedpointer_ext_classes
_objc_debug_taggedpointer_ext_classes:
    .fill 256, 8, 0
#endif

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    ldr p13, [x0]       // p13 = isa
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend

ENTRY表示入口,cmp、b.le、b.eq这些都是一些判空处理,基本不用关心。所以关键流程在于,先通过GetClassFromIsa_p16获取isa,拿到后进入LGetIsaDone流程,取isa的流程如下:

.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA
    // Indexed isa
    mov p16, $0         // optimistically set dst = src
    tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f  // done if not non-pointer isa
    // isa in p16 is indexed
    adrp    x10, _objc_indexed_classes@PAGE
    add x10, x10, _objc_indexed_classes@PAGEOFF
    ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
    ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:

#elif __LP64__
    // 64-bit packed isa
    and p16, $0, #ISA_MASK

#else
    // 32-bit raw isa
    mov p16, $0

#endif

.endmacro

比较关键就是这句and p16, $0, #ISA_MASK,其实在前面的isa结构分析中我们看到过获取对象的isa代码:

inline Class 
objc_object::ISA() 
{
    ASSERT(!isTaggedPointer()); 
#if SUPPORT_INDEXED_ISA
    if (isa.nonpointer) {
        uintptr_t slot = isa.indexcls;
        return classForIndex((unsigned)slot);
    }
    return (Class)isa.bits;
#else
    return (Class)(isa.bits & ISA_MASK);
#endif
}

可以看到这里只是用汇编语言在做同样的事情,瞬间感觉汇编也没那么难了,有木有~
接下来,在拿到isa后会调用CacheLookup方法并传入NORMAL参数:

.macro CacheLookup
    LLookupStart$1:

    // p1 = SEL, p16 = isa
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    and p10, p11, #~0xf         // p10 = buckets
    and p11, p11, #0xf          // p11 = maskShift
    mov p12, #0xffff
    lsr p11, p12, p11               // p11 = mask = 0xffff >> p11
    and p12, p1, p11                // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif


    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
                    // p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    add p12, p12, p11, LSL #(1+PTRSHIFT)
                    // p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif

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

    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

LLookupEnd$1:
LLookupRecover$1:
3:  // double wrap
    JumpMiss $0

.endmacro

这里是在通过指针平移后遍历Class中的cache,比较其中的bucket->sel和当前传进来的_cmd。其实通过c语法也可以实现,只是这里使用了汇编。后续流程有三种情况CacheHitCheckMissJumpMiss
先看CacheHit:

.macro CacheHit
.if $0 == NORMAL
    TailCallCachedImp x17, x12, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
    mov p0, p17
    cbz p0, 9f          // don't ptrauth a nil imp
    AuthAndResignAsIMP x0, x12, x1, x16 // authenticate imp and re-sign as IMP
9:  ret             // return IMP
.elseif $0 == LOOKUP
    // No nil check for ptrauth: the caller would crash anyway when they
    // jump to a nil IMP. We don't care if that jump also fails ptrauth.
    AuthAndResignAsIMP x17, x12, x1, x16    // authenticate imp and re-sign as IMP
    ret             // return imp via x17
.else
.abort oops
.endif
.endmacro

传入的是NORMAL,直接调用了方法。
CheckMiss和JumpMiss:

.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL
    cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz p9, __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

由于传入参数也是NORMAL所以都会调用到__objc_msgSend_uncached
__objc_msgSend_uncached继续跟踪代码如下:

STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
    
MethodTableLookup
TailCallFunctionPointer x17

END_ENTRY __objc_msgSend_uncached
    
//MethodTableLookup的实现
.macro MethodTableLookup
    
// push frame
SignLR
stp fp, lr, [sp, #-16]!
mov fp, sp

// save parameter registers: x0..x8, q0..q7
sub sp, sp, #(10*8 + 8*16)
stp q0, q1, [sp, #(0*16)]
stp q2, q3, [sp, #(2*16)]
stp q4, q5, [sp, #(4*16)]
stp q6, q7, [sp, #(6*16)]
stp x0, x1, [sp, #(8*16+0*8)]
stp x2, x3, [sp, #(8*16+2*8)]
stp x4, x5, [sp, #(8*16+4*8)]
stp x6, x7, [sp, #(8*16+6*8)]
str x8,     [sp, #(8*16+8*8)]

// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
bl  _lookUpImpOrForward

// IMP in x0
mov x17, x0
    
// restore registers and return
ldp q0, q1, [sp, #(0*16)]
ldp q2, q3, [sp, #(2*16)]
ldp q4, q5, [sp, #(4*16)]
ldp q6, q7, [sp, #(6*16)]
ldp x0, x1, [sp, #(8*16+0*8)]
ldp x2, x3, [sp, #(8*16+2*8)]
ldp x4, x5, [sp, #(8*16+4*8)]
ldp x6, x7, [sp, #(8*16+6*8)]
ldr x8,     [sp, #(8*16+8*8)]

mov sp, fp
ldp fp, lr, [sp], #16
AuthenticateLR

.endmacro

可以看到__objc_msgSend_uncached是调用了MethodTableLookup方法,而MethodTableLookup经过一系统的指针操作后调用了_lookUpImpOrForward方法进入到c语言层面的方法查找,这也就是后续的慢速查找流程,至此汇编语言层面的cache中快速查找流程结束。

二、慢速查找

全局搜索lookUpImpOrForward方法实现:

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

    // 多线程情况下,方法的实现在另外的线程中被加载过了,所以先查询一次,如果查询到了直接到done_nolock
    if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
    }

    runtimeLock.lock();
    checkIsKnownClass(cls);
    //如果类还未实现,会先加载类的一些初始化信息,例如继承链等
    if (slowpath(!cls->isRealized())) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
    }

    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
    }

    runtimeLock.assertLocked();
    curClass = cls;
    //死循环遍历继承链上的所有的类对象,查看有没有对应的方法实现
    for (unsigned attempts = unreasonableClassCount();;) {
        //1.先查找当前类的bits信息中methods,找到的话跳转到done
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            imp = meth->imp;
            goto done;
        }
        //2.如果当前类中未找到,则把curClass指向其父类,进入下一个循环,直到找到根NSObject的父类nil为止
        if (slowpath((curClass = curClass->superclass) == nil)) {
            //直到根NSObject还未找到对应方法实现,指向imp为默认值
            imp = forward_imp;
            break;
        }

        // Halt if there is a cycle in the superclass chain.
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        //此处属于优化项,通过汇编在cache中快速查找,如果父类中有方法实现,直接跳出循环
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            break;
        }
        if (fastpath(imp)) {
            goto done;
        }
    }

    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;//确保方法只进来一次
        //在上面的循环结束后如果还没有找到方法实现,将会调用resolveMethod_locked进入动态方法决议
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    //如果找到方法的实现,会进行缓存并记录日志
    log_and_fill_cache(cls, imp, sel, inst, curClass);
    runtimeLock.unlock();
 done_nolock:
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

上面的流程主要是在类的继承链上找方法的实现,如果找到则会调用log_and_fill_cache进行缓存并返回,如果循环结束了还是未找到会把imp指向默认值_objc_msgForward_impcache,这将会进入消息转发流程。并且会调用resolveMethod_locked方法进入动态方法决议流程。

其中有一个比较关键方法调用getMethodNoSuper_nolock,从当前类中查找方法实现,查看其方法实现:

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    ASSERT(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?

    auto const methods = cls->data()->methods();
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         mlists != end;
         ++mlists)
    {
        //  getMethodNoSuper_nolock is the hottest
        // caller of search_method_list, inlining it turns
        // getMethodNoSuper_nolock into a frame-less function and eliminates
        // any store from this codepath.
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

ALWAYS_INLINE static method_t *
search_method_list_inline(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
    
    if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
        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;
}

//二分查找进行遍历方法实现
ALWAYS_INLINE 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每次除2
    for (count = list->count; count != 0; count >>= 1) {
        //把probe指向base偏移总量的一半位置
        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;
        }
        //如果要查找的值比probeValue大,则需要把base指针后移,否则count除2即可
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

三、动态方法决议

先看一下动态方法决议resolveMethod_locked的方法的实现:

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();

    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNil(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    //经过上面的resolveInstanceMethod和resolveClassMethod这两个方法调用,给了开发者一次机会重新实现sel的方法的机会,如果开发者实现了,则此处再次把imp查找出来返回,则慢速查找流程中的imp为新的方法实现了。
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}

如果不是元类(表示调用的是对象方法)则进入resolveInstanceMethod,如果是元类(表示调用的是类方法)则先调用resolveClassMethod,如果还是没找到则再调用resolveInstanceMethod。在resolveClassMethodresolveClassMethod这两个方法中给了开发者一次动态实现imp的机会,如果在这个过程中实现了方法,则最后返回新的imp给上层调用方。

resolveInstanceMethod和resolveClassMethod代码实现:

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    //先看一下当前类有没有实现resolveInstanceMethod方法,lookUpImpOrNil也会在当前类的继承链上找,NSObject类肯定实现了,所以不会return
    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        // Resolver not implemented.
        return;
    }
    //直接调用resolveInstanceMethod方法,所以如果你在自定义的类中重写resolveInstanceMethod方法,则会调用到你的实现中来
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    //调用完resolveInstanceMethod方法后再尝试查找sel方法的实现,如果找到了
    IMP imp = lookUpImpOrNil(inst, sel, cls);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());

    if (!lookUpImpOrNil(inst, @selector(resolveClassMethod:), cls)) {
        // Resolver not implemented.
        return;
    }

    Class nonmeta;
    {
        mutex_locker_t lock(runtimeLock);
        nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
        // +initialize path should have realized nonmeta already
        if (!nonmeta->isRealized()) {
            _objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
                        nonmeta->nameForLogging(), nonmeta);
        }
    }
    //注意此时cls传进来的是元类,所以此处要取nonmeta(类),因为objc_msgSend的第一个参数是方法接收者,应该是我们的自定义类
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveClassMethod adds to self->ISA() a.k.a. cls
    IMP imp = lookUpImpOrNil(inst, sel, cls);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

至此,动态方法决议流程结束,在这个过程中如果用户把sel对应的imp实现了,那整个方法查找流程结束,否则imp将返回默认的_objc_msgForward_impcache,则会进入到消息转发流程,再给开发者两次处理消息的机会。

动态方法决议的思考

从上面的动态方法决议流程来看,不管是调用对象方法还是类方法,最后都会调用resolveInstanceMethod方法,为什么我调用一个类方法也需要走到resolveInstanceMethod中来呢?我是这么理解的:类的继承链往上会找到根NSObject,元类的继承链往上也会找到根NSObject,而开发者可以选择在当前类中实现resolveClassMethod方法,或者重写NSObject的resolveInstanceMethod(用category),所以这里相当于苹果给了开发者额外的一次机会来处理类方法的调用。
基于上面的分析,我们可以有这样一种猜测,是否能增加一个NSObject的category,重写其resolveInstanceMethod,这样所有的未实现方法不都可以处理了吗?

//定义一个LGPerson类,声明一个对象方法和类方法,但都不实现,实现另外两个方法
@interface LGPerson : NSObject
- (void)say666;
+ (void)sayNB;
@end
@implementation LGPerson
- (void)sayMaster{
    NSLog(@"%s",__func__);
}

+ (void)lgClassMethod{
    NSLog(@"%s",__func__);
}
@end

//NSObject+Fix中
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(say666)) {
        NSLog(@"%@ 来了",NSStringFromSelector(sel));

        IMP imp           = class_getMethodImplementation(self, @selector(sayMaster));
        Method sayMMethod = class_getInstanceMethod(self, @selector(sayMaster));
        const char *type  = method_getTypeEncoding(sayMMethod);
        return class_addMethod(self, sel, imp, type);
    }
    else if (sel == @selector(sayNB)) {
        
        IMP imp           = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        Method sayMMethod = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        const char *type  = method_getTypeEncoding(sayMMethod);
        return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
    }
    return NO;
}

在main函数中测试代码如下:

LGPerson *person = [LGPerson alloc];
[person say666];
[LGPerson sayNB];

//程序打印结果
2020-09-28 09:53:50.739887+0800 002-testObjc[5875:2952619] say666 来了
2020-09-28 09:53:50.740931+0800 002-testObjc[5875:2952619] -[LGPerson sayMaster]
2020-09-28 09:54:04.081564+0800 002-testObjc[5875:2952619] +[LGPerson lgClassMethod]
Program ended with exit code: 0

但是调过过程发现,系统底层的方法调用也会进来,所以此方法侵入性较强,可能会影响系统层面的有影响,慎用!

四、消息转发

在进入消息转发流程之前我们先看一下这种情况,如果在上面的动态方法决议中还是没有对应的imp实现,则整个lookUpImpOrForward方法将返回默认的,_objc_msgForward_impcache,该方法是实现在汇编层面:

STATIC_ENTRY __objc_msgForward_impcache

    // No stret specialization.
    b   __objc_msgForward

    END_ENTRY __objc_msgForward_impcache

    
    ENTRY __objc_msgForward

    adrp    x17, __objc_forward_handler@PAGE
    ldr p17, [x17, __objc_forward_handler@PAGEOFF]
    TailCallFunctionPointer x17
    
    END_ENTRY __objc_msgForward

可以看到最终调用的方法是__objc_forward_handler,而全局搜索它的定义在runtime源码中有个赋值操作objc_defaultForwardHandler默认消息转发处理也就是我们经常遇到的方法未实现异常:

void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

// Default forward handler halts the process.
__attribute__((noreturn, cold)) 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);
}

既然有默认消息转发,那肯定有非默认的情况。这也是系统留给开发者最后的二次处理消息的机会。消息转发分为两个步骤:

  • 快速转发
  • 慢速转发
快速转发

实例方法forwardingTargetForSelector,如果你在自定义类中实现了该方法则可以修改objc_msgSend方法调用时的第一个参数,将方法的调用者修改成另一个实例对象。例如:

@interface LGStudent : NSObject
- (void)sayInstanceMethod;
@end

@implementation LGStudent
- (void)sayInstanceMethod{
    NSLog(@"%s",__func__);
}
@end

@interface LGPerson : NSObject
- (void)sayInstanceMethod;
@end
@implementation LGPerson

- (id)forwardingTargetForSelector:(SEL)sel {
    if (sel == @selector(sayInstanceMethod)) {
       return [LGStudent alloc];
    }
    return [super forwardingTargetForSelector:sel];
}

定义LGStudent类和LGPerson类,LGStudent类中定义并实现sayInstanceMethod方法,LGPerson类中只定义不实现,然后测试代码如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        [person sayInstanceMethod];
    }
    return 0;
}

结束正常打印:

2020-09-27 19:37:07.166357+0800 002-TestObjc[1924:2860333] -[LGStudent sayInstanceMethod]
Program ended with exit code: 0
慢速转发

慢速转发有两个关键方法需要配合使用,methodSignatureForSelectorforwardInvocation,还是上面的例子,但把forwardingTargetForSelector修改成下面的实现:

@implementation LGPerson

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

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    anInvocation.target = [LGStudent alloc];
    anInvocation.selector = @selector(sayInstanceMethod);
    [anInvocation invoke];
}

结果任然会正常输出,并且在forwardInvocation中什么事情都不处理也不会报方法未实现的异常了,说明系统在前面所有流程中都未找到方法实现的情况下,将sayInstanceMethod的方法调用完全交给开发者处理了。

怎么来的

上面的快速转发和慢速转发目前确实能帮助开发者解决方法未实现的崩溃问题,但由于在runtime源码层面没有对应的实现,我们看不到它真正的调用流程,所以为了进一步的分析整个过程我们可以通过下面两个步骤。

  • instrumentObjcMessageSends方法 日志文件分析
  • 堆栈+Hopper工具 反编译流程分析

1.instrumentObjcMessageSends方法,它是runtime源码中的方法,具体使用方式如下:

//声明为外部实现的方法
extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        LGPerson *person = [LGPerson alloc];
        //打开日志记录
        instrumentObjcMessageSends(YES);
        //调用oc方法
        [person sayHello];
        //关闭日志记录
        instrumentObjcMessageSends(NO);
        NSLog(@"Hello, World!");
    }
    return 0;
}

而在我们电脑的/tmp目录下将生成一个msgSends-xxx的文件,其中内容如下:

+ LGPerson NSObject resolveInstanceMethod:
+ LGPerson NSObject resolveInstanceMethod:
- LGPerson NSObject forwardingTargetForSelector:
- LGPerson NSObject forwardingTargetForSelector:
- LGPerson NSObject methodSignatureForSelector:
- LGPerson NSObject methodSignatureForSelector:
- LGPerson NSObject class
+ LGPerson NSObject resolveInstanceMethod:
+ LGPerson NSObject resolveInstanceMethod:
- LGPerson NSObject doesNotRecognizeSelector:
- LGPerson NSObject doesNotRecognizeSelector:
- LGPerson NSObject class
- OS_xpc_serializer OS_xpc_object dealloc
- OS_object NSObject dealloc
...

当前这里只能看到动态方法决议以及消息转发中几个关键方法的调用情况,那具体是谁来调用它们呢,还需要更进一步的分析工具来分析。

2.堆栈+Hopper工具,首页我们在当前类中重写forwardingTargetForSelector方法,并在其中打个断点,bt查看堆栈信息如下:

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x0000000100003e14 002-testObjc`-[LGPerson forwardingTargetForSelector:](self=0x00000001006a2b60, _cmd="forwardingTargetForSelector:", aSelector="sayHello") at LGPerson.m:17:52
    frame #1: 0x00007fff36910f0a CoreFoundation`___forwarding___ + 226
    frame #2: 0x00007fff36910d98 CoreFoundation`__forwarding_prep_0___ + 120
    frame #3: 0x0000000100003db0 002-instrumentObjcMessageSends辅助分析`main(argc=1, argv=0x00007ffeefbff420) at main.m:19:9
    frame #4: 0x00007fff709c7cc9 libdyld.dylib`start + 1

可以看到forwardingTargetForSelector方法是在CoreFoundation中的___forwarding___方法中调用的。我们可以用Hopper反编译工具看一下CoreFoundation中的实现。先通过在lldb下输入image list拿到CoreFoundation的路径。大概在这个位置:

/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation 

将其拖到Hopper工具中来,搜索___forwarding___方法,然后查看伪代码实现:

int ____forwarding___(int arg0, int arg1) {
    rsi = arg1;
    rdi = arg0;
    r15 = rdi;
    rcx = COND_BYTE_SET(NE);
    if (rsi != 0x0) {
            r12 = _objc_msgSend_stret;
    }
    else {
            r12 = _objc_msgSend;
    }
    rax = rcx;
    rbx = *(r15 + rax * 0x8);
    rcx = *(r15 + rax * 0x8 + 0x8);
    var_140 = rcx;
    r13 = rax * 0x8;
    if ((rbx & 0x1) == 0x0) goto loc_649bb;

loc_6498b:
    rcx = *_objc_debug_taggedpointer_obfuscator ^ rbx;
    rax = rcx >> 0x1 & 0x7;
    if (rax == 0x7) {
            rcx = rcx >> 0x4;
            rax = (rcx & 0xff) + 0x8;
    }
    if (rax == 0x0) goto loc_64d48;

loc_649bb:
    var_148 = r13;
    var_138 = r12;
    var_158 = rsi;
    rax = object_getClass(rbx);
    r12 = rax;
    r13 = class_getName(rax);
    r14 = @selector(forwardingTargetForSelector:);
    if (class_respondsToSelector(r12, r14) == 0x0) goto loc_64a67;

loc_649fc:
    r14 = @selector(forwardingTargetForSelector:);
    rdi = rbx;
    rax = _objc_msgSend(rdi, r14);
    if ((rax == 0x0) || (rax == rbx)) goto loc_64a67;

loc_64a19:
    r12 = var_138;
    r13 = var_148;
    if ((rax & 0x1) == 0x0) goto loc_64a5b;

loc_64a2b:
    rdx = *_objc_debug_taggedpointer_obfuscator ^ rax;
    rcx = rdx >> 0x1 & 0x7;
    if (rcx == 0x7) {
            rcx = (rdx >> 0x4 & 0xff) + 0x8;
    }
    if (rcx == 0x0) goto loc_64d45;

loc_64a5b:
    *(r15 + r13) = rax;
    r15 = 0x0;
    goto loc_64d82;

loc_64d82:
    if (*___stack_chk_guard == *___stack_chk_guard) {
            rax = r15;
    }
    else {
            rax = __stack_chk_fail();
    }
    return rax;

loc_64d45:
    rbx = rax;
    goto loc_64d48;

loc_64d48:
    if ((*(int8_t *)__$e48aedf37b9edb179d065231b52a648b & 0x10) != 0x0) goto loc_64ed1;

loc_64d55:
    rax = _getAtomTarget(rbx);
    *(r15 + r13) = rax;
    ___invoking___(r12, r15);
    if (*r15 == rax) {
            *r15 = rbx;
    }
    goto loc_64d82;

loc_64ed1:
    ____forwarding___.cold.4();
    rax = *(rdi + 0x8);
    return rax;

loc_64a67:
    var_138 = rbx;
    if (strncmp(r13, "_NSZombie_", 0xa) == 0x0) goto loc_64dc1;

loc_64a8a:
    r14 = var_138;
    var_148 = r15;
    if (class_respondsToSelector(r12, @selector(methodSignatureForSelector:)) == 0x0) goto loc_64dd7;

loc_64ab2:
    rax = [r14 methodSignatureForSelector:var_140];
    rbx = var_158;
    if (rax == 0x0) goto loc_64e3c;

loc_64ad5:
    r12 = rax;
    rax = [rax _frameDescriptor];
    r13 = rax;
    if (((*(int16_t *)(*rax + 0x22) & 0xffff) >> 0x6 & 0x1) != rbx) {
            rax = sel_getName(var_140);
            r8 = "";
            rcx = r8;
            if ((*(int16_t *)(*r13 + 0x22) & 0xffff & 0x40) == 0x0) {
                    rcx = " not";
            }
            if (rbx == 0x0) {
                    r8 = " not";
            }
            _CFLog(0x4, @"*** 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.", rax, rcx, r8, r9, stack[2003]);
    }
    var_150 = r13;
    if (class_respondsToSelector(object_getClass(r14), @selector(_forwardStackInvocation:)) == 0x0) goto loc_64c19;

loc_64b6c:
    if (*____forwarding___.onceToken != 0xffffffffffffffff) {
            dispatch_once(____forwarding___.onceToken, ^ {/* block implemented at ______forwarding____block_invoke */ } });
    }
    r15 = [NSInvocation requiredStackSizeForSignature:r12];
    rsi = *____forwarding___.invClassSize;
    rsp = rsp - ___chkstk_darwin();
    r13 = rsp;
    __bzero(r13, rsi);
    rbx = rsp - ___chkstk_darwin();
    objc_constructInstance(*____forwarding___.invClass, r13);
    var_140 = r15;
    [r13 _initWithMethodSignature:r12 frame:var_148 buffer:rbx size:r15];
    [var_138 _forwardStackInvocation:r13];
    r14 = 0x1;
    goto loc_64c76;

loc_64c76:
    if (*(int8_t *)(r13 + 0x34) != 0x0) {
            rax = *var_150;
            if (*(int8_t *)(rax + 0x22) < 0x0) {
                    rsi = var_148 + *(int32_t *)(rax + 0x1c);
                    rdx = *(int8_t *)(rax + 0x20) & 0xff;
                    memmove(*(rdx + rsi), *(rdx + *(int32_t *)(rax + 0x1c) + *(r13 + 0x8)), *(int32_t *)(*rax + 0x10));
            }
    }
    rax = [r12 methodReturnType];
    rbx = rax;
    rax = *(int8_t *)rax;
    if ((rax != 0x76) && (((rax != 0x56) || (*(int8_t *)(rbx + 0x1) != 0x76)))) {
            r15 = *(r13 + 0x10);
            if (r14 != 0x0) {
                    r15 = [[NSData dataWithBytes:r15 length:var_140] bytes];
                    [r13 release];
                    rax = *(int8_t *)rbx;
            }
            if (rax == 0x44) {
                    asm { fld        tword [r15] };
            }
    }
    else {
            r15 = ____forwarding___.placeholder;
            if (r14 != 0x0) {
                    [r13 release];
            }
    }
    goto loc_64d82;

loc_64c19:
    if (class_respondsToSelector(object_getClass(r14), @selector(forwardInvocation:)) == 0x0) goto loc_64ec2;

loc_64c3b:
    rax = [NSInvocation _invocationWithMethodSignature:r12 frame:var_148];
    r13 = rax;
    [r14 forwardInvocation:rax];
    var_140 = 0x0;
    r14 = 0x0;
    goto loc_64c76;

loc_64ec2:
    rdi = &var_130;
    ____forwarding___.cold.3(rdi, r14);
    goto loc_64ed1;

loc_64e3c:
    rax = sel_getName(var_140);
    r14 = rax;
    rax = sel_getUid(rax);
    if (rax != var_140) {
            _CFLog(0x4, @"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort", var_140, r14, rax, r9, stack[2003]);
    }
    if (class_respondsToSelector(object_getClass(var_138), @selector(doesNotRecognizeSelector:)) == 0x0) {
            ____forwarding___.cold.2(var_138);
    }
    _objc_msgSend(var_138, @selector(doesNotRecognizeSelector:));
    asm { ud2 };
    rax = loc_64ec2(rdi, rsi);
    return rax;

loc_64dd7:
    rbx = class_getSuperclass(r12);
    r14 = object_getClassName(r14);
    if (rbx == 0x0) {
            _CFLog(0x4, @"*** NSForwarding: warning: object %p of class '%s' does not implement methodSignatureForSelector: -- did you forget to declare the superclass of '%s'?", var_138, r14, object_getClassName(var_138), r9, stack[2003]);
    }
    else {
            _CFLog(0x4, @"*** NSForwarding: warning: object %p of class '%s' does not implement methodSignatureForSelector: -- trouble ahead", var_138, r14, r8, r9, stack[2003]);
    }
    goto loc_64e3c;

loc_64dc1:
    ____forwarding___.cold.1(var_138, r13, var_140, rcx, r8);
    goto loc_64dd7;
}

可以看到其中forwardingTargetForSelector methodSignatureForSelector forwardInvocation doesNotRecognizeSelector 这几个方法间的跳转逻辑。这样也能辅助我们对整个objc_msgSend流程的分析。

最后

好了,分析了这么多,最后来一张整体流程的思维导图总结一下吧。


objc_msgSend总结

你可能感兴趣的:(objc_msgSend流程分析)