Runtime 之消息机制

76 阅读20分钟

🔗 原文:Runtime 之消息机制 - 李峰峰博客

1、objc_msgSend 简介

在 OC 中,所有的方法调用底层都会转换成 objc_msgSend 函数进行调用,例如,对于下面方法调用:

[myObject test1];
[myObject test2:100];

将其转换成 C++ 源码:

((void (*)(id, SEL))(void *)objc_msgSend)((id)myObject, sel_registerName("test1"));
((void (*)(id, SEL, int))(void *)objc_msgSend)((id)myObject, sel_registerName("test2:"),100);

简化处理后:

objc_msgSend(myObject, sel_registerName("test1"));
objc_msgSend(myObject, sel_registerName("test2:"),100);

可在源码中找到 objc_msgSend 函数定义:

OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)

objc_msgSend 函数会有一个可能为空的返回值,一般来说需要至少两个参数:消息调用者及方法名,以及不确定个数的方法参数。

objc_msgSend 伪代码如下:

id objc_msgSend(id self, SEL _cmd, ...) {
  Class class = object_getClass(self);
  IMP imp = class_getMethodImplementation(class, _cmd);
  return imp ? imp(self, _cmd, ...) : 0;
}

其实 objc_msgSend 主要做的就是获取 IMP 并调用,并且在找到对应方法实现 IMP 调用的时候,会默认带入两个参数:self _cmd_cmd 就是 SEL。所以我们可以在方法里使用 self_cmd

实际上,objc_msgSend 是使用汇编来实现的,采用汇编来实现,主要是因为有的方法返回 id,有的返回 int,单独一个方法定义满足不了多种类型返回值,汇编对可变参数处理起来更方便。再加上汇编程序执行效率高,所以 objc_msgSend 这种高频率调用的函数使用汇编实现有助于提高系统运行效率。

2、objc_msgSend 源码分析

(1) 方法快速查找流程(汇编实现)

源码下载地址:opensource.apple.com/tarballs/ob…

objc_msgSend 相关源码主要在 objc-msg-arm64.s 中,从源码可以看出 objc_msgSend 是使用汇编实现的。

objc_msgSend 部分源码如下:

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    // 检查 objc_msgSend 第一个参数(消息接受者)是否为 nil
    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
    // 如果支持 tagged pointer,如果 p0 <= 0,执行 LNilOrTagged 函数
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    ldr p13, [x0]       // p13 = isa
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    b.eq    LReturnZero     // nil check

    // tagged
    ......
    cmp x10, x16
    b.ne    LGetIsaDone

    // ext tagged
    ......
    b   LGetIsaDone
#endif

LReturnZero:
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    ret

    END_ENTRY _objc_msgSend

首先会对 objc_msgSend 的第一个参数是否为 nil,即消息接收者是否存在:

  • 如果不存在,函数将直接 return,所以给 nil 对象发送消息不会引发 crash。
  • 如果存在,则继续执行,进入缓存查找逻辑 CacheLookup(传入的参数是 NORMAL)。

CacheLookup 源码如下:

.macro CacheLookup
    // p1 = SEL, p16 = isa(p1 存放着 SEL,p16 存放这当前消息接收者的 isa 指针)
    ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
    and w11, w11, 0xffff    // p11 = mask
#endif
    and w12, w1, w11        // x12 = _cmd & mask
    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
    add p12, p12, w11, UXTW #(1+PTRSHIFT)
                                // p12 = buckets + (mask << 1+PTRSHIFT)

    // 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

3:  // double wrap
    JumpMiss $0
    
.endmacro

.macro 在汇编中用于定义宏,所以 CacheLookup 是被定义成了宏。 CacheLookup 的功能其实就是去当前类的方法缓存中去查找方法:

  • 如果在缓存中找到方法,即命中,会调用 CacheHit,直接调用对应方法或返回方法 imp。
  • 如果没有找到方法,则会调用 CheckMiss

