Reputation: 13
I have Network layer class, which has method with URL request. Seems like this:
- (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
I create instance of this network layer in controller and I want to handle error in completion block:
__block NSError *responseError;
[self.networkService networkRequestWithError:&responseError
withCompletion:^(NSData*) {
if (responseError != nil) {
NSLog(@"%@",responseError.localizedDescription);
} else {
//Some action with data. No matter
}
}];
Problem: responseError has some value in dataTask completion scope (when I init it), but in my completion block in controller it always nil. I don't have any idea why.
Upvotes: 0
Views: 464
Reputation: 122519
For a technical explanation of why your code didn't work, it has to with the fact that __block
variables can be moved, and thus getting a pointer to it at one point doesn't mean that the pointer still points to the variable later. You have to be careful when taking the address of a __block
variable.
As an optimization, blocks start out on the stack, and __block
variable also start out in a special structure on the stack. When some code need to store the block for use after the current scope, it copies the block, which causes the block to be moved to the heap if it isn't already there (since things on the stack may not live beyond the current call). Moving a block to the heap also causes any __block
variables that the block captures to be moved to the heap if it isn't already there (again, since it needs to be on the heap to outlive the current call).
Normally, when you get or set a __block
variable, the compiler translates that into several pointer lookups behind the scenes to access the real location of the __block
variable, so it transparently works, no matter if it's on the stack or heap. However, if you take the address of it to get a pointer, as in &responseError
, you only get the address of it as the variable currently is, but it does not get updated if the variable is moved.
So what is happening in your case is that the __block
variable responseError
in your second piece of code starts out on the stack, and when you do &responseError
, you get a pointer to its location on the stack. This pointer is passed into -networkRequestWithError:withCompletion:
, and this pointer (confusingly also called responseError
) is then captured into the block it creates and passes into -[NSURLSession dataTaskWithURL:completionHandler:]
.
At the same time, the __block
variable responseError
in your second piece of code is captured by the block you pass into -networkRequestWithError:withCompletion:
, and this block (called completion
) is then captured by the block you pass into -[NSURLSession dataTaskWithURL:completionHandler:]
. That is an asynchronous operation, so they copy the block, which copies the first block, which moves the blocks, as well as the __block
variable responseError
, to the heap.
By the time the completion handler of -[NSURLSession dataTaskWithURL:completionHandler:]
is called asynchronously, the stack frame where the original __block
variable responseError
was created has ended, and the pointer that we captured (called responseError
in -networkRequestWithError:withCompletion:
, which is a pointer to the original __block
variable responseError
on the stack) is a dangling pointer. Assigning to the thing pointed to by that pointer is undefined behavior, and may actually be overwriting some unrelated variables on the stack. On the other hand, the actual __block
variable responseError
on the heap is not changed, and still holds the value it originally held (nil
).
In your second piece of code, if you printed out the address of responseError
(&responseError
) before the block and within the block, you will see that they are different, demonstrating that it was moved.
Upvotes: 0
Reputation: 535945
This is an asynchronous method. Don’t set the response error by indirection. Do exactly what dataTaskWithURL
does: pass it as another parameter into the completion block.
- (void)networkRequestWithCompletion:(void (^)(NSData*, NSError*))completion
Upvotes: 0