Objective-C 动态方法决议

上篇文章分析了 消息慢速查找 流程,当消息找不到的时候会执行_objc_msgForward_impcache汇编代码。最终调用到_objc_forward_handler进行报错处理,那么在报错之前能够进行处理么?

一、动态方法决议

imp没有找到的时候的时候会赋值libobjc.A.dylib_objc_msgForward_impcache`,首先会进入如下代码逻辑:

  if (slowpath(behavior & LOOKUP_RESOLVER)) {
      behavior ^= LOOKUP_RESOLVER;
      //要查找的对象,方法,类,1
      return resolveMethod_locked(inst, sel, cls, behavior);
  }
  • 这其实可以理解为一个单类,相同流程只会进入一次。
  • behavior上篇文章已经分析,值中有LOOKUP_INITIALIZE|LOOKUP_RESOLVER进入后异或LOOKUP_INITIALIZE|LOOKUP_RESOLVER^ LOOKUP_RESOLVER = LOOKUP_INITIALIZE,相当于清空了LOOKUP_RESOLVER
  • resolveMethod_locked参数最后一个是LOOKUP_INITIALIZE

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()) {
        //这里的cls是类
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            //这里的cls是元类
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    //又会去查找一次,既然这里又会去查找一次,那么肯定有什么地方会加入之前查找不存在的方法。
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
  • 当快速和慢速消息查找都没有找到的时候进入了resolveMethod_locked
  • 查找的是实例方法则进行对象方法动态决议resolveInstanceMethod
  • 查找的是类方法则先进行类方法动态决议resolveClassMethod,再执行resolveInstanceMethod(这里resolveInstanceMethod调用与实例方法的resolveInstanceMethod参数不同。)。
  • 最后会调用lookUpImpOrForwardTryCache查找。

核心问题是最后要返回imp,那么先看下lookUpImpOrForwardTryCache进行的操作:

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

只是一个简单的调用,继续排查:

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

    //是否初始化,正常情况下是已经初始化了。
    if (slowpath(!cls->isInitialized())) {
        // see comment in lookUpImpOrForward
        //这就是慢速消息查找流程,与之前的区别是 behavior = LOOKUP_INITIALIZE,没有动态方法决议参数了。
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }
    //缓存查找
    IMP imp = cache_getImp(cls, sel);
    //找到直接跳转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)) {
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

done:
    //是否消息转发
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        return nil;
    }
    //返回imp
    return imp;
}
  • isInitialized正常情况是不会进入的。
  • 先去缓存查找对应的imp,找到直接返回。
  • 没有找到会去动态共享缓存查找(如果支持)。
  • 仍然没有会进行lookUpImpOrForward也就是再进行一次慢速消息查找。

既然这个函数也是进行快速和慢速消息查找的,那么就说明resolveInstanceMethodresolveClassMethod可以在某个时机将方法加入类中。这样后面方法的调用才有意义。

二、对象方法动态决议 resolveInstanceMethod

通过源码分析发现在进行了快速与慢速消息查找后如果找不到imp,苹果仍然给了机会进行resolveInstanceMethod处理,那么核心肯定是要给类中添加imp,源码如下:

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    //先进行元类查找是否实现了`resolveInstanceMethod`实例方法,也就是类的类方法。没有实现直接返回,这里不会返回,因为NSobject默认实现了。
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    //系统自动发送了`resolveInstanceMethod`消息,由于消息的接受者是类,所以是+方法。
    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 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));
        }
    }
}
  • 先进行元类resolveInstanceMethod的查找和缓存。
  • 系统自动给类发送了resolveInstanceMethod消息。既然是类调用的,那么就是+方法。
  • 接着进行了快速慢速方法查找imp,但是没有返回imp(为什么不返回?这里只是缓存,如果有的话)。
  • lookUpImpOrNilTryCachelookUpImpOrForwardTryCache唯一的区别是是否进行动态转发。这里不进行动态转发。
  • 可以看到返回的resolved只是进行了日志打印。也就是resolved返回YES/NO对功能没有影响。

那么就有个问题?
既然查找了imp为什么不进行返回操作?而resolveInstanceMethod调用结束后还查了一次?

2.1 + (BOOL)resolveInstanceMethod 调试分析

resolveInstanceMethod源码跟踪流程如下:

  • cls->ISA元类也就是HPObject元类中查找有没有实现resolveInstanceMethod-imp,最终会找到NSObject元类然后将resolveInstanceMethod-imp缓存写入HPObject元类的缓存。(NSObject默认实现了)。
  • 给类发送resolveInstanceMethod消息。
  • HPObject查找instanceMethod有没有实现,没有实现会将instanceMethod-_objc_msgForward_impcache(IMP)写入HPObject缓存。这个时候由于LOOKUP_NIL的存在返回的是`nil。
  • 如果lookUpImpOrNilTryCache没有找到imp会返回继续执行lookUpImpOrForwardTryCache继续进行缓存->消息慢速查找流程(消息慢速查找不会执行)。因为前面已经写入了对应缓存。这次会从缓存中获取到imp_objc_msgForward_impcacheimp。不会进入消息慢速查找流程,直接进行了消息转发。

