前言

一、动态方法决议解析

二、打开系统日志

三、消息转发 - 快速转发流程

四、消息转发 - 慢速转发流程

五、动态方法解析&消息转发流程图总结

六、动态方法决议执行2次的原因


经过对消息慢速查找流程分析之后,我们知道如果还是没有找到方法,最终是会走objc_defaultForwardHandler抛出异常。那么在慢速查找失败到抛出异常的中间,还会有一层动态方法解析和消息转发机制给开发者提供补救的机会。接下来就重点分析一下动态方法解析和消息转发机制的整体流程。

为了方便探索问题,我创建一个测试工程,建立4个类

其中MyTestObject 继承MyBaseTestObject。MyOtherObject只继承NSObject。还有一个NSObject的分类。

一、动态方法决议解析

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

在lookUpImpForward跳出查找循环时,会进入resolveMethod_locked方法: (简写)

//最多可能进入2次,第2次是在 返回签名和forwardinvocation 方法之间。
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    //判断是否是元类
    if (! cls->isMetaClass()) {
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }
    //如果没有补救,走lookUpImpOrForwardTryCache
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

从resolveMethod_locked来看,如果不是元类,会走resolveInstanceMethod。

如果是元类会走resolveClassMethod 同时如果再次进入慢速查找之后没有找到,又会走一次 resolveInstanceMethod。(因为元类的父类是根元类,根元类的父类是NSObject,所以最终会去再次查找NSObject的实例方法,也就会再走一次resolveInstanceMethod)。这里可以在NSObject+Test分类里面重写resolveInstanceMethod来验证。

下面我们只分析resolveInstanceMethod,因为resolveClassMethod大同小异(找的是元类的继承链)

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    //一般不会走这里,因为NSObject会默认实现
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        return;
    }
    //objc_msgSend发送resolveInstanceMethod消息
    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
    //再次查找sel对应的imp有没有
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
    //resolveInstanceMethod 这里resolved只是判断打印。没有什么实际的意义。
    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的源码分析,动态方法解析主要目的是为了再给一次机会重新查询,那我们在这个方法中需要做的事情就是给当前类或者当前类的继承链中添加一个方法。(注意一定是要给要查找的sel绑定imp)。
看下例子:

main.m
 MyTestObjcet *objc = [[MyTestObjcet alloc] init];
 [objc test];

@implementation MyTestObjcet

- (void)test1 {
    NSLog(@"来了");
}

//sel = test
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    //获取test1的方法(主要是为了拿到方法签名)
    Method m = class_getInstanceMethod(self, @selector(test1));
    //拿到方法签名
    const char *type = method_getTypeEncoding(m);
    //获取test1的imp
    IMP imp = class_getMethodImplementation(self, @selector(test1));
    //通过传进来的sel和获取到的imp,给当前类添加方法
    class_addMethod(self, sel, imp, type);
    return true;
}
@end

在上面的例子中,最后添加方法时候,同样可以class_addMethod(MyBaseTestObject.class, sel, imp, type); 因为在消息慢速查找的过程中,我们已经分析过了整个查找流程,当前类的方法列表二分查找未找到的时候,会去先查父类的缓存,如果还未找到,再次二分查找父类的方法列表。

resolveClassMethod 的坑点

main.m
 [MyTestObjcet performSelector:@selector(test)];

+ (void)test1 {
    NSLog(@"来了");
}

//sel = test
+ (BOOL)resolveClassMethod:(SEL)sel {
    //获取test1的方法(主要是为了拿到方法签名)
    //Method m = class_getClassMethod(self, @selector(test1)); 或者下面的
    Method m = class_getInstanceMethod(objc_getMetaClass("MyTestObjcet"), @selector(test1));
    //拿到方法签名
    const char *type = method_getTypeEncoding(m);
    //获取test1的imp 注意:这里需要获取到元类的imp
    IMP imp = class_getMethodImplementation(objc_getMetaClass("MyTestObjcet"), @selector(test1));
    //通过传进来的sel和获取到的imp,给当前类的元类添加方法
    class_addMethod(objc_getMetaClass("MyTestObjcet"), sel, imp, type);
    
    return true;
}

动态方法解析做简单的AOP切面

同过isa的走位图,我们可以知道,在NSObject的分类里面重写resolveClassMethod和resolveInstanceMethod,每一个没有实现的方法都会进入。
所以我们在NSObject的分类里面可以利用resolveClassMethod和resolveInstanceMethod做简单的切面。
但是这种方式不是很友好,因为如果项目比较大,其他的开发人员很可能不知道已经做了这层切面处理,自己又去实现了一套,这样子的话就会出现混乱的问题。
那么具体的AOP实现,后面会具体的分析。

二、打开系统日志

动态方法解析之后,貌似代码流程结束了,并没有发现有去调用消息转发的方法。在lookUpImpOrForward方法开始的时候const IMP forward_imp = (IMP)_objc_msgForward_impcache; 但是 forward_imp这个流程是在哪里进行的呢?

	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

通过汇编代码也没有发现什么流程,证明这部分代码并没有再objc的源码里面。

//1: objcMsgLogEnabled 控制开关
//2: 被外界知道 extern

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

通过这个方法,我们打开日志来看下。

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyTestObjc *test = [[MyTestObjc alloc] init];
        instrumentObjcMessageSends(true);
        [test test5];
        instrumentObjcMessageSends(false);
    }
    return 0;
}

将instrumentObjcMessageSends这个方法暴漏出来。

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/下会产生日志。直接command+G 来到tmp路径下

可以看到多了一个msgSends-84139文件。(注意这里需要是基于终端的项目,否则不会打印日志)。

通过文件内容可以看到 resolveInstanceMethod 之后会执行forwardingTargetForSelector快速转发流程。