CheckMiss 源码如下:

.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

因为前面 CacheLookup 传入的参数是 NORMAL,所以这里 CheckMiss 调用了 __objc_msgSend_uncached

STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

MethodTableLookup
TailCallFunctionPointer x17

END_ENTRY __objc_msgSend_uncached


在 __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

可以看到,MethodTableLookup 中又调用了 _lookUpImpOrForward。 并且调用 lookUpImpOrForward 函数时传入参数如下:

x0 = receiver 
x1 = selector 
x2 = class 
x3 = LOOKUP_INITIALIZE | LOOKUP_RESOLVER

到这里,快速查找流程结束,进入慢速查找流程。

(2) 方法慢速查找流程(C/C++ 实现)

_lookUpImpOrForward 在 C++ 中对应的函数为 lookUpImpOrForward,位于 objc-runtime-new.m 中:

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();

    if (fastpath(behavior & LOOKUP_CACHE)) {
      // cache_getImp 为汇编实现,内部实际上就是执行前面快速查找流程
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
    }
    
    // 加锁
    runtimeLock.lock();

    // 查询是否为已知类
    checkIsKnownClass(cls);

    // 类没有实现,尝试实现
    if (slowpath(!cls->isRealized())) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
        // runtimeLock may have been dropped but is now locked again
    }

    // 类如果没有初始化,进行初始化操作
    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
    }

    runtimeLock.assertLocked();
    curClass = cls;

    for (unsigned attempts = unreasonableClassCount();;) {
        //  当前类方法列表(如果方法列表已排序采用二分查找算法,否则直接遍历查找),如果找到,则返回,将方法缓存到 cache 中。
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            imp = meth->imp;
            // 跳转到 done
            goto done;
        }
        // curClass = superclass 继续查找,一直到 curClass == nil 的时候让 imp = forward_imp, break,终止循环
        if (slowpath((curClass = curClass->superclass) == nil)) {
            // 没找到,也没有父类了,使用转发 
            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.");
        }

        // 获取父类的缓存
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // 父类中找到的是forward_imp,则终止查找
            break;
        }
        if (fastpath(imp)) {
            // 找到imp,则跳转到 done,进行方法缓存
            goto done;
        }
    }

    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        // 进入动态方法决议
        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;
}

通过源码可以发现,是通过 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)
    {
        // <rdar://problem/46904873> 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;
}

通过函数中以下源码:

auto const methods = cls->data()->methods();

cls->data() 返回的就是 class_rw_t:
class_rw_t* data() {
    return (class_rw_t *)(bits & FAST_DATA_MASK);
}

可知,查找的方法列表就是 class_rw_t 中的 methods

getMethodNoSuper_nolock 函数中读取了方法列表后,具体 sel 查找是通过 search_method_list_inline 函数:

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

由以上源码可知,查找方法列表前会对否排好序进行判断,如果方法列表已排序,则会调用 findMethodInSortedMethodList 函数进行查找,findMethodInSortedMethodList 函数实际上进行的是进行二分查找,通过二分查找提高查找效率。否则直接遍历去查询方法列表。

