iOS runtime之objc_msgSend动态方法决议

453 阅读17分钟

前言

前面已经分析了objc_msgSend快速查找流程objc_msgSend慢速查找流程,本文就来探索动态方法决议。

准备工作

1: 动态方法决议相关源码解析

前文objc_msgSend慢速查找流程里已经分析过了,在慢速查找过程中,如果父类返回的是消息转发forward_imp或者所有父类都查找完之后也没有查找到对应的imp,就会执行一次动态方法决议。

// 篇幅原因,仅截取部分代码

NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    ...
    
    // No implementation found. Try method resolver once.
    // 1.父类缓存返回forward_imp,先不缓存执行一次动态方法决议
    // 2.所有父类都查找完了,也没查找到对应的imp,执行消息转发之前,执行一次动态方法决议
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }
    
    ...
}

  • 父类缓存快速查找返回forward_imp,先不缓存执行一次动态方法决议。
  • 所有父类都查找完了(父类已经为nil了),也没查找到对应的imp,执行消息转发之前,执行一次动态方法决议。

1.1: resolveMethod_locked源码解析

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

    runtimeLock.unlock();
    
    // 判断cls是否是元类,如果不是说明调用的是实例方法
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else { // 如果是元类,说明调用的是类方法
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        // 如果+resolveClassMethod:方法没有实现就找resolveInstanceMethod:方法
        // NSObject默认实现了这两个方法
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    // 动态方法决议可能已经填充了缓存(可能为动态方法决议添加的`imp`或者消息转发`forward_imp`),所以尝试使用它
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
  • 判断cls是否是元类元类
  • 不是元类就执行resolveInstanceMethod流程。
  • 是元类就先执行resolveClassMethod流程。
  • 动态方法决议可能已经填充了缓存(可能为动态方法决议添加的imp或者消息转发forward_imp),所以尝试使用它。

1.2: resolveInstanceMethod源码解析

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    // inst 对象  cls 类
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    
    // NSObject默认实现了+resolveInstanceMethod:方法
    // 在cls的元类查找+resolveInstanceMethod:方法,先快速,再慢速
    // 目的是将+resolveInstanceMethod:方法缓存到cls的元类中
    // 通过lookUpImpOrNilTryCache的参数我们知道`resolveInstanceMethod:`是类方法
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        return;
    }
    // 发送消息调用resolveInstanceMethod:方法
    // 如果动态决议给当前sel添加了imp,就会存入类的方法列表
    // 通过objc_msgSend发送消息,receiver是cls,说明是类方法
    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
    // 如果动态决议处理了,就缓存动态决议添加的imp到类中
    // 如果动态决议没处理就缓存forward_imp
    // 动态方法决议下次不会再触发
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
    
    if (resolved  &&  PrintResolving) {
        if (imp) { // imp存在,说明动态添加了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:方法创建sel
  • 调用lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))缓存resolveInstanceMethod:方法到cls的元类中。
  • objc_msgSend进行类型转换,然后给类发消息调用resolveInstanceMethod:方法,如果动态决议给当前sel添加了imp,就会存入类的方法列表。
  • 调用lookUpImpOrNilTryCache(inst, sel, cls),看缓存中是否有sel对应的imp,如果没有,就调用lookUpImpOrForward,动态方法决议处理了的话,就缓存动态方法决议添加的imp,动态方法决议没处理的话,就缓存消息转发的forward_imp,动态方法决议下次不会再触发。

1.3: resolveClassMethod源码解析

static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    // inst 类  cls 元类
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());
    
    // NSObject默认实现了+resolveClassMethod:方法
    // 在元类cls查找+resolveClassMethod:方法,先快速,再慢速
    // 目的是将+resolveClassMethod:方法缓存到元类cls中
    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);
        }
    }
    // 发送消息调用resolveClassMethod:方法
    // 如果动态决议给当前sel添加了imp,就会存入元类的方法列表
    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到元类中
    // 如果动态决议没处理就缓存forward_imp
    // 动态方法决议下次不会再触发
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

    if (resolved  &&  PrintResolving) {
        if (imp) { // imp存在,说明动态添加了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));
        }
    }
}
  • 调用lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)缓存resolveInstanceMethod:方法到元类cls中。
  • objc_msgSend进行类型转换,然后给类发消息调用resolveClassMethod:方法,如果动态决议给当前sel添加了imp,就会存入元类的方法列表。
  • 调用lookUpImpOrNilTryCache(inst, sel, cls),看缓存中是否有sel对应的imp,如果没有,就调用lookUpImpOrForward,动态方法决议处理了的话,就缓存动态方法决议添加的imp,动态方法决议没处理的话,就缓存消息转发的forward_imp,动态方法决议下次不会再触发。

