IOS底层(十五): 消息流程(三)动态方法决议和消息转发

OC底层源码/原理合集

建议先看下
IOS底层(十四): 消息流程(二)慢速查找

  • sel : 方法编号, 可以理解成一本书的目录, 可通过对应名称找到页码

  • imp : 函数指针地址, 可以理解成书的页码, 方便找到具体实现的函数

慢速查找核心源码lookUpImpOrForward

NEVER_INLINE
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 (slowpath(!cls->isInitialized())) {
        // The first message sent to a class is often +new or +alloc, or +self
        // which goes through objc_opt_* or various optimized entry points.
        //
        // However, the class isn't realized/initialized yet at this point,
        // and the optimized entry points fall down through objc_msgSend,
        // which ends up here.
        //
        // We really want to avoid caching these, as it can cause IMP caches
        // to be made with a single entry forever.
        //
        // Note that this check is racy as several threads might try to
        // message a given class for the first time at the same time,
        // in which case we might cache anyway.
        
        
        behavior |= LOOKUP_NOCACHE;
    }

    // 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.lock();

    // We don't want people to be able to craft a binary blob that looks like
    // a class but really isn't one and do a CFI attack.
    //
    // To make these harder we want to make sure this is a class that was
    // either built into the binary or legitimately registered through
    // objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
    checkIsKnownClass(cls);

    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    // runtimeLock may have been dropped but is now locked again
    runtimeLock.assertLocked();
    curClass = cls;

    // The code used to lookup the class's cache again right after
    // we take the lock but for the vast majority of the cases
    // evidence shows this is a miss most of the time, hence a time loss.
    //
    // The only codepath calling into this without having performed some
    // kind of cache lookup is class_getInstanceMethod().

    for (unsigned attempts = unreasonableClassCount();;) {
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
            // curClass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }

            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                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.");
        }

        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            break;
        }
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }

    // No implementation found. Try method resolver once.

    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
 done_unlock:
    runtimeLock.unlock();
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

书接上回

动态方法决议

  • 慢速查找流程未找到后(即父类链, 元类链都没找到),会执行一次动态方法决议

  • 动态方法决议可以理解成, 再给一次机会重新再来一次, 看看能不能找到imp

    // 此次动态方法决议控制条件, 主要用于判断流程只走一次
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        // LOOKUP_RESOLVER = 2, 这里做的一个异或操作
        // 如果a、b两个值不相同,则异或结果为1。
        // 如果a、b两个值相同,异或结果为0。 
        behavior ^= LOOKUP_RESOLVER;  
        return resolveMethod_locked(inst, sel, cls, behavior);
    }


/***********************************************************************
* resolveMethod_locked
* Call +resolveClassMethod or +resolveInstanceMethod.
*
* Called with the runtimeLock held to avoid pressure in the caller
* Tail calls into lookUpImpOrForward, also to avoid pressure in the callerb
**********************************************************************/
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());
   
    runtimeLock.unlock();
    
    // 当前方法没有未实现会走动态方法决议(可以理解成没找到再给次机会)
    // 例如  1. performSelector:@selector(sayGunDan), 但是.h/.m  sayGunDan方法并不存在
    // 2. .h存在, .m没有实现方法, 也会走动态方法决议


    // 判断当前类是否为元类
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        
        // 详细见下方: 源码分析一: resolveInstanceMethod 实例方法的动态方法决议
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]

        // 详细见下方: 源码分析二: resolveClassMethod 类方法的动态方法决议
        resolveClassMethod(inst, sel, cls);

        // 
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}


源码分析一: resolveInstanceMethod 实例方法的动态方法决议

如果当前类不是元类, 走resolveInstanceMethod

