iOS底层原理--动态方法决议&消息转发

449 阅读10分钟

前言

在前面的文章iOS底层原理--RunTime之objc_msgSend探究(快速查找)iOS底层原理--方法的慢速查找流程中,我们已经分析了方法的本质:消息发送,我们也分析了消息发送过程中的快速查找慢速查找流程。我们发现在快速查找和慢速查找都找不到时,方法没有实现时,真的没有任何补救方法吗?直接崩?那也太不苹果了。下面👇我们就开始研究如果方法没有实现的时候,会再给我们哪些机会呢?

动态方法决议

其实在上一篇文章iOS底层原理--方法的慢速查找流程中,我们已经分析了为什么方法未实现会报错,也简单的提到过了动态方法决议image.png 也分析了,此处的判断条件就相当于单例,只会走一次哦,也就是说苹果在此处只会给你一次机会,去保住自己最后那一丢丢尊严。 image.png

文字总结流程

  1. 判断cls是否是元类,如果是类(非元类),则调用resolveInstanceMethod实例方法的动态方法决议。
  2. 如果是元类,则先调用resolveClassMethod类方法的动态方法决议。如果在元类中没有找到,则调用元类的resolveInstanceMethod实例方法动态决议。(因为类方法元类中也是以实例方法存在的,本质上并没有+-之分)。
  3. 如果动态方法决议中,将实现指向了另一个方法,则继续进行lookUpImpOrForwardTryCache方法慢速查找流程。

动态方法解析小小流程图

动态方法决议流程图.png

resolveInstanceMethod方法探究

我们看下该方法的源码。 image.png

  • 定义1个resolveInstanceMethod的SEL,名字是resolve_sel
  • 通过lookUpImpOrNilTryCache方法,查找是否实现了resolveInstanceMethod该方法,如果未实现,则直接返回。如果实现了,则发送resolveInstanceMethod消息。
  • 通过lookUpImpOrNilTryCache方法,再次进行方法查找。

resolveClassMethod方法探究

image.png

  • resolveClassMethodNSobject中已经实现,只要元类初始化就可以了,目的是缓存在元类中
  • imp = lookUpImpOrNilTryCache(inst, sel, cls) 缓存sel对应的imp,不管imp有没有动态添加,如果未找到,此处都是forward_imp

lookUpImpOrNilTryCache方法探究

我们先看下源码,发现此处只是调用了另外一个方法_lookUpImpTryCacheimage.png 我们看下_lookUpImpTryCache该方法的源码。 image.png

  1. 缓存查找
  • 在缓存中查找sel对应的imp
  • imp存在,则跳转done流程
  • 如果有共享缓存给系统底层库用的,如果在缓存中没有查询到imp,则进入慢速查找流程
  1. 慢速查找流程
  • 慢速查找流程中,behavior= 44 & 2 = 0不会再次进入动态方法决议,所以不会死循环
  1. done流程
  • done流程: (behavior & LOOKUP_NIL) 且 imp = _objc_msgForward_impcache,如果imp是forward_imp,直接返回nil,否者返回imp

动态方法决议实测

1.首先我们先给YSHPerson类添加+(BOOL)resolveInstanceMethod:(SEL)sel image.png 2.我们发现在崩溃之前调用了两次resloveInstanceMethod方法 image.png 3. 为什么会调用两次resolveInstanceMethod方法呢? 第一次是动态方法决议,系统自动向resolveInstanceMethod发送消息,那么第二次是怎么调用的呢?👇我们通过lldb来分析一波。

image.png 第一次调用之后,我们通过bt分析调用栈发现就是走的方法慢速查找流程的动态决议。
第二次调用调用之后,由底层系统库CoreFoundation调起NSObject(NSObject) methodSignatureForSelector:后,会再次进入动态决议

动态添加sayHappy方法

我们添加如下代码,发现程序不再崩溃,可以调用sayNB方法 image.png

