从 URLSession 传递 NSError 的正确方法

Correct way to pass NSError from URLSession

我有网络层 class,它有 URL 请求的方法。好像是这样的:

- (void)networkRequestWithError:(NSError *__strong *)responseError
                  andCompletion:(void (^)(NSData*))completion
{
    NSURL *url = ...
                        
    NSURLSessionDataTask *dataTask = [NSURLSession.sharedSession
                                      dataTaskWithURL:url
                                      completionHandler:^(NSData *data,
                                                          NSURLResponse *response,
                                                          NSError *error) {
        // *responseError = error; for real errors
        *responseError = [NSError errorWithDomain:@"1"
                                             code:1
                                         userInfo:@{}];        
        completion(data);
    }];
    
    [dataTask resume];
}
@end

我在控制器中创建了这个网络层的实例,我想在完成块中处理错误:

    __block NSError *responseError; 
    
    [self.networkService networkRequestWithError:&responseError
                                  withCompletion:^(NSData*) {
                
        if (responseError != nil) {
            NSLog(@"%@",responseError.localizedDescription);
        } else {
            //Some action with data. No matter
        }
    }];

问题: responseError 在 dataTask 完成范围内有一些值(当我初始化它时),但在我的控制器完成块中它总是 nil。我不知道为什么。

这是一个异步方法。不要间接设置响应错误。完全按照 dataTaskWithURL 做:将它作为另一个参数传递到完成块中。

 - (void)networkRequestWithCompletion:(void (^)(NSData*, NSError*))completion

对于为什么你的代码不起作用的技术解释,它必须考虑到 __block 变量可以移动的事实,因此在某一点获得指向它的指针并不意味着指针稍后仍指向该变量。获取 __block 变量的地址时必须小心。

作为优化,块从堆栈开始,__block变量也从堆栈的特殊结构开始。当某些代码需要存储块以供在当前作用域之后使用时,它会复制该块,如果该块不在堆中,这会导致该块被移动到堆中(因为堆栈上的东西可能不会在当前调用之后存在) ).将块移动到堆也会导致块捕获的任何 __block 变量移动到堆,如果它还不存在的话(同样,因为它需要在堆上才能比当前调用更长寿)。

通常,当您获取或设置 __block 变量时,编译器会在后台将其转换为多个指针查找以访问 __block 变量的实际位置,因此它透明地工作,不管它是在栈上还是堆上。但是,如果你用它的地址来得到一个指针,如&responseError,你只能得到它作为当前变量的地址,但是如果变量被移动它不会得到更新。

所以在你的情况下发生的是你第二段代码中的 __block 变量 responseError 从堆栈开始,当你做 &responseError 时,你得到指向它在堆栈上的位置的指针。这个指针被传递到 -networkRequestWithError:withCompletion:,然后这个指针(也被混淆地称为 responseError)被捕获到它创建的块中并传递到 -[NSURLSession dataTaskWithURL:completionHandler:].

同时,你第二段代码中的 __block 变量 responseError 被你传入 -networkRequestWithError:withCompletion: 的块捕获,这个块(称为 completion) 然后被你传递给 -[NSURLSession dataTaskWithURL:completionHandler:] 的块捕获。这是一个异步操作,所以他们复制块,复制第一个块,移动块,以及 __block 变量 responseError 到堆。

当异步调用 -[NSURLSession dataTaskWithURL:completionHandler:] 的完成处理程序时,创建原始 __block 变量 responseError 的堆栈帧已经结束,我们捕获的指针 ( called responseError in -networkRequestWithError:withCompletion:,它是指向堆栈上原始 __block 变量 responseError 的指针)是一个悬挂指针。分配给该指针指向的东西是未定义的行为,实际上可能会覆盖堆栈上一些不相关的变量。另一方面,堆上的实际 __block 变量 responseError 并没有改变,仍然保持它原来持有的值 (nil).

在你的第二段代码中,如果你在块之前和块内打印出responseError&responseError)的地址,你会看到它们是不同的,证明它被感动了。