聊聊NSInvocation和NSMethodSignature

前言

咱们这里不会通过源码介绍Runtime,已经有很多文章介绍了,而且太晦涩,读起来不舒服,也不会介绍Runtime的一些基本原理,这个作为iOS开发最熟悉了,只是通过一些我们平时用到的操作,来宏观的介绍NSInvocationNSMethodSignature,随便聊聊,做一些简单的记录,还记得刚接触这个的时候咱们脑海里面的问号吗?

什么是方法,什么是选择器,什么是方法签名,什么是IMP,什么是消息?下面简单的回顾下

Selector

选择器是方法的名称。你肯定对以下选择器非常熟悉:allocinitreleasedictionaryWithObjectsAndKeys:setObject:forKey:等,而且冒号是选择器的一部分。这就是我们确定此方法需要参数的方式。不过你也可以不带参数名,但是这样做不推荐doFoo :::。这是一个带有三个参数的方法,可以像[someObject doFoo:arg1:arg2:arg3]一样调用它。不需要在选择器的每个部分之前都包含字母。Cocoa框架下
它们具有SEL类型:SEL aSelector = @selector(doSomething :)SEL aSelector = NSSelectorFromString(@“ doSomething:”)

Method Signature

方法签名叫起来比较专业,其实他就是一个记录方法返回值和参数的数据类型罢了。 他们可以在运行时用NSMethodSignature和C的char *来表示

Message

消息就是上面的提到的选择器加上你要随着选择器发送的参数。比如[dict setObject:obj forKey:key],这个消息就是选择器 SEL aSelector = @selector(setObject: forKey:),加上参数obj和key。可以将消息封装在NSInvocation中,这两点就是提到的SELarguments,选择器 + 参数列表,后续还有Targetreturn value进一步介绍,这里还涉及到方签名。

Method

struct objc_method {
    SEL _Nonnull method_name       //方法名                        
    char * _Nullable method_types  //方法签名                      
    IMP _Nonnull method_imp        // 方法实现               
} 

方法是选择器(SEL)和实现(IMP)的组合。IMP其实就是一个函数指针。我个人理解,这里的method_types就是我们下面要提到的SEL对应的方法签名

Implementation

方法实际的可执行代码。他在运行时用IMP表示,实际上就是一个函数指针。

NSMethodSignature

A record of the type information for the return value and parameters of a method.
一个对于方法返回值和参数的记录。 也可以叫做一个对于方法的签名

第一种形态

介绍NSInvocation之前,咱们先来了解下这个类NSMethodSignature,根据上面文档的介绍,主要是记录了方法的返回值和参数。咱们先来看看生成一个NSMethodSignature所需要的步骤

根据头文件提供的两个方法,一个+方法一个-方法

NSMethodSignature *sign1 = [@"" methodSignatureForSelector:@selector(initWithFormat:)];
NSMethodSignature *sign2 = [NSClassFromString(@"NSString") instanceMethodSignatureForSelector:@selector(initWithFormat:)];
NSLog(@"%@---%@",sign1,sign2);

打印结果如下:

(lldb) po sign1
<NSMethodSignature: 0x600001fb42a0>
    number of arguments = 3
    frame size = 224
    is special struct return? NO
    return value: -------- -------- -------- --------
        type encoding (@) '@'
        flags {isObject}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 0: -------- -------- -------- --------
        type encoding (@) '@'
        flags {isObject}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 1: -------- -------- -------- --------
        type encoding (:) ':'
        flags {}
        modifiers {}
        frame {offset = 8, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 2: -------- -------- -------- --------
        type encoding (@) '@'
        flags {isObject}
        modifiers {}
        frame {offset = 16, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}

2020-05-18 17:15:32.545730+0800 YTKNetworkDemo[14057:11030644] <NSMethodSignature: 0x600001fb42a0>---<NSMethodSignature: 0x600001fb42a0>

这里可以看出,同一个方法,无论哪种方式拿到的方法签名对象都是一样的,而且这里还有个小知识点po和直接NSLog打印的时候,为什么不同呢?这是因为NSObject提供了两个协议方法

@property (readonly, copy) NSString *description;
@optional
@property (readonly, copy) NSString *debugDescription;

description专门是用来为log服务的,而debugDescription就体现在lldb上面的调试指令。

除了这个不同,我们还从打印的日志中看到了type encoding (@) '@',先看下如下代码

Method m = class_getInstanceMethod(NSString.class, @selector(initWithFormat:));
const char *c = method_getTypeEncoding(m);

打印@24@0:8@16,你肯定会有下面的疑惑

疑惑点

1.这里的@符号代表什么?
2.这里的数字代表什么
3.SEL + Arguments 的消息原型 <return_type>Class_selector(id self ,SEL _cmd,...)objc_msgSend的原型(prototypevoid objc_msgSend(id self,SEL cmd,...)有什么关联,为什么长的差不多?

TypeEncoding 方法编码( 辅助签名)

为了辅助运行时系统,编译器对字符串中每个方法的返回和参数类型进行编码,并将字符串与方法选择器相关联

OC类型编码表

例如NSString的类方法isEqualToString: 的方法签名为B24@0:8@16

  1. @encode(BOOL) (B) 返回值

  2. @encode(id) (@) 默认第一个参数 self

  3. @encode(SEL) (:)默认第二个参数 _cmd

  4. @encode(NSString *) (@) 实际上的第一个参数NSString

那么下面的打印就很容易理解了

'NSString'|'initWithFormat:locale:arguments:' of encoding '@40@0:8@16@24[1{__va_list_tag=II^v^v}]32'
'NSString'|'initWithCoder:' of encoding '@24@0:8@16'
'NSString'|'initWithString:' of encoding '@24@0:8@16'

方法签名包含一个或多个用于方法返回类型的字符,后跟隐式参数self和_cmd的字符串编码,后跟零个或多个显式参数。

[返回值][target][action][参数]

解惑点

1.各种符号就是参数类型的字符串编码,方便与SEL关联,而且OC方法默认带了self_cmd这两个参数,所以这也是为什么能直接在方法中用这两个”关键字”的原因,所以配合上面的编码表 B(返回值)24@(self)0:(_cmd)8@(第一个参数NSString)

2.根据上面的offset描述,可以揣测出大概的意思是类基地址的偏移,比如上面的@24代表返回值,一般在最后,其中@0代表基地址偏移0,指针变量8个字节,然后:8代表_cmd,再然后@16代表对应的参数,这里的参数是字符串,因此就只有8个字节,如果你用NSRange可以试试,这里就会扩充出16个字节,里面存了两个unsign long long类型

3.根据上面的疑惑点,发送消息是转换成void objc_msgSend(id self,SEL cmd,...),会根据接受者和SEL选择器来调用适当的方法。那么一旦找到对应的方法实现之后,会直接跳转过去,之所以能这样是因为Objective-C对象的每个方法都可以视为简单的C函数,其原型如下 <return_type>Class_selector(id self ,SEL _cmd,...)。每个类中都有一张表格,其中的指针都会指向这种函数,而选择器对应的名称就是查表的Key,SEL可以简单理解为字符串 + 签名。其中这里原型的样子和objc_msgSend很像,这显然不是巧合,这是为了利用尾调用优化(tail-call optimization),另方法跳转变得更加简单。如果函数的最后是调用另一个函数,那么久可以利用尾调用优化技术。编译器会生成调转另一个函数所需的指令码,而且不会向调用堆栈中推入新的栈帧。只有当函数的最后一个操作仅仅是调用其他函数而不会将其返回值另做他用,才可以执行尾调用优化。别小看这个优化,如果不这么做,那么每次调用OC的方法,都要为objc_msgSend函数准备栈帧,若不优化,很容易发生Stack OverFlow

案例分析

根据上面的介绍,NSMethodSignature本质上就是对方法返回值和参数的签名。那么下面根据Runtime的消息转发,来动态给类添加一个方法。

@interface MKJAutoDictionary : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) id obj;

@end

@interface MKJAutoDictionary ()

@property (nonatomic, strong) NSMutableDictionary *storeDict;

@end

@implementation MKJAutoDictionary
@dynamic name, obj;

- (instancetype)init
{
    self = [super init];
    if (self) {
        _storeDict = [[NSMutableDictionary alloc] init];
    }
    return self;
}

就这样声明了一个类,目的是调用属性的方法时,自动存储到字典里面。这里我们给新增的两个属性nameobj设置为@dynamic,这个关键字就不介绍了,我们的另一个置顶博客有介绍。那么当外面调用时

extern void instrumentObjcMessageSends(BOOL);
instrumentObjcMessageSends(YES);        
MKJAutoDictionary *atd = [[MKJAutoDictionary alloc] init];
[atd performSelector:@selector(setName:) withObject:@"123456"];
instrumentObjcMessageSends(NO);

这里肯定会蹦,由于我们给了dynamic关键字,那么这里有一个多余的函数,主要我是用来证明是否进入消息转发。
这里有个传送门介绍方法
根据介绍我们可以在private/tmp目录下找到msgSends-xxxxx的一个日志文件,打开就能在里面看到整个消息调用过程。

...
- MKJAutoDictionary NSObject performSelector:withObject:
+ MKJAutoDictionary MKJAutoDictionary resolveInstanceMethod:
+ MKJAutoDictionary MKJAutoDictionary resolveInstanceMethod:
- MKJAutoDictionary NSObject forwardingTargetForSelector:
- MKJAutoDictionary NSObject forwardingTargetForSelector:
- MKJAutoDictionary NSObject methodSignatureForSelector:
- MKJAutoDictionary NSObject methodSignatureForSelector:
- MKJAutoDictionary NSObject class
- MKJAutoDictionary NSObject doesNotRecognizeSelector:
- MKJAutoDictionary NSObject doesNotRecognizeSelector:
- MKJAutoDictionary NSObject class
...

上面只是一个简单的小插曲,告诉大家有个方法能打印整个调用过程。
下面我们通过Runtime的消息转发,动态给类添加一个方法,来理解下Method结构体以及上面提到的签名是如何使用的

void autoDictSetter(id self, SEL _cmd, id value){
    MKJAutoDictionary *dict = (MKJAutoDictionary *)self;
    [dict.storeDict setValue:value forKey:@"123"];
}
id autoDictGetter(id self, SEL _cmd){
    MKJAutoDictionary *dict = (MKJAutoDictionary *)self;
    return [dict.storeDict valueForKey:@"123"];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSString *selName = NSStringFromSelector(sel);
    if ([selName containsString:@"set"]) {
        class_addMethod(self, sel, (IMP)autoDictSetter, "v@:@");
    }else{
        class_addMethod(self, sel, (IMP)autoDictGetter, "@@:");
    }
    return YES;
}

第二种形态 char *

这里我把功能简化了,只是演示一下如何动态给类添加方法,首先进入resolveInstanceMethod,然后根据需要调用class_addMethod方法,回顾下Method结构体,由SEL,IMP和签名组成,那么当我们动态添加的时候,根据参数就能看出class_addMethod第一个参数self就是Target,第二个参数就是SEL,第三个就是IMP,第四个就是我们所说的方法签名,还记得这句话吗? 他们可以在运行时用NSMethodSignature和C的char *来表示

NSInvocation

An Objective-C message rendered as an object.

把消息呈现为对象形式。可以存储消息的所有配置和直接调用给任意对象,这就是万物皆对象的一种实践了。
这个东西就是苹果工程师提供的一个高层消息转发系统。他是一个命令对象,可以给任意OC对象发送消息,那么与之类似的还有一个performSelector,这里咱们介绍NSInvocation,相比前者有他的短板

  1. ARC下可能会导致内存泄露
  2. performSelector最多接收两个参数,如果参数多余两个 ,就需要组装成字典类型了
  3. 他的参数类型限制为id,如果用普通配型Int Double NSInteger为参数的方法使用时会导致一些诡异的问题

步骤

使用这个类大致可以总结为如下几个步骤:

  1. 根据Selector来初始化方法签名对象 NSMethodSignature
  2. 根据方法签名对象来初始化NSInvocation对象,必须使用 invocationWithMethodSignature:方法
  3. 设置默认的TargetSelector
  4. 设置方法签名对应的参数,从下标2开始,超出签名参数index就越界报错
  5. 调用NSInvocation对象的invoke方法
  6. 若有返回值,使用NSInvocationgetReturnValue 来获取返回值,注意该方法仅仅就是把返回数据拷贝到提供的内存缓存区,并不会考虑这里的内存管理

案例分析一(简单Demo)

- (void)viewDidLoad {
    [super viewDidLoad];
	......
	NSString *name1 = @"Kimi猫";
    NSString *category1 = @"波斯猫";
    SEL targetSel = @selector(getCatAction:category:);
    NSMethodSignature *signature1 = [self methodSignatureForSelector:targetSel];
    NSInvocation *invocation1 = [NSInvocation invocationWithMethodSignature:signature1];
    [invocation1 setTarget:self];
    [invocation1 setSelector:targetSel];
    [invocation1 setArgument:&name1 atIndex:2];
    [invocation1 setArgument:&category1 atIndex:3];
    // 越界崩溃
    // [invocation1 setArgument:&category1 atIndex:4];
    [invocation1 invoke];
    
    // Error Code
    // Cat *cat1 = nil;
    // [invocation getReturnValue:&cat1];
    // NSLog(@"%@",cat1);

    // Plan A
     Cat *__unsafe_unretained cat1 = nil;
     [invocation1 getReturnValue:&cat1];
     Cat *finalCat = cat1;
    
    // Plan B
//    void *cat1 = NULL;
//    [invocation getReturnValue:&cat1];
//    Cat *finalCat = (__bridge Cat *)cat1;
 
    NSLog(@"Get Cat Instance Description---%@",finalCat);
 	......  
}
    
- (Cat *)getCatAction:(NSString *)name category:(NSString *)category{
    NSLog(@"%@--%@",self,NSStringFromSelector(_cmd));
    Cat *t = [[Cat alloc] init];
    t.name = name;
    t.category = category;
    return t;
}
https://developer.apple.com/documentation/foundation/nsinvocation/1437834-setargument?language=objc

这个Demo可以很好的反映出用法以及一些涉及到的坑。首先用法步骤最基本就是如此,这里有几个需要注意的点。

1.setArgument的下标是从2开始的,0存储的是self,1存储的是_cmd,而且参数需要传入一个变量的指针或者内存地址,方便内部直接把数据写入内存,而且signature1对象中有实际参数数量,如果超过数量就会越界崩溃

2.还是setArgument:atIndex:认不会强引用它的 argument,如果 argument 在 NSInvocation 执行的时候之前被释放就会造成野指针异常(EXC_BAD_ACCESS),必要时加上[invocation retainArguments];即可

3.getReturnValue参数也是传入变量的指针地址,这里就有个很关键的东西,看上面的Error Code注释那一坨,想当然,我们会这么写,但是这样就崩溃了,而且报错的是Bad Acess.....,这个作为一个iOS开发菜鸟,可以断定,显然是内存泄漏了。但是怎么看都没看到泄漏呀,找到StackOverFlow的介绍
意思是该方法,不管类型是是, 它只负责把返回的数据复制到给定的内存缓冲区内。很显然,它不关心内存管理。如果返回的是被对象指针类型引用,比如上面的Cat *cat1默认就是__strong类型的,ARC下,编译器会接收内存管理的操作,因此,编译器认为它已经retain一次了,然后再后面补了一个release操作,在超出范围的时候释放掉,但是由于上面的是直接把返回值写入内存,我才不管你什么内存管理,你编译器自己自作多情认为__strong修饰我已经在自己生产的setter方法retain一次了,很明显不正确,所以这次加的release直接放内存泄漏了。 该段是个人理解,如果理解上有问题,欢迎在评论区指出

那么咱们列举了两个解决方案PlanAPlanB,原理都是一样的,就是getReturnValue是简单的内存赋值,不会有任何内存管理,那么我们也给这个对象修饰成让编译器认为不需要retain的样子,比如__unsafe_unretained__weakvoid *,之后再用其他变量来引用赋值即可。

这里建议用PlanB模式,因为getReturnValue本来就是给内存缓存区写入数据,缓存区声明为void *更为合理,然后通过__bridge的方式转换为OC对象类型把内存管理交给ARC,因此就有了下面的通用方案

案例分析(通用类型)

@implementation MKJInvocationManager

+ (BOOL)invokeTarget:(id)target
              action:(SEL)selector
           arguments:(NSArray *)arguments
         returnValue:(void* _Nullable)result{
    if (target && [target respondsToSelector:selector]) {
            NSMethodSignature *sig = [target methodSignatureForSelector:selector];
            NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
            [invocation setTarget:target];
            [invocation setSelector:selector];
            for (NSUInteger i = 0; i<[arguments count]; i++) {
                if (i >= (sig.numberOfArguments - 2)) {
                    break;
                }
                NSUInteger argIndex = i+2;
                id argument = arguments[i];
                if ([argument isKindOfClass:NSNumber.class]) {
                    //convert number object to basic num type if needs
                    BOOL shouldContinue = NO;
                    NSNumber *num = (NSNumber*)argument;
                    const char *type = [sig getArgumentTypeAtIndex:argIndex];
                    if (strcmp(type, @encode(BOOL)) == 0) {
                        BOOL rawNum = [num boolValue];
                        [invocation setArgument:&rawNum atIndex:argIndex];
                        shouldContinue = YES;
                    }
                    
                    /....此处省略NSNumber其他类型的判断.../
                    
                    if (shouldContinue) {
                        continue;
                    }
                }
                if ([argument isKindOfClass:[NSNull class]]) {
                    argument = nil;
                }
                [invocation setArgument:&argument atIndex:argIndex];
            }
            [invocation invoke];
            NSString *methodReturnType = [NSString stringWithUTF8String:sig.methodReturnType];
            if (result && ![methodReturnType isEqualToString:@"v"]) { //if return type is not void
                if([methodReturnType isEqualToString:@"@"]) { //if it's kind of NSObject
                    // 初始化一个 const void * 类型
                    CFTypeRef cfResult = nil;
                    // 获取值
                    [invocation getReturnValue:&cfResult];
                    if (cfResult) {
                        // 手动 retain一次
                        CFRetain(cfResult);
                        // 手动retain 才能在这里不崩,如果没有retain,那么 __bridge_transfer 会先执行release
                        // “被转换的变量”所持有的对象在变量赋值给“转换目标变量”后随之释放  cfReslut 所以上面需要手动retain一次
                        id transferObj = (__bridge_transfer id)cfResult;
                        *(void **)result = (__bridge_retained void *)transferObj;
                        // const void *  Assigning to 'void *' from 'CFTypeRef' (aka 'const void *') discards qualifiers
                        // *(void**)result = cfResult;
                    }
                } else {
                    [invocation getReturnValue:result];
                }
            }
            return YES;
        }
        return NO;
}
@end

这段代码就是通用类型了的处理了,首先处理了参数越界问题,又可以处理NSNumber类型的判断,而且我们返回值接收的类型的是void *类型,如果不好理解可以把它理解为我们经常使用的NSError * __autoreleasing *错误的捕获。

看下核心[invocation invoke];之后的获取返回值的操作,区分两种类型,如果是普通数据类型,直接调用[invocation getReturnValue:result];即可,但是如果是对象类型,返回值不再是v的签名而是@。里面我们用到的类型是CFTypeRef进行取值,拿到的虽然是void *类型,甚至是const修饰的,正常情况直接*(void**)result = cfResult;就行了,但是类型有const修饰,会有警告,就有了上面的桥接转换。我们要把cfResult转换成id类型,用到了__bridge_transfer,改修饰符我上面也加了注释,是把C转换成OC对象,“被转换的变量”所持有的对象在变量赋值给“转换目标变量”后随之释放,会手动释放一起,因此我们就需要在之前先执行CFRetain(cfResult),避免内存泄漏。后面再把id类型转换成void *即可。

下面演示一下
定义一个类,直接通过我们写的NSInvocation的方式调用

@interface Person : NSObject

- (instancetype)initWithVooVVideoManager:(void(^)(void))playBlock;

- (void)instanceSayHello;

@end

@implementation Person

- (instancetype)initWithVooVVideoManager:(void (^)(void))playBlock{
    self = [super init];
    if (self) {
        NSLog(@"Video初始化%@--%@",NSStringFromClass(self.class),NSStringFromSelector(_cmd));
    }
    return self;
}

- (void)instanceSayHello{
    NSLog(@"Hello instance JX VV I am Back %@--%@",NSStringFromClass(self.class),NSStringFromSelector(_cmd));
}

@end


// 在另一个类中触发
void(^block)(void) = ^(void){
        NSLog(@"我丢");
    };
id obj = nil;
[MKJInvocationManager invokeTarget:[NSClassFromString(@"Person") alloc] action:@selector(initWithVooVVideoManager:) arguments:@[block] returnValue:&obj];
[MKJInvocationManager invokeTarget:obj action:@selector(instanceSayHello) arguments:nil returnValue:nil];

// 2020-05-19 17:28:45.896318+0800 YTKNetworkDemo[62992:1222263] Video初始化Person--initWithVooVVideoManager:
// 2020-05-19 17:28:45.896409+0800 YTKNetworkDemo[62992:1222263] Hello instance JX VV I am Back Person--instanceSayHello

可以看到,NSInvocation通过签名初始化后,我们只要组合出这四个参数,就可以中转所有的方法了
1、target
2、selector
3、arguments
4、return value

手动触发消息转发

先回顾下Runtime的消息转发步骤。咱们姑且认为有三次机会处理未被找到的消息。

1.resolveInstanceMethod:与resolveClassMethod:
该方法的实现我们再上面讲NSMethodSignature的时候通过class_addMethod介绍了

2.forwardingTargetForSelector
这个更没有什么讲的,就是指定一个新的Target转发,只能转给一个对象

3.forwardInvocation: 和 methodSignatureForSelector:
这个就厉害了,支持将消息转发给任意多个对象,所以多继承也只能采用forwardInvocation:的方式,由于咱们正好在讲NSInvocation,就顺便带一个Demo来看看,多继承这种能做,但没必要,毕竟不常用。

模拟不再找不到消息而崩溃

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
  [anInvocation invokeWithTarget:nil];
}