1.4: lookUpImpOrNilTryCache源码解析

resolveInstanceMethod函数和resolveClassMethod函数中都会调用此函数。

extern IMP lookUpImpOrNilTryCache(id obj, SEL, Class cls, int behavior = 0);

IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
{
    return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}
  • 调用_lookUpImpTryCache根据sel查找impbehavior默认为0LOOKUP_NIL = 4behavior | LOOKUP_NIL大于等于LOOKUP_NIL

1.5: _lookUpImpTryCache源码解析

ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertUnlocked();

    if (slowpath(!cls->isInitialized())) {
        // see comment in lookUpImpOrForward
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

    IMP imp = cache_getImp(cls, sel);
    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
    if (slowpath(imp == NULL)) {
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

done:
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        return nil;
    }
    return imp;
}
  • 判断cls是否初始化,一般都已经初始化了。

快速查找流程

  • 根据selcls缓存中查找对应的imp
  • 如果imp存在就跳转done流程,``。
  • 不存在就先判断是否支持共享缓存,支持就到共享缓存中查找对应的imp
  • 如果缓存中没有查找到对应的imp,就进入慢速查找流程。

慢速查找流程

  • 慢速查找流程中,behavior = 4,LOOKUP_RESOLVER = 24 & 2 = 0,不会再次进入动态方法决议,所以不会形成死循环。
  • 慢速流程如果没有查找到,imp会被赋值为消息转发forward_imp,然后插入缓存,如果(behavior & LOOKUP_NIL) && imp == forward_imp),返回nil,否则返回forward_imp,进入消息转发流程(后续单独发文解析)。

done流程

  • 如果(behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache,就直接返回nil,否则返回imp,此判断条件说明了imp不为空一定是动态方法决议里面给sel动态添加了imp

1.6: lookUpImpOrForwardTryCache源码解析

extern IMP lookUpImpOrForwardTryCache(id obj, SEL, Class cls, int behavior = 0);

IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
    return _lookUpImpTryCache(inst, sel, cls, behavior);
}
  • 调用_lookUpImpTryCache根据sel查找impbehavior默认为0,此处为1,所以不会触发LOOKUP_NIL返回nil,而是返回动态方法决议添加的imp或是消息转发forward_imp

2: resolveInstanceMethod案例分析

2.1: 二次调用分析

还是前文objc_msgSend慢速查找流程的单身狗没有女朋友案例,创建SDSingleDog类,声明- (void)girlfriend方法,不实现,运行代码,单身狗开始找女朋友,没找到,他就崩溃了(充分说明了没有女朋友是一件很让人崩溃的事)。

@interface SDSingleDog : NSObject

- (void)girlfriend;

@end

@implementation SDSingleDog

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    NSLog(@"__对象方法动态决议来了%s__", __func__);

    return [super resolveInstanceMethod:sel];
}

@end

********************* 打印输出 *********************

2021-07-11 02:25:15.231564+0800 MsgResolve[11978:289761] __对象方法动态决议来了+[SDSingleDog resolveInstanceMethod:]__
2021-07-11 02:25:15.232133+0800 MsgResolve[11978:289761] __对象方法动态决议来了+[SDSingleDog resolveInstanceMethod:]__
2021-07-11 02:25:15.232250+0800 MsgResolve[11978:289761] -[SDSingleDog girlfriend]: unrecognized selector sent to instance 0x10049d700

在崩溃之前确实调用了+resolveInstanceMethod:方法,但是却调用了两次,第一次是方法慢速查找流程中触发的,那么第二次是怎么触发的呢?

image.png

第一次调用+resolveInstanceMethod:方法的堆栈信息,可以看到是方法慢速查找流程触发的动态方法决议。

image.png

第二次调用+resolveInstanceMethod:方法的堆栈信息,可以看到是由CoreFoundation库在消息转发流程完成以后再次开启方法慢速查找流程触发的方法动态决议,消息转发流程后续单独发文分析。

2.2: 动态添加对象方法

为了让单身狗不崩溃,我们给他加上一个备胎的方法,给他点希望。

@implementation SDSingleDog

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    NSLog(@"__对象方法动态决议来了%s__", __func__);
    if (sel == @selector(girlfriend)) {
        IMP imp = class_getMethodImplementation(self, @selector(spareTire));
        Method meth = class_getInstanceMethod(self, @selector(spareTire));
        const char *type = method_getTypeEncoding(meth);
        return class_addMethod(self, sel, imp, type);
    }
    return [super resolveInstanceMethod:sel];
}