三、消息转发 - 快速转发流程

//类方法
+ (id)forwardingTargetForSelector:(SEL)aSelector {
}
//实例方法
- (id)forwardingTargetForSelector:(SEL)aSelector {
}

在forwardingTargetForSelector中,主要是返回一个转发对象。
我们平时开始过程中,肯定知道forwardingTargetForSelector,但是上面分析到这个方法并不在objc的源码中,那么我们应该探索它究竟在哪里。
我们通过堆栈分析

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGABRT
    frame #0: 0x00007fff6113133a libsystem_kernel.dylib`__pthread_kill + 10
    frame #1: 0x00007fff61166e60 libsystem_pthread.dylib`pthread_kill + 430
    frame #2: 0x000000010d8a9b94 libsystem_c.dylib`abort + 120
    frame #3: 0x000000010d4ff818 libc++abi.dylib`abort_message + 231
    frame #4: 0x000000010d4f0e7d libc++abi.dylib`demangling_terminate_handler() + 266
    frame #5: 0x000000010bf0f0d1 libobjc.A.dylib`_objc_terminate() + 96
    frame #6: 0x000000010d4fec47 libc++abi.dylib`std::__terminate(void (*)()) + 8
    frame #7: 0x000000010d5013d0 libc++abi.dylib`__cxxabiv1::failed_throw(__cxxabiv1::__cxa_exception*) + 27
    frame #8: 0x000000010d501397 libc++abi.dylib`__cxa_throw + 113
    frame #9: 0x000000010bf0ef9c libobjc.A.dylib`objc_exception_throw + 340
    frame #10: 0x000000010c08d6f7 CoreFoundation`-[NSObject(NSObject) doesNotRecognizeSelector:] + 132
    frame #11: 0x000000010c083036 CoreFoundation`___forwarding___ + 1489
    frame #12: 0x000000010c085068 CoreFoundation`__forwarding_prep_0___ + 120
  * frame #13: 0x000000010b6fc0ae forward_test`main(argc=1, argv=0x00007ffee4503e00) at main.m:21:9
    frame #14: 0x000000010d77f3e9 libdyld.dylib`start + 1
(lldb) 

可以看到___forwarding___是在CoreFoundation中。CoreFoundation源码
在CF的源码中也没有找到forwarding的相关源码,应该是没有开源,那么通过继续通过反编译来探索。
首先 image list 查看所有镜像文件,找到CF可执行文件的路径:/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
将CF可执行文件拖进Hopper(反汇编工具)里面,然后搜索__forwarding_prep_0___

loc_649bb:
    var_148 = r13;
    var_138 = r12;
    var_158 = rsi;
    rax = object_getClass(rbx);
    r12 = rax;
    r13 = class_getName(rax);
    if (class_respondsToSelector(r12, @selector(forwardingTargetForSelector:)) == 0x0) goto loc_64a67;

可以看到伪代码 forwardingTargetForSelector出现了。
如果forwardingTargetForSelector没有实现,继续跟

loc_64dd7:
    rbx = class_getSuperclass(r12);
    r14 = object_getClassName(r14);
    if (rbx == 0x0) {
            _CFLog(0x4, @"*** NSForwarding: warning: object %p of class '%s' does not implement methodSignatureForSelector: -- did you forget to declare the superclass of '%s'?", var_138, r14, object_getClassName(var_138), r9, stack[-360]);
    }
    else {
            _CFLog(0x4, @"*** NSForwarding: warning: object %p of class '%s' does not implement methodSignatureForSelector: -- trouble ahead", var_138, r14, r8, r9, stack[-360]);
    }
    goto loc_64e3c;

可以看到报错。其余的流程就不在这里具体分析了。


四、消息转发 - 慢速转发流程

//实例方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {

}
//实例方法调用
- (void)forwardInvocation:(NSInvocation *)anInvocation {

}
//类方法签名
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {

}
//类方法调用
+ (void)forwardInvocation:(NSInvocation *)anInvocation {

}

//实例方法未找到
- (void)doesNotRecognizeSelector:(SEL)aSelector {

}
//类方法未找到
+ (void)doesNotRecognizeSelector:(SEL)aSelector {

}

慢速转发流程,需要先返回方法签名,这里方法签名一定要对应将要转发的方法,否则当invoke的时候会报错。
在慢速消息转发的过程中,我们可以控制invocation什么调用,比较灵活。

五、动态方法解析&消息转发流程图总结


注意:
1、即使forwardInvocation没有去invoke,也不会崩溃,但是forwardInvocation这个方法必须实现。所以是在方法签名没有返回的情况下才会崩溃。
2、doesNotRecognizeSelector方法是最终没有找到方法,但是不一定走到doesNotRecognizeSelector就会崩溃,因为如果在forwardinvocation中没有去invoke是的时候,会走到doesNotRecognizeSelector,但是不会崩溃。

六、动态方法决议执行2次的原因

如果再第一次resolveInstanceMethod方法中没有处理,在forwardingTargetForSelector中也没有处理。但是methodSignatureForSelector返回了签名信息,则会在methodSignatureForSelector之后,forwardInvocation之前再走一次resolveInstanceMethod。
比如下面的代码,就可以在正确返回方法签名之后,再走一次resolveInstanceMethod进行补救。

//保存方法签名
char *cType;

- (void)test1 {
    NSLog(@"test1");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (cType == NULL) {
        return true;
    }
    //获取test1的imp
    IMP imp = class_getMethodImplementation(self, @selector(test1));
    //通过传进来的sel和获取到的imp,给当前类添加方法
    class_addMethod(self, sel, imp, cType);
    return true;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return  nil;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    cType = "v16@0:8";
    return [NSMethodSignature signatureWithObjCTypes:cType];
}