美文网首页
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的使用

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