- (void)spareTire
{
    NSLog(@"The single dog becomes spare tire");
}

@end

********************* 打印输出 *********************

2021-07-11 03:27:35.190669+0800 MsgResolve[13132:322186] __对象方法动态决议来了+[SDSingleDog resolveInstanceMethod:]__
2021-07-11 03:27:35.191228+0800 MsgResolve[13132:322186] ____The single dog becomes spare tire____
  • +resolveInstanceMethod:方法只调用了一次,因为resolveInstanceMethod函数将动态方法决议添加的imp缓存到了类中,lookUpImpOrForwardTryCache函数获取到了imp,直接调用imp,查找流程结束。
  • 崩溃也解决了,实现动态方法决议,系统给了我们一次机会。
  • 具体流程:resolveMethod_locked->resolveInstanceMethod->lookUpImpOrNilTryCache->+resolveInstanceMethod:->lookUpImpOrNilTryCache->lookUpImpOrForwardTryCache->调用imp

3: resolveClassMethod案例分析

3.1: 二次调用分析

SDSingleDog类声明一个+ (void)getMarried方法,不实现,运行代码,单身狗想结婚,很显然,他又崩溃了。

@interface SDSingleDog : NSObject

+ (void)getMarried;

@end

@implementation SDSingleDog

+ (BOOL)resolveClassMethod:(SEL)sel
{
    NSLog(@"__类方法动态决议来了%s__", __func__);

    return [super resolveClassMethod:sel];
}

@end

********************* 打印输出 *********************

2021-07-11 16:35:53.973393+0800 MsgResolve[1441:24199] __类方法动态决议来了+[SDSingleDog resolveClassMethod:]__
2021-07-11 16:35:53.974148+0800 MsgResolve[1441:24199] __类方法动态决议来了+[SDSingleDog resolveClassMethod:]__
2021-07-11 16:35:53.974246+0800 MsgResolve[1441:24199] +[SDSingleDog getMarried]: unrecognized selector sent to class 0x100008188
  • 在崩溃之前确实调用了+resolveClassMethod:方法,而且调用了两次,调用两次的逻辑和+resolveInstanceMethod:方法是一致的。

  • 调用resolveClassMethod函数后,会调用lookUpImpOrNilTryCache函数去查找有没有为sel动态添加的imp,元类缓存中此时sel对应的impforward_implookUpImpOrNilTryCache里面的判断直接返回nil,此时再调用resolveInstanceMethod函数查找,因为根元类的父类也是根类。

  • 如果+resolveClassMethod:方法和+resolveInstanceMethod:方法都没有为当前sel动态添加imp,调用lookUpImpOrForwardTryCache函数就会返回消息转发forward_imp,进入消息转发流程。

3.2: 动态添加类方法

为了让单身狗不崩溃,我们给他加上一个相亲的方法,单身狗也有很多很优秀的,他们只是太忙了,没时间找女朋友,通过相亲,希望他们可以获得美好的婚姻。

@implementation SDSingleDog

+ (BOOL)resolveClassMethod:(SEL)sel
{
    NSLog(@"__类方法动态决议来了%s__", __func__);
    if (sel == @selector(getMarried)) {
        IMP imp = class_getMethodImplementation(self, @selector(blindDate));
        Method meth = class_getClassMethod(self, @selector(blindDate));
        const char *type = method_getTypeEncoding(meth);
        return class_addMethod(object_getClass(self), sel, imp, type);
    }
    return [super resolveClassMethod:sel];
}

+ (void)blindDate
{
    NSLog(@"____The single dog began to blind date____");
}

@end

********************* 打印输出 *********************

2021-07-11 17:29:31.416153+0800 MsgResolve[2387:51634] __类方法动态决议来了+[SDSingleDog resolveClassMethod:]__
2021-07-11 17:29:31.416787+0800 MsgResolve[2387:51634] ____The single dog began to blind date____
  • +resolveClassMethod:方法只调用了一次,因为resolveClassMethod函数将动态方法决议添加的imp缓存到了类中,lookUpImpOrForwardTryCache函数获取到了imp,直接调用imp,查找流程结束。
  • 崩溃也解决了,实现动态方法决议,系统给了我们一次机会。
  • resolveClassMethodresolveInstanceMethod流程大致相同,只是如果+resolveClassMethod:没有为当前sel动态添加imp就会调用一次resolveInstanceMethod

3.3: resolveClassMethod流程分析

源码逻辑显示调用resolveClassMethod函数没有查找到动态添加的imp,就会再调用一次resolveInstanceMethod,那么在SDSingleDog类中同时实现+ (BOOL)resolveInstanceMethod:(SEL)sel方法和+ (BOOL)resolveClassMethod:(SEL)sel方法,理论上应该都会调用。