在这里,调用invoke的时候,是可以传nil的,毕竟给nil传递任何消息都会返回nil。

模拟下另一个手动触发消息转发,我们知道如果methodimp被指向__objc_msgForward,消息将直接进入转发模式。下面我们假定上面的Person对象方法reloadData已经被检测到崩溃了,因此我们需要手动替换调,一种方法是通过MethodSwizzle,咱们这里用class_replace来模拟下

@interface Person : NSObject

- (instancetype)initWithVooVVideoManager:(void(^)(void))playBlock;

- (void)instanceSayHello;

- (void)reloadData;

@end


@implementation Person

- (instancetype)initWithVooVVideoManager:(void (^)(void))playBlock{
    self = [super init];
    if (self) {
        NSLog(@"Video初始化%@--%@",NSStringFromClass(self.class),NSStringFromSelector(_cmd));
    }
    return self;
}

- (void)instanceSayHello{
    NSLog(@"Hello instance JX VV I am Back %@--%@",NSStringFromClass(self.class),NSStringFromSelector(_cmd));
}


- (void)reloadData{
    NSLog(@"Person ReloadData");
}

@end


void customForward(id self, SEL _cmd, NSInvocation *invo){
    if (invo.selector == @selector(instanceSayHello)) {
        NSLog(@"instanceSayHello  被完全转发覆盖");
    }else if (invo.selector == @selector(reloadData)){
        NSLog(@"reloadData  被完全转发覆盖");
    }
}