image.png

  • 这次发现resolveInstanceMethod只调用一次,因为动态添加了sayHappy方法lookUpImpOrForwardTryCache直接获取imp,直接调用imp,查找流程结束
  • 调用流程:resolveMethod_locked-->resolveInstanceMethod-->resolveInstanceMethod-->lookUpImpOrNilTryCache(inst, sel, cls)-->lookUpImpOrForwardTryCache-->调用imp

类方法

类方法与实例方法类似,同样可以通过重写resolveClassMethod类方法来解决崩溃问题,即在LGTeacher类中重写该方法,并将sayHappy类方法的实现sayKC image.png

resolveClassMethod类方法的重写。注意:传入的cls不再是类,而是元类,可以通过objc_getMetaClass方法获取类的元类,原因是因为类方法在元类中是实例方法

动态方法决议总结

刚才我们使用的方式,是在每个类中单独重写,一个项目中,少说几百上千个类,如果按照这种方式,岂不是得累死。那有没有更好的方式呢?我们之前通过方法慢速查找流程可以发现方法查找路径有两条:

  • 实例方法:类 --> 父类 --> 根类 --> nil
  • 类方法:元类 --> 根元类 --> 根类 --> nil 他们最后都会找到根类即NSObject中查找,那么我们可以通过NSObject添加分类的方式来实现统一处理,而且由于类方法的查找,在其继承链,查找的也是实例方法,所以可以将实例方法和类方法的统一处理放在resolveInstanceMethod方法中,如下所示: image.png 这种方式的实现,正好与源码中针对类方法的处理逻辑是一致的,即完美阐述为什么调用了类方法动态方法决议,还要调用对象方法动态方法决议,其根本原因还是因为类方法在元类中也是已实例方法的形态存在的

弊端:上面这种写法会有一些其他问题,比如系统方法也会被更改,所以我们可以针对自定义类中的方法统一命名方法名的前缀,根据前缀来判断是否是自定义方法,然后统一处理自定义方法,也方便我们进行业务上的区别。
优点

  • 可以统一处理方法崩溃的问题,出现方法崩溃可以上报服务器,或者pop到首页,从而提升用户体验
  • 如果项目中是不同的模块,方法名前缀不同,可以进行业务区分。 在这里扩展下AOPOOP
  • OOP:实际上是对对象的属性和行为的封装,功能相同的抽取出来单独封装,高耦合、强依赖性。
  • AOP:上述这种在分类添加方法其实就是AOP的一种展现形式。是处理某个步骤和阶段的,从中进行切面的提取,有重复的操作行为,AOP就可以提取出来,运用动态代理,实现程序功能的统一维护,依赖性小,耦合度小,单独把AOP提取出来的功能移除也不会对主代码造成影响。AOP更像一个三维的纵轴,平面内的各个类有共同逻辑的通过AOP串联起来,本身平面内的各个类没有任何的关联。

消息转发流程

此处就偷个懒吧,将两篇博客整合到一篇文章里。

方法如果在快速+慢速查找中以及在经过动态方法决议仍然没有查找到真正的方法实现即IMP,此时动态方法决议抛出imp = forward_imp进入消息转发流程。但是我们通过搜索源码,发现并没有找到消息转发的实现。下面👇。我介绍两种方式。

  • 通过instrumentObjcMessageSends方式打印发送消息的日志
  • 通过hopper/IDA反编译

日志辅助

通过lookUpImpOrForward --> log_and_fill_cache --> logMessageSend进入logMessageSend 看到源码的实现。 image.png 我们分析implementer肯定是有值的,所以logMessageSend如果能调用的话,objcMsgLogEnabled必须是YESimage.png /tmp/msgSends就是日志保存的沙盒路径,开启以后直接到沙盒路径下就能获取文件。而我们发现objcMsgLogEnabled默认是false 所以需要找到赋值的地方。 image.png 全局搜索,我们发现只有一个地方会赋值,就是通过instrumentObjcMessageSendsobjcMsgLogEnabled赋值,所以在需要日志信息的地方声明instrumentObjcMessageSends