@implementation SDSingleDog

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    NSLog(@"__对象方法动态决议来了%s__", __func__);
    return [super resolveInstanceMethod:sel];
}

+ (BOOL)resolveClassMethod:(SEL)sel
{
    NSLog(@"__类方法动态决议来了%s__", __func__);
    return [super resolveClassMethod:sel];
}

@end

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

        [SDSingleDog getMarried];
        
    }
    return 0;
}

********************* 打印输出 *********************

2021-07-11 18:11:26.621227+0800 KCObjcBuild[3722:76122] __类方法动态决议来了+[SDSingleDog resolveClassMethod:]__
2021-07-11 18:11:26.621915+0800 KCObjcBuild[3722:76122] __类方法动态决议来了+[SDSingleDog resolveClassMethod:]__
2021-07-11 18:11:26.622111+0800 KCObjcBuild[3722:76122] +[SDSingleDog getMarried]: unrecognized selector sent to class 0x1000082b0

输出结果出乎我们的意料,只是调用了+resolveClassMethod:方法,下面就跟着流程来分析一下。

image.png

  • instSDSingleDog类,clsSDSingleDog元类,如果+resolveClassMethod:方法没有动态为当前sel动态添加imp,就会调用resolveInstanceMethod函数。

image.png

  • 调用lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))+resolveInstanceMethod:方法缓存到元类cls(SDSingleDog(metaclass))isa指向的根元类NSObject(metaclass)中。

  • 给元类cls(SDSingleDog(metaclass))发送+resolveInstanceMethod:的消息,首先到根元类NSObject(metaClass)中查找,但是系统默认实现的在根类NSObject(class)中,只是返回了NO,我们实现的在类SDSingleDog(class)中(方法存在元类SDSingleDog(metaclass)),所以不会走SDSingleDog(class)类中实现的+resolveInstanceMethod:方法。

  • 调用lookUpImpOrNilTryCache函数->_lookUpImpTryCache函数,默认参数behavior | LOOKUP_NIL,将+resolveInstanceMethod:方法动态添加的imp或消息转发的forward_imp缓存到元类SDSingleDog(metaclass)中(此处因为根本没有调用到+resolveInstanceMethod:方法,所以缓存的是消息转发的forward_imp)。

由上面的分析推测如果在根类NSObject的分类中添加+resolveInstanceMethod:方法,在+resolveClassMethod:方法调用之后,因为没有为当前sel动态添加imp,所以也会调用到根类NSObject分类的+resolveInstanceMethod:方法。

@implementation NSObject (Goddess)

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(getMarried)) {
        NSLog(@"__对象方法动态决议来了%s__", __func__);
    }
    return NO;
}

@end

********************* 打印输出 *********************

2021-07-11 23:59:45.932837+0800 KCObjcBuild[10139:220823] __类方法动态决议来了+[SDSingleDog resolveClassMethod:]__
2021-07-11 23:59:45.933468+0800 KCObjcBuild[10139:220823] __对象方法动态决议来了+[NSObject(Goddess) resolveInstanceMethod:]__
2021-07-11 23:59:45.933814+0800 KCObjcBuild[10139:220823] __类方法动态决议来了+[SDSingleDog resolveClassMethod:]__
2021-07-11 23:59:45.933883+0800 KCObjcBuild[10139:220823] __对象方法动态决议来了+[NSObject(Goddess) resolveInstanceMethod:]__
2021-07-11 23:59:45.933990+0800 KCObjcBuild[10139:220823] +[SDSingleDog getMarried]: unrecognized selector sent to class 0x1000082d0

跟推测的一样,先调用+resolveClassMethod:方法,再调用+resolveInstanceMethod:方法,由于没有动态添加imp,所以都是两次。

4: 动态方法决议整合

经过上面分析,+resolveClassMethod:方法中没有为当前sel动态添加imp的话,也会调用+resolveInstanceMethod:方法,那么就可以建立一个公共类,实现+resolveInstanceMethod:方法,使类方法对象方法都能调用。

已知方法查找流程

  • 对象方法查找流程:->父类->...->根类(NSObject)->nil
  • 类方法查找流程:元类->父元类->...->根类(NSObject)->nil

由方法查找流程可知,所有方法最后都会查找到根类(NSObject)中,所以我们就可以定义一个NSObject的分类来充当动态方法决议的公共类。