lookUpImpOrForward 源码分析:

  • 1、对类做合法性检查及处理,判断是否时已知类、类是否实现、是否已经初始化,如果类没有实现或初始化,进行一下对应的实现及初始化操作。

    • 函数的 behavior 参数 valueLOOKUP_INITIALIZE | LOOKUP_RESOLVER,所以不再去查找缓存(因为之前已经查找过缓存了)。
  • 2、到当前类的方法列表中、查询此方法,如果找到方法,则将方法(Method 中的 IMP)存放到当前类的 cache 中去,并且返回 IMP,方法结束。如果没找到,则执行第 3 步。

    • 此处当前类的方法列表即 class_rw_t 中的 methods
    • 查找方法列表前会对否排好序进行判断,如果方法列表已排序,使用二分查找算法进行查找,提高查找效率。否则直接遍历去查询方法列表。
  • 3、递归向父类中查找,先查找缓存,再查找类方法列表,如果找到方法,执行第 4 步(forward_imp 判断)。如果没有找到,imp = forward_imp,然后执行第 4 步。

  • 4、判断此方法是否是 forward_imp 方法,如果不是,则将此方法存放到当前类的 cache 中,并且返 IMP,方法结束。如果此方法是 forward_imp,直接执行第 5 步。

    • forward_imp 实际上就是:_objc_msgForward_impcache
    • _objc_msgForward_impcache 其实是一个存放在内存中的函数指针,为汇编实现,内部会调用 __objc_msgForward 函数(消息转发的函数)。
  • 6、判断当前是否执行执行过方法解析(动态方法决议),如果没有执行过,进入动态方法解析阶段。如果执行过 1 次动态方法解析,则走到消息转发流程。

(3) 动态方法解析(动态方法决议)

根据前面 lookUpImpOrForward 源码可知,在方法查找时,如果发现方法是 forward_imp,会判断是否执行执行过方法解析,如果没有执行过,进入动态方法解析阶段,进入 resolveMethod_locked 函数。 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
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}

其中调用的 lookUpImpOrNil 函数源码如下:

static inline IMP
lookUpImpOrNil(id obj, SEL sel, Class cls, int behavior = 0)
{
    return lookUpImpOrForward(obj, sel, cls, behavior | LOOKUP_CACHE | LOOKUP_NIL);
}

可以看到,lookUpImpOrNil 函数内部就是直接调用了 lookUpImpOrForward 函数,重新进入慢速查找流程,注意这里 behavior 的参数值,进到 lookUpImpOrForward 里后是需要先去缓存中查找方法的(快速查找流程)。

resolveInstanceMethod 函数源码如下:

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        // Resolver not implemented.
        // 如果你没有实现类方法 +(BOOL)resolveInstanceMethod:(SEL)sel
        // NSObject 已经实现了,所以一般不会走这里
        return;
    }

    // 调用类方法: +(BOOL)resolveInstanceMethod:(SEL)sel,
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);

    // 检测是否有 sel 对应的 IMP。如在 +(BOOL)resolveInstanceMethod:(SEL)sel 中动态添加了 sel 对应方法,此时再次去查找这个 IMP 就能找到,并且在这一步就会将其保存到缓存中,下次调用从缓存中就可以找到了。
    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self 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 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));
        }
    }
}

resolveInstanceMethod 函数源码解析:

  • 如果实现了 +(BOOL)resolveInstanceMethod:(SEL)sel 方法直接调用该方法。

  • 调用 +(BOOL)resolveInstanceMethod:(SEL)sel 方法后,还会调用一次 lookUpImpOrNil 函数查找一下方法。所以如果在 +(BOOL)resolveInstanceMethod:(SEL)sel 方法中动态添加了 sel 对应方法之后,调用 lookUpImpOrNil 函数就能找到对应 imp,并且会将方法保存到缓存里,下次就能直接从缓存中找到并调用了。

  • 根据源码可发现 +(BOOL)resolveInstanceMethod:(SEL)sel 方法返回值并没什么实际用处,只用来做日志打印而已,所以返回 YES 还是 NO 都无所谓。但是在实际开发的时候还应当按照 OC 规范去做,处理了 sel 就返回 YES,否则返回 NO。

resolveClassMethod 主要逻辑和 resolveInstanceMethod 基本一样,源码如下:

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

