OC 原理探索:动态方法决议

459 阅读5分钟

前言

OC 原理探索:objc_msgSend 流程 中我们对方法的慢速查找流程进行了分析,如果方法最终找不到时,会将imp赋值为forward_imp然后返回,返回后又发生了什么呢,今天来继续探索。

准备工作

一、方法找不到的报错 底层原理

方法找不到的报错

在工程中添加如下代码:

@interface SSLStudent : SSLPerson
- (void)say1;
@end

@implementation SSLStudent
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        SSLStudent *student = [SSLStudent alloc];
        [student say1];
    }
    return 0;
}

运行程序,可以看到下面的报错:

image.png

  • unrecognized selector sent to instance这个错误大家都遇到过,那么它是怎么发生的呢,接下来就探索一下它的底层原理。

报错的底层原理

通过 OC 原理探索:objc_msgSend 流程 我们可以知道方法找不到时,会返回forward_imp的值,forward_imp是通过_objc_msgForward_impcache来赋值的,如下:

image.png

我们来探索下_objc_msgForward_impcache的实现,全局搜索查找函数:

image.png

  • _objc_msgForward_impcache的实现只调用了__objc_msgForward一行代码。
  • __objc_msgForward调用了TailCallFunctionPointer函数,TailCallFunctionPointer中调用了$0$0也就是__objc_forward_handler函数,我们接下来查找__objc_forward_handler的实现。

image.png

  • OK!!,到这里就找到了 unrecognized selector sent to instance报错的出处,字符串还拼接了+-的方法符号。

二、对象方法动态决议

在报错之前有没有调用了什么函数呢,接下来断点调试源码,探索函数的调用。

image.png

  • 第一次时,behavior = 3 、LOOKUP_RESOLVER = 23&2 = 2,所以if表达式成立。

    image.png

  • if表达式成立,会调用resolveMethod_locked函数。

resolveMethod_locked 解析

image.png

  • if (! cls->isMetaClass()),判断是否非元类
  • 元类时,调用resolveInstanceMethod(inst, sel, cls)
  • 元类时,调用下面的代码: image.png
  • 最终通过 lookUpImpOrForwardTryCache(inst, sel, cls, behavior)返回,这个函数会对方法进行再次的查找。

lookUpImpOrForwardTryCache 解析

我们先来看lookUpImpOrForwardTryCache函数:

image.png

  • 可以看到最终的 _lookUpImpTryCache函数,会进行cache_getImp快速查找和lookUpImpOrForward慢速查找。

resolveInstanceMethod 解析

我们来看一下resolveInstanceMethod(inst, sel, cls)函数的实现:

image.png

1. lookUpImpOrNilTryCache

image.png

  • 这部分代码是对resolveInstanceMethod:没有实现的容错处理,这里是不会进入的,系统提供了默认的实现。 image.png

2. objc_msgSend

image.png

  • 通过objc_msgSendcls发送了一个resolveInstanceMethod:的消息,因为是给发消息,所以是个+方法,在这个消息中我们可以添加相应代码解决报错。

3. lookUpImpOrNilTryCache

lookUpImpOrNilTryCache(inst, sel, cls)会进行方法的查找。

image.png

  • lookUpImpOrNilTryCache最终也是调用了 _lookUpImpTryCache函数,会进行快速查找和慢速查找。

resolveInstanceMethod 容错处理

SSLStudent添加下面的代码,并运行程序:

@implementation SSLStudent

- (void)ssl_say1
{
    NSLog(@"ssl_say1");
}
    
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(say1)) {
        IMP imp = class_getMethodImplementation(self, @selector(ssl_say1));
        Method method = class_getInstanceMethod(self, @selector(ssl_say1));
        const char *type = method_getTypeEncoding(method);
        class_addMethod(self, sel, imp, type);
    }
    return NO;
}

@end

image.png

  • OK!!ssl_say1成功打印,并且程序没有报错。

三、类方法的动态决议

SSLStudent中添加say2类方法,不去实现,运行项目:

@interface SSLStudent : SSLPerson

