美文网首页
NSInvocation的使用

NSInvocation的使用

作者: sxcccc | 来源:发表于2018-09-26 14:57 被阅读0次

背景

在最近的项目开发中遇到了一个很别扭的问题。项目本身是一个SDK,供业务方App集成使用。SDK内部有网络请求相关的逻辑,所以对外依赖AFNetworking(在SDK内部依赖一个第三方库本身不是一个合理设计,这次遇到的坑也是因为这个)。在SDK集成到业务方App的过程中发现,A、B两个业务方的App里虽然都集成了AFNetworking,但集成的版本有差异,导致部分API存在差异,例如 AFURLSessionManager 类里,下载相关的接口:

// 新版本:
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request
                                             progress:(nullable void (^)(NSProgress *downloadProgress))downloadProgressBlock
                                          destination:(nullable NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination
                                    completionHandler:(nullable void (^)(NSURLResponse *response, NSURL * _Nullable filePath, NSError * _Nullable error))completionHandler;
// 旧版本
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request
                                             progress:(NSProgress * __autoreleasing *)progress
                                          destination:(NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination
                                    completionHandler:(void (^)(NSURLResponse *response, NSURL *filePath, NSError *error))completionHandler;

这时需要SDK内部做一些适配,来适应不同版本AFN。但这里的难点在于 新、旧接口的方法名、参数名、返回值等的定义都是一致的,只有 progress 参数的类型不一致,而且没有其他可替代的接口。
一种解决方案是定义一个宏来标记依赖的AFN版本,在AFN头文件以及接口调用的地方用宏判断进行隔离。这种方法的缺点是SDK需要针对不同的业务App使用不同的编译项进行编译,将来维护起来会比较麻烦。

在这种场合下,NSInvocation就显得非常有用了。

解决方案

贴一段官方文档里对NSInvocation的定义:

An Objective-C message rendered as an object.
NSInvocation object contains all the elements of an Objective-C message: a target, a selector, arguments, and the return value. Each of these elements can be set directly, and the return value is set automatically when the NSInvocation object is dispatched.

简单来说,借助NSInvocation,我们可以向"Objective-C对象发送消息"这一操作保存到一个对象里,并执行这一操作。NSInvocation的几个主要的属性和接口:

@property (readonly) BOOL argumentsRetained;
@property (nullable, assign) id target;
@property SEL selector;

+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;

- (void)getReturnValue:(void *)retLoc;
- (void)setReturnValue:(void *)retLoc;

- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;

- (void)invoke;
- (void)invokeWithTarget:(id)target;

使用的过程也比较容易理解:

  1. 使用方法签名创建创建NSInvocation对象
  2. 设置target (发送消息的对象)
  3. 设置selector
  4. 设置参数
  5. 配置返回值的指针
  6. invoke 执行
  7. 从返回值指针里获取返回值

针对背景里介绍的问题,解决的代码如下:

        NSURLSessionDownloadTask __unsafe_unretained *downloadTaskP;
        NSProgress __unsafe_unretained *progressP;
        NSProgress* __unsafe_unretained *progressAddress = &progressP;
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:
                                    [[manager class] instanceMethodSignatureForSelector:@selector(downloadTaskWithRequest:progress:destination:completionHandler:)]];
        [invocation setSelector:@selector(downloadTaskWithRequest:progress:destination:completionHandler:)];
        [invocation setTarget:manager];
        [invocation setArgument:&request atIndex:2];
        if (afn_version_new) {
              [invocation setArgument:&progressBlock atIndex:3];
        } else {
              [invocation setArgument:&progressAddress atIndex:3];
        }
        [invocation setArgument:&destinationBlock atIndex:4];
        [invocation setArgument:&completionBlock atIndex:5];
        [invocation invoke];
        [invocation getReturnValue:&downloadTaskP];
        NSURLSessionDownloadTask* downloadTask = downloadTaskP;
        NSProgress* progress = progressP;
        if (progress) {
            // 处理NSProgress对象
        }
        [downloadTask resume];

其中,

  • 参数的设置是从index = 2 开始的
  • 设置参数时传递的是参数对象的地址 (NSInvocation对象设置参数的方法,接收的是 void *)

尤其需要注意的是,针对原方法参数本身类型就是 指针的地址以及返回值,需要考虑Objective-C的内存管理规则。
例如以上代码中,对返回值 downloadTask的处理,如果直接传递一个 NSURLSessionDownloadTask** 参数进去,最终程序运行的时候会crash,且crash的原因是 downloadTask 被多次dealloc。造成这种crash的原因是,NSInvocation在处理返回值时,仅会向我们传递的指针指向的地址里写入数据,不会做 retain/release等OC对象内存管理相关的操作,而我们直接用NSURLSessionDownloadTask* 接收返回值时,OC会认为接收到的对象已经被retain过了,导致downloadTask被重复dealloc,引起crash。
所以以上代码中,对接收返回值的指针标记了 __unsafe_unretained:

NSURLSessionDownloadTask __unsafe_unretained *downloadTaskP;
......
NSURLSessionDownloadTask* downloadTask = downloadTaskP;

对NSProgress ** 参数的处理也是一样的道理。

内存管理部分参考这个问答: https://stackoverflow.com/questions/22018272/nsinvocation-returns-value-but-makes-app-crash-with-exc-bad-access

相关文章

  • NSInvocation个人理解

    NSInvocation的使用: //NSInvocation;用来包装方法和对应的对象,它可以存储方法的名称,对...

  • NSInvocation

    NSInvocation的基本使用 11 异常处理

  • NSInvocation 使用

    NSString*result1 = [selfappend:@"a"withStr2:@"b"andStr3:@...

  • NSInvocation使用

    NSInvocation NSInvocation是一个消息调用类,主要作用是存储和传递消息。它存储的信息包含了一...

  • NSInvocation的使用

    一、介绍 在 iOS中可以直接调用方法的方式有两种: 1、performSelector:withObject;2...

  • NSInvocation的使用

    在iOS中方法调用的方式:第一种方式 (id)performSelector:(SEL)aSelector; (i...

  • NSInvocation的使用

    背景 在最近的项目开发中遇到了一个很别扭的问题。项目本身是一个SDK,供业务方App集成使用。SDK内部有网络请求...

  • NSInvocation的使用

    NSInvocation的使用 1 生成需要调用方法的签名NSMethodSignature,使用根据调用的Sel...

  • NSInvocation的使用

    1.NSInvocation的作用 封装了方法调用对象、方法选择器、参数、返回值等,可以给对象发送一个参数大于两个...

  • NSInvocation的使用

    版本:iOS13.6 一、简介 通常调用方法的方式是使用[实例 方法名]或[实例 方法名:参数] 若该方法没有公开...

网友评论

      本文标题:NSInvocation的使用

      本文链接:https://www.haomeiwen.com/subject/qcbdoftx.html