这就说明resolveInstanceMethod中首先元类查找resolveInstanceMethod,目的是将resolveInstanceMethod写入缓存。然后类发送resolveInstanceMethod消息。接着lookUpImpOrNilTryCache调用是将的imp加入缓存中(无论是否找到,找不到会存入_objc_msgForward_impcache)。返回后lookUpImpOrForwardTryCache从缓存中找方法返回。

结论:resolveInstanceMethod中lookUpImpOrNilTryCache只是将方法插入缓存,返回后lookUpImpOrForwardTryCache从缓存中获取imp 这也是调用两次的原因。

2.2 + (BOOL)resolveInstanceMethod 实现

既然系统已经给了+ (BOOL)resolveInstanceMethod:(SEL)sel进行容错处理,那么就实现下:

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"resolveInstanceMethod: %@-%@",self,NSStringFromSelector(sel));
    return [super resolveInstanceMethod:sel];
}

调用后发现这个方法调用了两次:

resolveInstanceMethod: HPObject-instanceMethod
resolveInstanceMethod: HPObject-instanceMethod
  • HPObject的元类中能找到resolveInstanceMethod方法,缓存的直接是自己的imp了。
  • 仍然是没有命中进行了消息转发。

消息转发会进入class_getInstanceMethod

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

又进行了一次lookUpImpOrForward所以这也是调用了两次的原因。但是这次不进行消息转发了,所以不会造成死循环。

总结:第一次没有命中后,再进行消息转发后又会进行一次lookUpImpOrForward消息慢速查找流程,所以resolveInstanceMethod会执行两次。

那么如果实现中添加了imp就肯定只调用一次了。
修改代码如下:

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

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"HPObject resolveInstanceMethod: %@-%@",self,NSStringFromSelector(sel));
    if (sel == @selector(instanceMethod)) {
        IMP instanceMethod1 = class_getMethodImplementation(self, @selector(instanceMethod1));
        Method method = class_getInstanceMethod(self, @selector(instanceMethod1));
        const char *type = method_getTypeEncoding(method);
        return class_addMethod(self, sel, instanceMethod1, type);
    }
    return NO;
}

按照源码理解在lookUpImpOrNilTryCache调用中只是增加到了缓存中,后面lookUpImpOrForwardTryCache会从缓存中查找,找到imp然后执行。

+ (BOOL)resolveInstanceMethod:(SEL)sel返回NO/YES根据源码来看只是打印日志相关的内容,应该是没有影响的。经过调试验证确实没有影响。

结论:

  • resolveInstanceMethod 调用中只是对方法的缓存,lookUpImpOrNilTryCache 从缓存中再次查找方法。这也是为什么会查找两次的原因。
  • resolveInstanceMethod 执行两次的原因是,在方法没有命中的时候消息转发过程中会再次进行lookUpImpOrForward(消息慢速查找),这就是执行两次的原因。
  • + (BOOL)resolveInstanceMethod:(SEL)sel 返回值不会影响功能,只是对日志打印有影响,并且默认情况下是不打印日志的。

三、类方法动态决议resolveClassMethod

在上面最开始分析的时候类方法动态决议会先调用resolveClassMethod,如果没有命中那么就会调用resolveInstanceMethod

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

resolveClassMethod的实现如下:

static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());
    //不会进入这里,先查找元类是否实现`resolveClassMethod`
    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) {//......}
}
  • 第一个lookUpImpOrNilTryCache查找元类是否实现,先将resolveClassMethod插入HPObject元类的缓存中。resolveClassMethodNSObject默认实现了。
  • 操作元类,防止元类没有实现。
  • 元类中是以对象方法存在,所以在类中实现类方法就可以了。系统主动给类方法发送+ resolveClassMethod消息。这里细节的一点是通过nonmeta来发送消息。
  • lookUpImpOrNilTryCache查找目标imp,先缓存后慢速。查找到后将imp插入缓存,没有找到则将_objc_msgForward_impcache插入缓存。

实现如下:

+ (BOOL)resolveClassMethod:(SEL)sel {
    NSLog(@"resolveClassMethod: %@-%@",self,NSStringFromSelector(sel));
    return [super resolveClassMethod:sel];
}

调用后发现打印了8次:

resolveClassMethod: HPObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: HPObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: HPObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: HPObject-classMethod
resolveClassMethod: HPObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: HPObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: HPObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: HPObject-classMethod
  • 其中encodeWithOSLogCoder与我们无关,classMethod出现两次符合预期(另外一次消息转发过程中调用)。
  • 当调用resolveClassMethod没有实现的时候,就调用resolveInstanceMethod去查找(这里的cls参数是元类,与查找实例方法不同),仍然没有找到就执行lookUpImpOrForwardTryCache
  • 最后在消息转发的时候会再执行一次方法动态决议。

修改实现:

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

+ (BOOL)resolveClassMethod:(SEL)sel {
    NSLog(@"resolveClassMethod: %@-%@",self,NSStringFromSelector(sel));
    if (sel == @selector(classMethod)) {
        IMP classMethod1 = class_getMethodImplementation(objc_getMetaClass("HPObject"), @selector(classMethod1));
        Method method = class_getClassMethod(self, @selector(classMethod1));
        const char *type = method_getTypeEncoding(method);
        return class_addMethod(objc_getMetaClass("HPObject"), sel, classMethod1, type);
    }
    return [super resolveClassMethod:sel];
}

输出:

resolveClassMethod: HPObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: HPObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: HPObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: HPObject-classMethod
+[HPObject classMethod1]

这个时候就调用一次了。

既然resolveClassMethod找不到的时候会执行一次resolveInstanceMethod,那意味者可以在resolveInstanceMethod中对类方法进行处理。

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"HPObject resolveInstanceMethod: %@-%@",self,NSStringFromSelector(sel));
    return NO;
}

+ (BOOL)resolveClassMethod:(SEL)sel {
    NSLog(@"HPObject resolveClassMethod: %@-%@",self,NSStringFromSelector(sel));
    return [super resolveClassMethod:sel];
}

这个时候调试发现resolveInstanceMethod并没有执行。为什么?因为这里是HPObject元类调用resolveInstanceMethod

根据isa的走位图,NSObject同时也是元类,那么元类调用+方法就要存到元类的元类中也就是存在根元类的元类,那么就是NSObject自己,通过NSObjectresolveInstanceMethod方法就可以实现了。
添加一个NSObject的分类,实现方法:

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

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

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"resolveInstanceMethod: %@-%p-%@",self,self,NSStringFromSelector(sel));
    if (sel == @selector(instanceMethod)) {
        IMP instanceMethod1 = class_getMethodImplementation(self, @selector(instanceMethod1));
        Method method = class_getInstanceMethod(self, @selector(instanceMethod1));
        const char *type = method_getTypeEncoding(method);
        return class_addMethod(self, sel, instanceMethod1, type);
    } else if (sel == @selector(classMethod)) {
        IMP classMethod1 = class_getMethodImplementation(objc_getMetaClass("HPObject"), @selector(classMethod1));
        Method method = class_getInstanceMethod(objc_getMetaClass("HPObject"), @selector(classMethod1));
        const char *type = method_getTypeEncoding(method);
        return class_addMethod(objc_getMetaClass("HPObject"), sel, classMethod1, type);
    }
    return NO;
}

分别调用instanceMethodclassMethod输出如下:

HPObject:0x1000082c8, HPMetaObject:0x1000082a0, NSObject:0x100358140, NSMetaObject:0x1003580f0

resolveInstanceMethod: HPObject-0x1000082c8-instanceMethod //类
-[NSObject(Additions) instanceMethod1]
resolveInstanceMethod: HPObject-0x1000082a0-classMethod //元类
HPObjcTest[59242:11857560] +[NSObject(Additions) classMethod1]