- (void)say1;
+ (void)say2;

@end

image.png

  • 这次代码走到了resolveClassMethod函数,接下里对它进行解析。

resolveClassMethod 解析

先看下resolveClassMethod的函数实现:

image.png

1. lookUpImpOrNilTryCache

image.png

  • 这部分代码是对resolveClassMethod:没有实现的容错处理,这里也是不会进入的,系统同样提供了默认的实现。

    image.png

2. nonmeta

image.png

  • getMaybeUnrealizedNonMetaClass函数中只是做了一些元类的容错处理。

  • nonmeta最终取值是不是元类

    image.png nonmetaisa0x0000000100008550,与cls的地址相同。

3. objc_msgSend

image.png

  • nonmeta,然后对进行发送消息处理,也就是+方法,和resolveInstanceMethod时是一样的。

4. lookUpImpOrNilTryCache

lookUpImpOrNilTryCache函数,上边已经说过进行了方法的慢速查找快速查找

resolveClassMethod 容错处理

SSLStudent添加下面的代码,并运行程序:

@implementation SSLStudent

+ (void)ssl_say2
{
    NSLog(@"ssl_say2");
}

+ (BOOL)resolveClassMethod:(SEL)sel
{
    if (sel == @selector(say2)) {
        IMP imp = class_getMethodImplementation(objc_getMetaClass("SSLStudent"), @selector(ssl_say2));
        Method method = class_getInstanceMethod(objc_getMetaClass("SSLStudent"), @selector(ssl_say2));
        const char *type = method_getTypeEncoding(method);
        class_addMethod(objc_getMetaClass("SSLStudent"), sel, imp, type);
    }
    
    return [super resolveClassMethod:sel];
}
@end

image.png

  • 没问题ssl_say2成功打印,并且程序没有报错。

再次 resolveInstanceMethod

image.png

  • 如果resolveClassMethod没有完成方法的添加,resolveInstanceMethod还可以有添加方法的机会。

四、aop 和 oop 总结

NSObject 统一处理报错

创建NSObject+SSL类,运行程序:

#import <Foundation/Foundation.h>

@interface NSObject (SSL)
@end

#import "NSObject+SSL.h"
#import <objc/runtime.h>

@implementation NSObject (SSL)

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

+ (void)say2{
    NSLog(@"say2");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {

    if (sel == @selector(say1)) {
        IMP say1     = class_getMethodImplementation(self, @selector(say1));
        Method method    = class_getInstanceMethod(self, @selector(say1));
        const char *type = method_getTypeEncoding(method);
        return class_addMethod(self, sel, say1, type);
    }else  if (sel == @selector(say2)) {
        IMP say2     = class_getMethodImplementation(objc_getMetaClass("LGTeacher"), @selector(say2));
        Method method    = class_getInstanceMethod(objc_getMetaClass("LGTeacher"), @selector(say2));
        const char *type = method_getTypeEncoding(method);
        return class_addMethod(objc_getMetaClass("LGTeacher"), sel, say2, type);
    }
    return NO;
}
@end

image.png

  • 成功打印say1say2

aop

我们项目中不可能把所有的报错方法全部hook,但是,我们可以只hook自己的方法,指定方法名为前缀_模块_具体这样。

比如说订单详情页的点击事件,方法定义为ssl_order_detailClick,如果发生了错误,可以执行跳转到订单列表页的操作,并进行错误的监控,这种方式叫做aop(面向切面编程)。

oop

与其相对的是oop(面向对象编程),它的特点是对象分工非常明确,但是会有很多的冗余代码。这个时候我们就会进行提取提取会产生一个公共的类,所有人都会对它进行继承而产生强依赖,造成强耦合

aop 的优缺点

所以我们应该无侵入动态的注入代码,以方法为切点切入进去。

那它的缺点是什么呢,ifelse太多,会造成性能的消耗。还有就是现在只是消息转发机制的前一个阶段,还没有进入转发流程,后面的流程就没有意义了,所以我们到转发流程再去做aop

转发流程将在下一篇进行探索,点个赞支持一下吧!!😄😄😄