Objective-C 动态方法决议

2021-07-02  本文已影响0人  HotPotCat

上篇文章分析了 消息慢速查找 流程,当消息找不到的时候会执行_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);
  }

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

核心问题是最后要返回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;
}

既然这个函数也是进行快速和慢速消息查找的,那么就说明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));
        }
    }
}

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

2.1 + (BOOL)resolveInstanceMethod 调试分析

resolveInstanceMethod源码跟踪流程如下:

这就说明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

消息转发会进入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根据源码来看只是打印日志相关的内容,应该是没有影响的。经过调试验证确实没有影响。

结论:

三、类方法动态决议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) {//......}
}

实现如下:

+ (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

修改实现:

+ (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类中,则都是类调用。

总结

三、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已经是熟悉的了。其它的是消息转发流程的方法了。这里很遗憾的是看不到参数。
forwardingTargetForSelector:
methodSignatureForSelector:
doesNotRecognizeSelector:

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

encodeWithOSLogCoder:options:maxLength

验证确实是:


image.png

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

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


动态方法决议流程
上一篇 下一篇

猜你喜欢

热点阅读