这样就在NSObjectresolveInstanceMethod中即处理了类方法也处理了实例方法。两次调用参数不同,一次是类调用,一次是元类调用。

⚠️如果两个都实现在HPObject类中,则都是类调用。

总结

  • resolveClassMethod 调用中只是对方法的缓存,lookUpImpOrNilTryCache会从缓存中再次查找方法,这也是为什么会查找两次的原因。
  • resolveClassMethod 执行两次的原因是在方法没有命中的时候消息转发过程中会再次进行lookUpImpOrForward(消息慢速查找),再次走这个流程。这就是执行两次的原因。LOOKUP_NIL有值,所以不会再次消息转发,不会造成死循环。)
  • resolveClassMethod 没有命中的时候会先调用resolveInstanceMethod(这里的cls是元类),再次调用时因为NSObject是元类的父类。
  • 这里resolveInstanceMethod由于是元类调用,所以只能实现在NSObject的分类中。(根元类的元类是自己,它的父类是NSObject
  • + (BOOL)resolveClassMethod:(SEL)sel 返回值不会影响功能,只是对日志打印有影响。

三、aop & oop

那么动态方法决议的意义在哪里呢?
这是苹果在sel查找imp找不到的时候给的一次解决错误的机会。有什么意义呢?在NSObject的分类中,所有找不到的OC方法都能在resolveInstanceMethod中监听到。
那么在自己的工程中可以根据类名前缀、模块以及事物进行区分prefix_ module_traffic。当发现有问题的时候可以进行容错处理并且上报错误信息。 比如HP_Setting_didClickLogin出现问题的时候进行上报,当超过阈值时进行报警。

这种方式就是aop切面编程。我们比较习惯的方式是oop

oop
oop分工非常明确,耦合度小,冗余代码。一般情况下会提取公共的类,但是遵循后会对它有强依赖,强耦合。
这些其实不是我们关心的,我们更关心业务的内容,所以公共类尽量少侵入,最好无侵入。通过动态方式注入代码,对原始方法没有影响。这就相当于整个切面切入了,要切入的方法和类就是切点。aopoop的延伸。

aop
aop的缺点在上面的例子中是if-else过多冗余。正如上面看到的那样,方法会调用很多次浪费了相应的性能。如果命中还好,没有命中会走多次,会有性能消耗。它是消息转发机制的前一个阶段。意味着如果在这里做了容错处理,后面的流程就被切掉了。苹果写转发流程就没有意义了。

如果其它模块也做了相应处理,重复了这块不一定会执行到。所以在后面的流程做aop更合理。

四、消息转发流程

如果最终动态方法决议也没有找到imp呢?动态方法决议会返回imp,这个时候的imp是指向_objc_msgForward_impcache的。

那么这个时候后面的流程怎么执行呢?

可以通过声明一个函数instrumentObjcMessageSends打印系统调用的方法的列表,调用和声明方式如下:

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        HPObject *obj = [HPObject alloc];
        instrumentObjcMessageSends(YES);
        [obj instanceMethod];
        instrumentObjcMessageSends(NO);
    }
    return 0;
}

在源码中它的实现如下:

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

作用是什么呢?
搜索objcMsgLogEnabled会发现在开启的情况下会在/tmp/msgSends-%d中写下日志:

image.png

调用输出结果如下:
image.png

可以看到调用了非常多的方法,其中resolveInstanceMethod已经是熟悉的了。其它的是消息转发流程的方法了。这里很遗憾的是看不到参数。

  • 根据日志可以看到在methodSignatureForSelector后再次进行了resolveInstanceMethod
  • 根据源码分析可知,应该是有两个方法被调用。
  • 在动态方法决议后消息转发流程包含方法:
forwardingTargetForSelector:
methodSignatureForSelector:
doesNotRecognizeSelector:

那么在源码中调用跟踪下参数呢?
既然都是调用的NSObject的方法不防在NSObject里面打断点,根据之前的调试也能判断出来应该是

encodeWithOSLogCoder:options:maxLength

验证确实是:


image.png

消息转发整个流程将在下篇文章详细分析。

动态方法决议整个流程图:


动态方法决议流程

你可能感兴趣的:(Objective-C 动态方法决议)