动态方法解析总结:

  • 如果不是元类(说明当前是对象方法),进行对象方法的动态方法解析。

    • 如果实现了 +(BOOL)resolveInstanceMethod:(SEL)sel 方法并且在方法中动态添加了 sel 对应方法,后面调用 lookUpImpOrNil 函数就能找到对应 imp,并且会将方法保存到缓存里。下次就能直接从缓存中找到 sel 并调用了。
  • 如果是元类(说明当前是类方法),先进行类方法的动态方法解析,如果没在 + (BOOL)resolveClassMethod:(SEL)sel 中动态添加 sel 对应方法,则执行一次对象方法解析。

    • 上面如果 !lookUpImpOrNil(inst, sel, cls) 条件成立,说明执行过类方法的动态方法解析之后仍然没有找到 sel,也就是没在 + (BOOL)resolveClassMethod:(SEL)sel 中动态添加 sel 对应方法。
    • 这里为什么要再执行一次对象方法解析?因为元类的父类是根元类,根元类的父类是根类(NSObject)。如果整个继承链都没找到 +(BOOL)resolveClassMethod:(SEL)sel 方法,最终会找到根类(NSObject),根类是 class 对象,class 对象中保存的是对象方法,所以需要再做一次对象方法动态方法决议。

对象方法、类方法动态方法解析查找路径:

  • 对象方法:类 -> 父类 -> 根类 -> nil
  • 类方法:元类 -> 根元类 -> 根类 -> nil

根据以上查找路径可知,无论是对象方法的动态方法解析还是类方法的动态方法解析,都会查找到根类(NSObject),所以都会走到根类(NSObject)的 +(BOOL)resolveInstanceMethod:(SEL)sel 方法。也就是说,我们可以直接使用 +(BOOL)resolveInstanceMethod:(SEL)sel 方法处理对象方法和类方法的动态方法解析。

动态方法解析 Demo:

// 对象方法的动态方法解析
- (void)myInstanceMethod {
    NSLog(@"%s",__func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    // 调用了未实现的对象方法 xxxxInstanceMethod
    if (sel == @selector(xxxxInstanceMethod)) {
        Method resolveMethod = class_getInstanceMethod(self, @selector(myInstanceMethod));
        IMP myInstanceIMP = method_getImplementation(resolveMethod);
        const char* types = method_getTypeEncoding(resolveMethod);
        NSLog(@"%s", types);
        return class_addMethod(self, sel, myInstanceIMP, types);
    }
    return [super resolveInstanceMethod:sel];
}


// 类方法的动态方法解析
+ (void)myClassMethod {
    NSLog(@"%s", __func__);
}

+ (BOOL)resolveClassMethod:(SEL)sel {
    // 调用了未实现的类方法 xxxxClassMethod
    if (sel == @selector(xxxxClassMethod)) {
        Method resolveMethod = class_getClassMethod(self, @selector(myClassMethod));
        IMP myClassIMP = method_getImplementation(resolveMethod);
        const char* types = method_getTypeEncoding(resolveMethod);
        NSLog(@"%s", types);
        return class_addMethod(object_getClass(self), sel, myClassIMP, types);
    }
    return [super resolveClassMethod:sel];
}

也可以全都在 + (BOOL)resolveInstanceMethod:(SEL)sel 方法中处理:

- (void)myInstanceMethod {
    NSLog(@"%s",__func__);
}

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

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(xxxxInstanceMethod)) {
        // 调用了未实现的对象方法 xxxxInstanceMethod
        Method resolveMethod = class_getInstanceMethod(self, @selector(myInstanceMethod));
        IMP myInstanceIMP = method_getImplementation(resolveMethod);
        const char* types = method_getTypeEncoding(resolveMethod);
        NSLog(@"%s", types);
        return class_addMethod(self, sel, myInstanceIMP, types);
    } else if (sel == @selector(xxxxClassMethod)) {
        // 调用了未实现的类方法 xxxxClassMethod
        Method resolveMethod = class_getClassMethod(self, @selector(myClassMethod));
        IMP myClassIMP = method_getImplementation(resolveMethod);
        const char* types = method_getTypeEncoding(resolveMethod);
        NSLog(@"%s", types);
        return class_addMethod(object_getClass(self), sel, myClassIMP, types);
    }
    return NO;
}