/***********************************************************************
* 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 resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    // lookUpImpOrNilTryCache 底层 lookUpImpTryCache, 
    // 判断方法是否有 resolveInstanceMethod 这个方法, 不存在直接return
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        return;
    }

    // 发送一个消息, resolve_sel是上面的 SEL resolve_sel = @selector(resolveInstanceMethod:);

    // 这个其实可以看我后面的例子, 其实是对resolveInstanceMethod这里面方法进行查找
    // resolveInstanceMethod 可以重写, 自定义里面内容
    // 如果 resolveInstanceMethod 写了方法并有imp , 则再次进行次后面的lookUpImpOrForward, 就能找到

    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

    // 调用lookUpImpOrNilTryCache, 再给次机会, 看下方法是否存在
    IMP imp = lookUpImpOrNilTryCache(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));
        }
    }
}



IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
{
    return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}

/***********************************************************************
* lookUpImpOrForward / lookUpImpOrForwardTryCache / lookUpImpOrNilTryCache
* The standard IMP lookup.
*
* The TryCache variant attempts a fast-path lookup in the IMP Cache.
* Most callers should use lookUpImpOrForwardTryCache with LOOKUP_INITIALIZE
*
* Without LOOKUP_INITIALIZE: tries to avoid +initialize (but sometimes fails)
* With    LOOKUP_NIL: returns nil on negative cache hits
*
* inst is an instance of cls or a subclass thereof, or nil if none is known.
*   If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use 
*   must be converted to _objc_msgForward or _objc_msgForward_stret.
*   If you don't want forwarding at all, use LOOKUP_NIL.
**********************************************************************/
ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertUnlocked();

    // 快速查找判断类是否初始化 
    if (slowpath(!cls->isInitialized())) {
        // see comment in lookUpImpOrForward
        // 没有回去重新调用 lookUpImpOrForward(里面前部分有针对初始化处理)
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

    // 已经初始化, 调用cache_getImp
    IMP imp = cache_getImp(cls, sel);

    // 如果 imp != null, 走done方法
    if (imp != NULL) goto done;
#if CONFIG_USE_PREOPT_CACHES
    // 如果有缓存, 缓存查找
    if (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) {
        imp = cache_getImp(cls->cache.preoptFallbackClass(), sel);
    }
#endif

    // 快速查找, 如果 imp还是没找到
    if (slowpath(imp == NULL)) {
        // 调用 lookUpImpOrForward
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

done:
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        return nil;
    }
    return imp;
}
IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
    // 底层还是调用 lookUpImpOrForward
    return _lookUpImpTryCache(inst, sel, cls, behavior);
}

流程图

动态方法决议流程图

总结: 实例方法动态方法决议

  • 慢速查找流程未找到方法实现时,会进行一次动态方法决议resolveMethod_locked
  • 如果不是元类, 执行实例方法resolveInstanceMethod

验证下动态方法决议 resolveInstanceMethod

建一个SAPerson类, .h里面写一个实例方法say666, .m中不给他实现
(用这个方法也行 performSelector:@selector(), 主要是不给实现)

SAPerson.h
SAPerson.m

main中初始化SAPerson, 调用say666方法

main

正常情况下会报方法找不到错误


报错

SAPerson.m 方法里面重写下动态决议方法resolveInstanceMethod, 运行我们再看一下

resolveInstanceMethod
resolveInstanceMethod 运行

可看到虽然还是报错, 因为resolveInstanceMethod中并没有给设置或者写什么imp。接下来补全一下, 写一个sayHello方法, 当say666来了将其指向sayHello, 运行可看到

补全动态方法决议

(留意下只是大致模拟, 真正动态方法决议更复杂)

其实留意下会发现我上面的例子, 会有个疑问为什么没有指定方法, 动态方法决议走了2次? 而指定了方法( if (sel == @selector(say666)) ), 动态方法走了1次。

只走一次

这块要等消息转发讲完之后才能解释



源码分析二: resolveClassMethod 类的动态方法决议

如果当是元类, 走resolveClassMethod

