iOS-OC底层09:动态方法决议 & 消息转发

前沿

我们在oc层面调用对象方法实质是向某对象发送消息也就是objc_msgSend,objc_msgSend需要找到对应方法的实现也就是函数指针IMP,查找IMP首先在缓存中查找也就是快速查找,然后慢速查找也就是在类的方法类表中查找,如果这两种方法都找不到IMP,则在源码中有lookUpImpOrForward,可以看到会走resolveMethod_locked函数也就是动态方法决议

动态方法决议

在NSObject头文件中有两个方法去实现动态方法决议

//类方法动态决议
+ (BOOL)resolveClassMethod:(SEL)sel ;
//对象方法动态决议
+ (BOOL)resolveInstanceMethod:(SEL)sel;

我们看一下在OC底层是怎么调用动态决议方法的

对象方法动态决议resolveInstanceMethod

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

    // lookup resolveInstanceMethod
    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        // 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 = lookUpImpOrNil(inst, sel, cls);

}

1.先慢速查询resolveInstanceMethod的IMP,如果查到进行下一步,如果查不到能返回
2.调用resolveInstanceMethod方法

  1. resolveInstanceMethod之后重新查询方法的IMP,并返回

类方法动态决议resolveClassMethod

我们需要注意一点,在调用resolveClassMethod代码

   resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNil(inst, sel, cls)) {  // 为什么要有这行代码
            resolveInstanceMethod(inst, sel, cls);
        }

通过继承树我们知道类方法的最后继承自NSObject,会最终调用NSObject的实例方法(对象方法)

动态方法决议使用

我们可以在resolveInstanceMethod或resolveClassMethod方法中添加对应方法的实现,在慢速查询方法中,调用resolveInstanceMethod方法之后查询sel的IMP,如果我们在resolveInstanceMethod方法中添加方法,所以在查询IMP时就能找到,程序就能正常运行。

动态方法决议项目中的运行

我们可以在NSObject 的resolveInstanceMethod收集信息,如果类方法和对象方法都过继承树都要走到NSObject的resolveInstanceMethod,我们可以添加NSObject的Category在来实现resolveInstanceMethod。我们可以在resolveInstanceMethod中收集崩溃信息,我们也可以添加一个静态IMP,来防止程序崩溃。
如果动态方法决议不能解决方法未找到的情况就要走消息转发了
示例如下

 LGPerson *person = [LGPerson alloc];
   [person sayNB];
//sayNB只声明不实现打印日志如下

-[LGPerson sayNB]: unrecognized selector sent to instance 0x1018560a0
//实现resolveInstanceMethod
void sayNB(){
    NSLog(@"12345");
    
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(sayNB)) {
       return  class_addMethod([self class], sel, sayNB, nil);
    }
    return [super resolveInstanceMethod:sel];
}
//打印结果如下
//2020-09-25 17:15:38.507991+0800 KCObjc[61134:553937] 12345

消息转发

消息转发分快速转发和慢速转发

快速转发forwardingTargetForSelector:

如果动态方法决议中没有给对应方法加IMP,则会走快速转发流程
快速转发就是让这个消息让其他对象去接收

-(id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(sayNB)) {
        
        return [LGTeacher new];
    }
    return [super forwardingTargetForSelector:aSelector];
}
//LGTeacher实现sayNB方法
//打印日志
//我是老师,我当然牛逼

如果返回的对象时nil则[LGPerson sayNB]: unrecognized selector sent to instance

慢速转发

当在快速转返回nil时,则进入最后的一次挽救机会,重写methodSignatureForSelector,

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"%s",__func__);
    NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
    
    return signature;
}
-(void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%s",__func__);
}
打印结果
 ---+[LGPerson resolveInstanceMethod:]--sayNB
-[LGPerson methodSignatureForSelector:]
---+[LGPerson resolveInstanceMethod:]--_forwardStackInvocation:
-[LGPerson forwardInvocation:]

我们在forwardInvocation中只是简单的做一个NSLog,程序就不会崩溃。
我们也可以对invocation事务进行处理,修改invocation的target为[LGStudent alloc],调用 [anInvocation invoke] 触发 即LGPerson类的say666实例方法的调用会调用LGStudent的say666方法。
动态方法决议和消息转发的流程图


iOS-OC底层09:动态方法决议 & 消息转发_第1张图片
消息转发.png

从日志出发查看方法动态方法决议和消息转发的先后顺序

我们在lookUpImpOrForward发现一个函数log_and_fill_cache,我们知道fillCache是怎么回事,那log呢?我走进函数,看到打印日志的标记是objcMsgLogEnabled,而objcMsgLogEnabled是可以我们设置的通过instrumentObjcMessageSends

extern void instrumentObjcMessageSends(BOOL flag); //声明
     LGPerson *person = [LGPerson alloc];
        instrumentObjcMessageSends(YES);
        [person sayHello];//只声明未实现
        instrumentObjcMessageSends(NO);

在logMessageSend中我们看到日志是写到/tmp文件下,运行之后出现文件msgSends-38368

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

我们在源码中看到resolveInstanceMethod之后就没有forwardingTargetForSelector,methodSignatureForSelector方法的调用,bt打印堆栈信息,看到关于CoreFoundation,我们猜想动态方法决议之后CoreFoundation接下来处理,查看CoreFoundation之后没看到关于堆栈信息的forwarding_prep_0_ forwarding

* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
    frame #0: 0x00007fff2fdbb6e6 CoreFoundation`CFStringGetLength + 6
    frame #1: 0x00007fff2fde5816 CoreFoundation`CFStringAppend + 229
    frame #2: 0x00007fff2fe25e4b CoreFoundation`-[NSArray componentsJoinedByString:] + 309
    frame #3: 0x00007fff2ff2b95f CoreFoundation`__handleUncaughtException + 761
    frame #4: 0x00007fff68cfb5a3 libobjc.A.dylib`_objc_terminate() + 90
    frame #5: 0x00007fff671ce887 libc++abi.dylib`std::__terminate(void (*)()) + 8
    frame #6: 0x00007fff671d11a2 libc++abi.dylib`__cxxabiv1::failed_throw(__cxxabiv1::__cxa_exception*) + 27
    frame #7: 0x00007fff671d1169 libc++abi.dylib`__cxa_throw + 113
    frame #8: 0x00007fff68cf96ed libobjc.A.dylib`objc_exception_throw + 350
    frame #9: 0x00007fff2ff31be7 CoreFoundation`-[NSObject(NSObject) doesNotRecognizeSelector:] + 132
    frame #10: 0x00007fff2fe173bb CoreFoundation`___forwarding___ + 1427
    frame #11: 0x00007fff2fe16d98 CoreFoundation`__forwarding_prep_0___ + 120
  * frame #12: 0x0000000100000e80 002-instrumentObjcMessageSends辅助分析`main(argc=1, argv=0x00007ffeefbff260) at main.m:19:9
    frame #13: 0x00007fff69ea1cc9 libdyld.dylib`start + 1

反编译查看CoreFoundation image list查看工程中使用的库

/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation 

前往该目录

iOS-OC底层09:动态方法决议 & 消息转发_第2张图片
image.png

使用反汇编工具 hopper Disassembler搜索forwarding_prep_0_ 或者forwarding查看消息转发流程
image.png

你可能感兴趣的:(iOS-OC底层09:动态方法决议 & 消息转发)