(4) 消息转发

根据 lookUpImpOrForward 函数实现可知,如果执行过方法动态解析仍然没有找到对应方法,则走到消息转发流程,lookUpImpOrForward 方法也会把需要消息转发的 _objc_msgForward_impcache 作为 imp 返回。

_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_msgForward_impcache 是一个内部函数指针,最终会拿到 __objc_forward_handler 的地址并调用,其 OC 实现如下:

__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);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

其中 _objc_fatal 作用就是打日志并调用 __builtin_trap() 触发 crash,根据以上源码可知,这里是将 objc_defaultForwardHandler 赋值给 _objc_forward_handler,但是再看下 objc_defaultForwardHandler 实现发现,这里只是 crash 并打印信息,也是那个熟悉的 crash:unrecognized selector sent to instance xxxxxxxx,并没有其他内容。

所以,要想实现消息转发,就需要在某处给 _objc_forward_handler 重新赋值:

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

上面 _objc_forward_stret_handler 是用在非 arm64 下的,这里不再深究。

objc_setForwardHandler 的调用,以及之后的消息转发相关方法调用栈,是在 CoreFoundation 中的,但是 Apple 故意在相关开源的代码中删除了在 CFRuntime.c 文件 __CFInitialize() 中调用 objc_setForwardHandler 的代码(CoreFoundation 源码地址)。 借助逆向可分析下: 根据逆向结果可以发现,__CFInitialize() 中是以 __CF_forwarding_prep_0___forwarding_prep_1___ 作为参数调用的。

接下来制造一个简单 crash 查看方法调用栈:

MessageCrashTest[7942:944693] -[TestObject test]: unrecognized selector sent to instance 0x100508d80
MessageCrashTest[7942:944693] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[TestObject test]: unrecognized selector sent to instance 0x100508d80'
*** First throw call stack:
(
    0   CoreFoundation                      0x00007fff34801b57 __exceptionPreprocess + 178
    1   libobjc.A.dylib                     0x00007fff6d6765bf objc_exception_throw + 48
    2   CoreFoundation                      0x00007fff34880be7 -[NSObject(NSObject) doesNotRecognizeSelector:] + 205
    3   CoreFoundation                      0x00007fff347663bb ___forwarding___ + 1009
    4   CoreFoundation                      0x00007fff34765d98 _CF_forwarding_prep_0 + 120
    5   MessageCrashTest                    0x0000000100000f26 main + 79
    6   libdyld.dylib                       0x00007fff6e81ecc9 start + 1
)

可以看到 _CF_forwarding_prep_0 函数内部调用了 ___forwarding___ 函数,然后调用 doesNotRecognizeSelector 方法,最后抛出异常。

___forwarding___ 也是不开源的,但是国外大神通过逆向分析后写了伪代码( 链接 ):

void __forwarding__(BOOL isStret, void *frameStackPointer, ...) {
  id receiver = *(id *)frameStackPointer;
  SEL sel = *(SEL *)(frameStackPointer + 4);

  Class receiverClass = object_getClass(receiver);

  if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
    id forwardingTarget = [receiver forwardingTargetForSelector:sel];
    if (forwardingTarget) {
      return objc_msgSend(forwardingTarget, sel, ...);
    }
  }

  const char *className = class_getName(object_getClass(receiver));
  const char *zombiePrefix = "_NSZombie_";
  size_t prefixLen = strlen(zombiePrefix);
  if (strncmp(className, zombiePrefix, prefixLen) == 0) {
    CFLog(kCFLogLevelError,
          @"-[%s %s]: message sent to deallocated instance %p",
          className + prefixLen,
          sel_getName(sel),
          receiver);
    <breakpoint-interrupt>
  }

  if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
    NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
    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.",
              sel_getName(sel),
              signatureIsStret ? "" : not,
              isStret ? "" : not);
      }
      if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
        NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature
                                                                          frame:frameStackPointer];
        [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;
      }
    }
  }

  const char *selName = sel_getName(sel);
  SEL *registeredSel = sel_getUid(selName);

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