- (void)manualForwaringMesage{
    class_replaceMethod(NSClassFromString(@"Person"), @selector(instanceSayHello), _objc_msgForward, "v@:");
    class_replaceMethod(NSClassFromString(@"Person"), @selector(reloadData), _objc_msgForward, "v@:");
    class_replaceMethod(NSClassFromString(@"Person"), @selector(forwardInvocation:), (IMP)customForward, "v@:@");
//    [MKJInvocationManager invokeTarget:[NSClassFromString(@"Person") alloc] action:@selector(instanceSayHello) arguments:nil returnValue:nil];
    Person *p = [Person new];
    [p instanceSayHello];
    [p reloadData];
}

// 2020-05-19 18:11:38.195088+0800 YTKNetworkDemo[25388:1340307] instanceSayHello  被完全转发覆盖
// 2020-05-19 18:11:38.195437+0800 YTKNetworkDemo[25388:1340307] reloadData  被完全转发覆盖

可以看到instanceSayHello方法直接跳到了自定义的customForward上面,首先通过class_replaceMethod覆盖掉instanceSayHello的实现为_objc_msgForward,直接进入消息转发,有三个步骤,上面说了,我们不做处理,直接来到forwardInvocation,因为该方法已经被replace掉了,替换成了我们自己的customForward,携带了最终的NSInvocation信息,然后我们可以在自定义的方法中实现自己的逻辑,完成转发覆盖。上述只是一个简单的例子,如果自定义的函数里根据每个invocationSEL名字动态化新建一个包含完整代码完全不同的invocation,功能将会异常强大。实际上JSPatch的某些核心部分也正是使用了这种方式直接替换掉某些类里的方法实现。

总结

简单聊了下NSInvocationNSMethodSignature,重新回顾后,发现理解起来更容易了。我记得之前做组件化的时候看到CasaCTMediator方案,中介者模式好像是用performSelector的方法实现的,或许也可以用NSInvocation来尝试下,各位大佬如果看完了,看到有哪些观点有问题的,欢迎在评论区留言指正。

参考文章:
performSelector内存泄漏
typeEncoding
方法编码
消息转发和消息发送
NSINvocation return Value EXC_BAD_ACCESS
名词介绍
Log Message Send打印
同上

©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页