/***********************************************************************
* 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 resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());

    if (!lookUpImpOrNilTryCache(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 = lookUpImpOrNilTryCache(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));
        }
    }
}

可看到大致跟实例的类似, 当然我们也可以重写的动态方法决议留。留意下类方法的话, 要取元类, 方法用objc_getMetaClass()

决议类方法

由于类方法底层其实也是写成实例方法, 所以其实我们可以将动态方法决议正好到一起

注意: 不能整合之后不能放入SAPerson中, 因为类方法在元类中以对象方法存在, 元类查询路线依次按 元类 → 根元类 → NSObject, 而不会在当类查找, 所以不会走当前类的resolveInstanceMethod。所以这块要写在NSObject 分类(Category )方法里面(分类执行在类方法执行前面), 如下
动态决议整合

动态决议整合调用结果

这么重写resolveInstanceMethod, 主要处理找不到等情况, 当然重写了系统方法也会存在一些问题等。针对于系统方法被修改, 我们可以针对自定义类中方法统一方法名的前缀,根据前缀来判断是否是自定义方法,然后统一处理自定义方法, 例如: sa_model_xxxclick (前缀所在模块名称所做事物), 这样我们可以针对sa前缀进行hook, 可以在crash崩溃前pop到首页或者指定模块(封装界面 → 界面SDK上传),用于app线上防崩溃的处理等,提升体验。



总结

经过快速查找, 慢速查找都没有找到实现方法, 那么系统会给我一次机会, 即动态方法决议

  • 判断类是否是元类

    • 如果是类,执行实例方法的动态方法决议resolveInstanceMethod
    • 如果是元类,执行类方法的动态方法决议resolveClassMethod,如果在元类中没有找到或者为空,则在元类的实例方法的动态方法决议resolveInstanceMethod中查找,主要是因为类方法在元类中是实例方法,所以还需要查找元类中实例方法的动态方法决议
  • 如果动态方法决议中,如果找到, 则正常返回对应 imp

  • 如果动态方法决议中,如果没有找到, 则进入消息转发



消息转发

针对于慢速查找imp指针找不到, 动态方法决议也没有处理的方法, 接下来走消息转发。但是其实查看源码可看到并无消息转发的源码, 那么它到底是怎么执行的呢?

快速消息转发

lookUpImpOrForward --> log_and_fill_cache --> logMessageSend, 在logMessageSend源码下方找到instrumentObjcMessageSends的源码实现

bool objcMsgLogEnabled = false;
static int objcMsgLogFD = -1;

bool logMessageSend(bool isClassMethod,
                    const char *objectsClass,
                    const char *implementingClass,
                    SEL selector)
{
    char    buf[ 1024 ];

    // Create/open the log file
    if (objcMsgLogFD == (-1))
    {
        // 留意下这里, objcMsgLogFD首次进入时候为-1, /tmp/msgSends是文件的写入目录
        snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
        objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
        if (objcMsgLogFD < 0) {
            // no log file - disable logging
            objcMsgLogEnabled = false;
            objcMsgLogFD = -1;
            return true;
        }
    }

    // Make the log entry
    snprintf(buf, sizeof(buf), "%c %s %s %s\n",
            isClassMethod ? '+' : '-',
            objectsClass,
            implementingClass,
            sel_getName(selector));

    objcMsgLogLock.lock();
    write (objcMsgLogFD, buf, strlen(buf));
    objcMsgLogLock.unlock();

    // Tell caller to not cache the method
    return false;
}



// 相当于开关, 调用是传YES, 用完传NO
void instrumentObjcMessageSends(BOOL flag)
{
    bool enable = flag;

    // Shortcut NOP
    if (objcMsgLogEnabled == enable)
        return;

    // If enabling, flush all method caches so we get some traces
    if (enable)
        _objc_flush_caches(Nil);

    // Sync our log file
    if (objcMsgLogFD != -1)
        fsync (objcMsgLogFD);

    objcMsgLogEnabled = enable;
}
  1. 建立一个main项目IOS创建个只有main.m工程, 里面建一个继承NSObject的类SATest, .h里面有一个sayHello, .m中不给他实现方法, main中调用

2.写入 instrumentObjcMessageSends方法, 但是由于main中调用objc-class里面方法, 是不允许的, 所以需要借用extern, extern void instrumentObjcMessageSends(BOOL flag);这样保证正常调用

instrumentObjcMessageSends

先运行一下依旧崩溃, 但是我们前往下/tmp/msgSends(文件写入目录), 可看到里面有个msgSends-XXXXX文件

msgSends文件位置

打开msgSends, 这里简单介绍下

msgSends文件

  • resolveInstanceMethod: 动态决议方法, 执行2次
  • forwardingTargetForSelector: 消息快速转发方法, 执行2次,
  • methodSignatureForSelector: 消息慢速转发, 执行2次

那么我们可以模拟一下, SATest.m中加入forwardingTargetForSelector方法

forwardingTargetForSelector

forwardingTargetForSelector 运行

可看到虽然crash但是, 的确走了forwardingTargetForSelector方法, 说明我们探索方向是正确的, 接下来先看下cmd + shift + 0, 查下forwardingTargetForSelector解释

forwardingTargetForSelector定义

可看到 forwardingTargetForSelector返回未识别消息应首先指向的对象, 通俗讲就是, 我们这个消息没人接收的话, 返回其第一继承人/接收者, 再通俗讲就是, 这个消息没人接收找个人接盘。

那么我们找个人来接收这个方法即可, 建立一个新类SATest1(继承于NSObject), 里面创建个sayHello方法

SATest1
SATest1调用

可看到正常运行。留意下这里不能调用自身 [SATest alloc]会循环引用。

慢速消息转发

快速消息转发 forwardingTargetForSelector这里也没有找到, 则走慢速消息转发methodSignatureForSelector。还是先看下定义, cmd + shift + 0, 查下methodSignatureForSelector

methodSignatureForSelector定义

最下边的forwardInvocation也留意下子类重写以将消息转发到其他对象。

forwardInvocation定义

返回一个NSMethodSignature对象(不能为空),该对象包含由Selector标识的方法。首先还是看一下该方法有没有走, SATest中加methodSignatureForSelector方法, 运行

methodSignatureForSelector

虽然还是报错不过可看到, 的确在forwardingTargetForSelector之后走了methodSignatureForSelector方法, 那么接下来我们返回个签名即可

forwardInvocation

加一个forwardInvocation, 可看到正常打印

接下来我们看一下NSInvocation

@interface NSInvocation : NSObject

+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;

@property (readonly, retain) NSMethodSignature *methodSignature;

- (void)retainArguments;
@property (readonly) BOOL argumentsRetained;

@property (nullable, assign) id target;
@property SEL selector;

我们可以根据NSInvocation打印获取部分信息

NSInvocation打印

这块其实可以这样理解, 例如sayHello方法, 最后大家都不接受, 那么就装到 Invocation里面, Invocation相当于一个漂流瓶(Invocation相当于保存), 漂流出去, 谁爱接收谁接收。当然我们也可以给他一个target

NSInvocation target

最后一段invoke是触发开关, 什么时候调用都行。



总结

先看下 动态决议, 消息转发流程图

消息转发

一起总结下 objc_msgSend 发送消息的流程

  • 消息发送首先进行快速查找流程, 在类的缓存cache中查找指定方法。

  • 快速查找流程没有查找到走慢速查找流程, 实例方法在当前类以及类继承链方法列表查找, 类方法在元类以及元类继承链方法列表查找。

  • 慢速查找流程没有查找到会进行次动态方法决议

  • 动态方法决议没有查找到会进行次消息转发, 快速消息转发慢速消息转发2次补救机会。

  • 消息转发也没找到的话, imp = forward_imp, 走Crash崩溃报方法找不到异常: unrecognized selector sent to instance



附一

上面留下个动态方法决议走2次的问题


动态方法决议走2次

在源码resolveInstanceMethod加断点, 每次走了say666时在lldbbt读一下内存段

bt打印

在第二次中,我们可以看到是[NSObject(NSObject) methodSignatureForSelector:]class_getInstanceMethod 再次进入动态方法决议, 看下他的源码

/***********************************************************************
* class_getInstanceMethod.  Return the instance method for the
* specified class and selector.
**********************************************************************/
Method class_getInstanceMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    // This deliberately avoids +initialize because it historically did so.

    // This implementation is a bit weird because it's the only place that 
    // wants a Method instead of an IMP.

#warning fixme build and search caches
        
    // Search method lists, try method resolver, etc.
    lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);

#warning fixme build and search caches

    return _class_getMethod(cls, sel);
}

我们在class_getInstanceMethod加个断点, 看一下

class_getInstanceMethod

能看出的确方法走了这里, 而且在第二次打印say666之前走的。苹果在走到慢速查找签名invocation之前,又再去查询查询一次,所以走到class_getInstanceMethod这里,又去走了一遍lookUpImpOrForward方法查询 say666 , 然后会再次走到动态方法决议。

接下来加forwardingTargetForSelector, methodSignatureForSelector, forwardInvocation方法, 主要验证下是在慢速查找之前, 还是之后, 可看到

image.png

也可看到第二次动态方法决议在慢速查找methodSignatureForSelectorforwardInvocation 方法之间。

你可能感兴趣的:(IOS底层(十五): 消息流程(三)动态方法决议和消息转发)