OC底层学习之动态方法决议与消息转发

在上篇方法快速查找和慢速查找中我们介绍了方法查找得整个流程,也留下了疑问,也就是如果在所有关联的类中都没有找到对应的方法时,会返回__objc_msgForward_impcache,那么今天我们就来看看这之后又会是怎样的流程

一.分析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

可以看到上面的一系列的汇编代码就是返回x17,那么关键就是 _objc_forward_handler,但是走到这里就全局搜索不到了,这时我们该怎么处理了?可以尝试去掉下划线,因为有可能此时已经不是走汇编代码了,前面的篇章中也有类似情况,最终找到了_objc_fatal

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

相信里面的内容大家看着都会有点眼熟,平时写代码时偶尔也会犯错,申明了方法,没有实现,调用的时候就会报这个错误,可参考下图

在这里插入图片描述

二.动态方法决议

我们在上一篇章方法快速查找和慢速查找里面有提到过在慢速查找过程中,如果找不到imp的话,会执行resolveMethod_locked,接下来去看下内部实现

1.resolveInstanceMethod

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

    runtimeLock.unlock();
    // 程序员 你是不是傻 没有这个方法 - imp-nil
    // 奔溃 - 友善
    // 给你一次机会 拯救地球 -> imp
    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 (!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);
}

可以看到最终是返回lookUpImpOrForwardTryCache函数的调用返回值

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

其实也就是_lookUpImpTryCache,再进去看下

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

其实光看这个函数名就大概能猜出来是干嘛的,其实就是再尝试一次快速查找cache_getImp,但是大家仔细点会发现,在快速查找之前还做了一个调用,也就是resolveMethod_locked函数中,调用了resolveInstanceMethod,我们再看下这个函数

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

    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        return;
    }

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

其实这个函数后面部分和第一部分内容相似,也是一种情况的问题描述打印,主要是看前面几句代码,其实就是创建了sel->resolveInstanceMethod:的进行消息发送的过程,发送发成后,再进行一次lookUpImpOrNilTryCache,也就是消息快速查找,所以说这也是给了我们动态修改方法的一次机会,接下来我们测试一下

LGTeacher *p = [LGTeacher alloc];
[p say666];

LGTeacher 并没有实现方法say666,然后再LGTeacher.m文件中添加以下代码

- (void)sayNB{
    NSLog(@"YCX--%@ - %s",self , __func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    // 处理 sel -> imp
    if (sel == @selector(say666)) {
        IMP sayNBImp     = class_getMethodImplementation(self, @selector(sayNB));
        Method method    = class_getInstanceMethod(self, @selector(sayNB));
        const char *type = method_getTypeEncoding(method);
        return class_addMethod(self, sel, sayNBImp, type);
    }
    
    NSLog(@"resolveInstanceMethod :%@-%@",self,NSStringFromSelector(sel));

    return [super resolveInstanceMethod:sel];
}

打印结果

2021-07-05 15:09:28.193337+0800 KCObjcBuild[8607:134342] resolveInstanceMethod :LGTeacher-encodeWithOSLogCoder:options:maxLength:
2021-07-05 15:09:28.194054+0800 KCObjcBuild[8607:134342] YCX--<LGTeacher: 0x10063cd90> - -[LGTeacher sayNB]

2.resolveClassMethod

通过上面的测试之后,我们还需要注意一点,在我们OC层面是有所谓的对象方法和类方法的区别的,并且2种方法的存放位置也并不一样,所以在执行resolveInstanceMethod之前,其实是有做判断的

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

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

可以发现其实和resolveInstanceMethod内容差不多,只不过对象变成了元类,所以多了Class nonmeta元类的相关操作,所以说如果想要动态修改类方法,只需要在类中重写resolveClassMethod,因为类的类方法正好就是存在元类中,同样的测试一下

+ (BOOL)resolveClassMethod:(SEL)sel{
    if (sel == @selector(sayHappy)) {
		NSLog(@"YCX---resolveClassMethod :%@-%@",self,NSStringFromSelector(sel));

        IMP sayNBImp     = class_getMethodImplementation(objc_getMetaClass("LGTeacher"), @selector(sayKC));
        Method method    = class_getInstanceMethod(objc_getMetaClass("LGTeacher"), @selector(sayKC));
        const char *type = method_getTypeEncoding(method);
        return class_addMethod(objc_getMetaClass("LGTeacher"), sel, sayNBImp, type);
    }

    return [super resolveClassMethod:sel];
}

以上代码就是把类方法sayHappy 改成执行类方法sayKC

注意self要改成元类objc_getMetaClass(“LGTeacher”)

当然如果是类方法的话,再执行resolveClassMethod之后,你会发现还会有这段代码

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

其实结合之前的讲到的isa走位图,就可以理解,说白了就是类的类方法存在元类中,那么在元类调用执行了动态消息转发后,也需要判断元类中是否存在此方法,如果没有也需要快速查找父类方法,所以就执行lookUpImpOrNilTryCache进行判断,如果还是没有,再进行对方方法的动态方法修改也就是执行resolveInstanceMethod,当然基本上不会存在这种情况会走到这里!只是为了让整个流程更加符合设计原理(我是这么认为的)

疑问:其实在执行resolveMethod_locked的时候,我们会发现,在进行动态方法决议之后,本身就执行了_lookUpImpTryCache也就是快速查找方法流程,但是为什么在最后return时,还要执行一次呢

3.instrumentObjcMessageSends

为什么会突然讲到这里呢,不知道大家对之前这个log_and_fill_cache函数是否还有印象,这个是在方法查找到之后,调用进行方法缓存的过程,代码如下

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

可能我们都只会注意到最后一句cache.insert,缓存插入,但是上面其实有个函数的调用logMessageSend,看这个名字可能就大概知道是干什么的了,消息发送的日志,点进去看也确实如此

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,通过全局搜索找到如下函数

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

所以如果我们想知道方法调用时,消息的转发流程,就可以这样做,代码如下,编译之后查找对应目录文件,就可以看到在调用方法say666,过程调用了函数

extern void instrumentObjcMessageSends(BOOL flag);

LGPerson *person = [LGPerson alloc];
instrumentObjcMessageSends(YES);
[LGPerson say666];
instrumentObjcMessageSends(NO);

当然了,为了更清楚的知道整个执行流程,这个方法say666并没有写实现,最终通过生成的文件打开发现了新的函数执行

+ LGPerson NSObject resolveInstanceMethod:
+ LGPerson NSObject resolveInstanceMethod:
- LGPerson NSObject forwardingTargetForSelector:
- LGPerson NSObject forwardingTargetForSelector:
- LGPerson NSObject methodSignatureForSelector:
- LGPerson NSObject methodSignatureForSelector:
- LGPerson NSObject class
+ LGPerson NSObject resolveInstanceMethod:
+ LGPerson NSObject resolveInstanceMethod:
- LGPerson NSObject doesNotRecognizeSelector:
- LGPerson NSObject doesNotRecognizeSelector:
- LGPerson NSObject class

可以看到在调用resolveInstanceMethod之后又执行了forwardingTargetForSelectormethodSignatureForSelector

4.快速转发流程

通过官方文档的注释,可以看出这是方法快速转发流程,在通过Rumtime的动态方法决议,还没有解决问题时,就会执行这里,我们也可以测试一下

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        [person sayHello];
        NSLog(@"Hello, World!");
    }
    return 0;
}
@implementation LGPerson
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    return [LGStudent alloc];
}
@end