其逻辑大致如下:

  • 1、调用 forwardingTargetForSelector,会返回一个新的 target,使用这个 target 作为新的 receiver 执行 selector,如果返回内容为 nil 或者和旧 receiver 一样,进行第 2 步。
  • 2、调用 methodSignatureForSelector 获取方法签名后,判断返回类型信息是否正确,然后调用 forwardInvocation 执行 NSInvocation 对象,并将结果返回。如果对象没实现 methodSignatureForSelector 方法,进行第 3 步。
  • 3、调用 doesNotRecognizeSelector 方法。

也就是说,调用了未实现的方法,如果在动态方法解析阶段未处理,我们还可以在消息转发阶段通过以下处理方式避免 crash,例如:

假设调用了当前类(AObject)中未实现的如下方法,且没有在动态方法解析阶段动态处理该方法:

 - (int)test:(int)arg;

我们现在将其转发到 BObject 中的如下相同方法:

- (int)test:(int)arg;

**方式 1:**在 AObject 类中实现如下方法转发到 BObject:

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(test:)) {
        // 返回一个可以处理方法的对象,需保证对象里有相同方法
        return [[BObject alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

如果 AObject 类没有实现该方法,还可以使用下面方式转发消息。

**方式 2:**在 AObject 类中实现如下方法执行 NSInvocation:

// 首先,调用如下方法返回对应方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(test:)) {
//        根据 Types 获取方法签名
//        return [NSMethodSignature signatureWithObjCTypes:"v20@0:8i16"];
//        return [NSMethodSignature signatureWithObjCTypes:"i@:i"];
//        如果 BObject 中实现了相同方法,可以使用下面方法获取方法签名
        return [[[BObject alloc] init] methodSignatureForSelector:aSelector];
    }
    return [super methodSignatureForSelector:aSelector];
}

// 然后在下面方法中做转发处理
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if (anInvocation.selector == @selector(test:)) {
//        方法 1:调用 BObject 中同名方法
//        [anInvocation invokeWithTarget:[[BObject alloc] init]];
        
//        方法 2:修改 anInvocation,改为调用 BObject 中其他方法
//        anInvocation.target == [[BObject alloc] init]
//        anInvocation.selector == xxxxTest:
//        [anInvocation invoke];

        
//        方法 3:直接使用 BObject 的对象调用 test 方法
//        [[[BObject alloc] init] test:xxxxx]
        
//        方法 4:什么都不做也可以,也不会 crash 了
        
    }
}

对于类方法的处理,也是一样的,只不过改调用对应类方法而已:

// 转发方式 1
+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(test:)) {
        // 注意,由于是类方法,所以这里应当返回 class,当然,如果想转发后调用 BObject 中的对象方法,这里就返回对象(例如:return [[BObject alloc] init];)即可
        return [BObject class];
    }

    return [super forwardingTargetForSelector:aSelector];
}

同理,如果 AObject 类没有实现上面方法,还可以使用下面方式转发消息:

// 转发方式 2
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        return [NSMethodSignature signatureWithObjCTypes:"i@:i"];
    }
    
    return [super methodSignatureForSelector:aSelector];
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 转发逻辑,或不做任何处理
}

NSInvocation 其他两个常用方法:

  • 获取参数
int arg;
// 如果只有一个参数 arg,那么这个参数的 index 是 2,即第三个参数,因为前两个参数是 self 和 _cmd
[anInvocation getArgument:&arg atIndex:2];
  • 获取返回值
int ret;
[anInvocation getReturnValue:&ret];

3、消息发送完整流程图

附上一张本人绘制的消息发送完整流程图:

lix_blog_49 拷贝.png

🔗 原文链接:
Runtime 之消息机制 - 李峰峰博客
📢 作者声明:
转载请注明来源