@implementation NSObject (Goddess)

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(getMarried)) {
        NSLog(@"__类方法动态决议来了%s__", __func__);
        IMP imp = class_getMethodImplementation(object_getClass(self), @selector(blindDate));
        Method meth = class_getClassMethod(object_getClass(self), @selector(blindDate));
        const char *type = method_getTypeEncoding(meth);
        return class_addMethod(object_getClass(self), sel, imp, type);
    } else if (sel == @selector(girlfriend)) {
        NSLog(@"__对象方法动态决议来了%s__", __func__);
        IMP imp = class_getMethodImplementation(self, @selector(spareTire));
        Method meth = class_getInstanceMethod(self, @selector(spareTire));
        const char *type = method_getTypeEncoding(meth);
        return class_addMethod(self, sel, imp, type);
    }
    return NO;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        SDSingleDog *singleDog = [SDSingleDog alloc];
        [singleDog girlfriend];
        [SDSingleDog getMarried];
    }
    return 0;
}

********************* 打印输出 *********************

2021-07-12 00:26:38.410687+0800 KCObjcBuild[10752:236964] __对象方法动态决议来了+[NSObject(Goddess) resolveInstanceMethod:]__
2021-07-12 00:26:38.411218+0800 KCObjcBuild[10752:236964] ____The single dog becomes spare tire____
2021-07-12 00:26:38.411350+0800 KCObjcBuild[10752:236964] __类方法动态决议来了+[NSObject(Goddess) resolveInstanceMethod:]__
2021-07-12 00:26:38.411517+0800 KCObjcBuild[10752:236964] ____The single dog began to blind date____
@end

对象方法和类方法的动态决议最后调用到了+resolveInstanceMethod:方法中,和上面分析的结果吻合。

动态方法决议的优点:

  • 统一处理方法找不到的崩溃问题,APP出现方法崩溃时可以上报服务器或者跳转到首页,增加APP的健壮性。

这种编程方式叫面向切面编程,即AOP

AOPOOP的区别

  • OOP(面向对象编程)针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。
  • AOP(面向切面编程)是处理某个步骤和阶段的,从中进行切面的提取,有重复的操作行为,AOP就可以提取出来,运用动态代理,实现程序功能的统一维护,依赖性小,耦合度小,单独把AOP提取出来的功能移除也不会对主代码造成影响。AOP更像一个三维的纵轴,平面内的各个类有共同逻辑的通过AOP串联起来,本身平面内的各个类没有任何的关联。

5: 动态方法决议流程图

动态方法决议流程图.jpg

6: 日志辅助调试

方法快速查找和慢速查找都没有查找到对应的imp,动态方法决议也没有动态添加对应的imp,就会进入消息转发的流程,但是从上面分析可知消息转发流程是CoreFunction实现的,而CoreFunction提供的源码既不全面也没有对应的工程,添加到现有工程搜索_CF_forwarding_prep_0函数、___forwarding___函数和doesNotRecognizeSelector方法都无法找到。这种情况下就需要使用苹果的日志辅助功能了(arm64架构不支持此方法)。

通过lookUpImpOrForward->log_and_fill_cache->logMessageSend查看源码。

static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    // objcMsgLogEnabled为YES才能调用logMessageSend
    if (slowpath(objcMsgLogEnabled && implementer)) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cls->cache.insert(sel, imp, receiver);
}

objcMsgLogEnabledYES才能调用logMessageSend函数。

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))
    {
        // 日志文件路径
        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;
}

日志文件的沙盒路径是/tmp/msgSends,写入后直接到沙盒路径下就可以获取到,objcMsgLogEnabled默认为false,所以需要找到赋值的地方。

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

调用instrumentObjcMessageSends函数可以为objcMsgLogEnabled赋值,所以在需要日志信息的地方公开声明下instrumentObjcMessageSends,既extern void instrumentObjcMessageSends(BOOL flag),就可以调用此函数控制日志写入了。

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        SDSingleDog *singleDog = [SDSingleDog alloc];
        // 在我们需要记录日志的方法前开启
        instrumentObjcMessageSends(YES);
        [singleDog girlfriend];
        // 在我们需要记录日志的方法后关闭,防止多余信息混淆我们
        instrumentObjcMessageSends(NO);
    }
    return 0;
}

d0e226ce806247339d88642842657954~tplv-k3u1fbpfcp-watermark.image.png

根据日志文件我们发现在动态方法决议之后的消息转发流程有forwardingTargetForSelectormethodSignatureForSelector两个方法,后续将单独发文分析消息转发流程。

7: 总结

动态方法决议是苹果为了程序的健壮性而设计的一种容错处理机制,在相关方法没有实现时,给开发者提供动态实现的机会,同时也为开发者提供了更多新的尝试的可能性。