// 慢速查找 
extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {

    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        instrumentObjcMessageSends(YES);
        [LGPerson say666];
        instrumentObjcMessageSends(NO);
    }
    return 0;
}

运行代码,并前往/tmp/msgSends目录,发现有msgSends开头的日志文件,打开发现在崩溃前,执行了以下方法: image.png

  • 动态方法决议resolveInstanceMethod方法

  • 消息快速转发forwardingTargetForSelector方法

  • 消息慢速转发methodSignatureForSelector + resolveInvocation

快速转发

forwardingTargetForSelector

打开Xcodecommand + shift +0注意此处是0而非o,然后搜索forwardingTargetForSelector image.png forwardingTargetForSelector含义是返回未识别消息重定向的对象,简单理解指定一个对象,让这个对象去接收这个消息

快速转发代码实测

// 慢速查找 
extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        instrumentObjcMessageSends(YES);
        [person sayHello];
        instrumentObjcMessageSends(NO);
        NSLog(@"Hello, World!");
    }
    return 0;
}
-(id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(sayHello)) {
        return [[LGStudent alloc] init];
    }
    return  [super forwardingTargetForSelector:aSelector];
}
@implementation LGStudent

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

根据上面代码,我们测试下,代码是否还会崩溃。

image.png 根据打印结果,我们发现项目并未崩溃。

  • 也就是说如果forwardingTargetForSelector方法中返回消息接收者,在消息接收者中还是没有找到,则进入另一个方法的查找流程,如果最终该消息接收者还是未找到,则一样会报错崩溃。
  • 如果返回nil,则进入慢速消息转发

慢速转发

慢速转发methodSignatureForSelector也是是方法查找的最后一个流程,俗话说事不过三,如果你的系统有方法错误,系统会先给一次动态方法决议的机会,再给一次快速转发的机会,如果都没把握住,那系统会最后再给一次慢速转发的机会。如果这最后一次的机会也把握不住,那只能saysorry了,崩溃闪退去吧。
我们按照快速转发的探索过程,同样搜索下methodSignatureForSelectorimage.png methodSignatureForSelector的本质是返回一个NSMethodSignature对象,该对象包含由给定选择器标识的方法的描述。methodSignatureForSelector一般搭配和forwardInvocation使用,如果methodSignatureForSelector方法返回的是一个nil就不会调用forwardInvocation

慢速转发代码实测

下面👇我们通过代码进行分析。

  1. 我们在methodSignatureForSelector方法中,直接返会nil,看下效果。
@implementation LGPerson

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"---%@---",anInvocation);
}

我们看下打印结果,照样崩溃了。 image.png 2. 我们返回一个NSMethodSignature对象,看下有什么效果。

@implementation LGPerson

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(sayHello)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return  [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"---%@---%@",anInvocation.target,NSStringFromSelector(anInvocation.selector));

}

我们看下打印结果,发现并没有崩溃,打印内容为forwardInvocation方法的内容。
由此我们发现,如果methodSignatureForSelector的返回值是NSMethodSignature对象,则会调用forwardInvocation进行处理。其中anInvocation保存了NSMethodSignature签名信息,还有目标方法的方法签名sel,以及方法的接收者。 image.png

也可以处理invocation事务,如下所示,修改invocationtarget[LGStudent alloc],调用[anInvocation invoke]触发,即LGPerson类的sayHello实例方法的调用会调用LGStudentsayHello方法。

@implementation LGPerson

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(sayHello)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return  [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"---%@---%@",anInvocation.target,NSStringFromSelector(anInvocation.selector));
    anInvocation.target = [LGStudent alloc];
    [anInvocation invoke];
}

打印结果如下👇: image.png 所以,由上述可知,无论在forwardInvocation方法中是否处理invocation事务,程序都不会崩溃

流程图总结

根据本篇动态方法决议和消息转发的分析,我们可以得出这样一张流程图。

动态方法决议和转发流程.png