还是一样,LGPerson没有方法的实现,通过在方法
forwardingTargetForSelector中,返回LGStudent对象,那么那就会在LGStudent中去寻找方法的实现

5.慢速转发流程

慢速转发流程是快发转发流程之后还是没有找到对应的方法实现才会执行,通过开发文档的描述可知,需要搭配forwardInvocation使用

methodSignatureForSelector返回一个方法签名

// 慢速转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(sayHello)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}

官方文档的注释

Discussion
This method is used in the implementation of protocols. This method is also used in situations where an NSInvocation object must be created, such as during message forwarding. If your object maintains a delegate or is capable of handling messages that it does not directly implement, you should override this method to return an appropriate method signature.

该方法用于协议的实现。此方法还用于必须创建NSInvocation对象的情况,例如在消息转发期间。如果您的对象维护委托或能够处理它不直接实现的消息,则应该重写此方法以返回适当的方法签名。

Related Documentation  关联文档
- forwardInvocation:
Overridden by subclasses to forward messages to other objects. 
被子类重写以将消息转发到其他对象。

forwardInvocation被子类重写以将消息转发到其他对象。

由于文档中的描述很多,这里我就不copy过来了
释义 :如果当一个对象收到一条没法响应方法的消息时,将有机会把消息委托给另一个接受者,通过NSInvocation对象并向接收方发送forwardInvocation,当然如果另一个也不能响应,也可以继续发送

参考文档中的使用方法,写出如下代码

- (void)forwardInvocation:(NSInvocation *)anInvocation{
//    NSLog(@"%@ - %@",anInvocation.target,NSStringFromSelector(anInvocation.selector));
    LGStudent *s = [LGStudent alloc];
    if ([self respondsToSelector:anInvocation.selector]) {
        [anInvocation invoke];
    }else if ([s respondsToSelector:anInvocation.selector]){
        [anInvocation invokeWithTarget:s];
    }else{
        NSLog(@"%s - %@",__func__,NSStringFromSelector(anInvocation.selector));
    }
}

目的就是判断当前对象是否能够响应,可以的话就直接传递,如果不行就在判断LGStudent是否可以,再不行就是没有找到方法,结果就是调用了方法却没有执行

三.aop 与 oop 编程

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

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

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

  • aop 面向切面编程(Aspect Oriented Programming)
    aop无侵入式的动态注入代码,只需要找到切入的点,也就是切入的方法,切入的类,但是缺点在上面resolveClassMethod的例子中也可以看到,侵入的方法会很多,也就导致了需要很多if-else的判断,会有性能消耗,代码也会过多冗余。如果命中还好,没有命中会走多次,会有性能消耗。它是消息转发机制的前一个阶段。意味着如果在这里做了容错处理,后面的流程就被切掉了。苹果写转发流程就没有意义了。

如果其它模块也做了相应处理,重复了这块不一定会执行到。所以在后面的流程做aop更合理。也就是上面提到的消息转发流程里面去处理。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值