使用信号量从异步调用返回值
returning a value from asynchronous call using semaphores
我需要使用NSURLSession
进行网络通话。基于某些事情,我收到响应后,我需要return一个NSError
对象。
我正在使用信号量使异步调用同步运行。
问题是,err 在调用中正确设置,但是一旦信号量结束(在
dispatch_semaphore_wait(信号量,DISPATCH_TIME_FOREVER);
),err
变为零。
请帮忙
代码:
-(NSError*)loginWithEmail:(NSString*)email Password:(NSString*)password
{
NSError __block *err = NULL;
// preparing the URL of login
NSURL *Url = [NSURL URLWithString:urlString];
NSData *PostData = [Post dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
// preparing the request object
NSMutableURLRequest *Request = [[NSMutableURLRequest alloc] init];
[Request setURL:Url];
[Request setHTTPMethod:@"POST"];
[Request setValue:postLength forHTTPHeaderField:@"Content-Length"];
[Request setHTTPBody:PostData];
NSMutableDictionary __block *parsedData = NULL; // holds the data after it is parsed
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.TLSMinimumSupportedProtocol = kTLSProtocol11;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:nil];
NSURLSessionDataTask *task = [session dataTaskWithRequest:Request completionHandler:^(NSData *data, NSURLResponse *response1, NSError *err){
if(!data)
{
err = [NSError errorWithDomain:@"Connection Timeout" code:200 userInfo:nil];
}
else
{
NSString *formattedData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@", formattedData);
if([formattedData rangeOfString:@"<!DOCTYPE"].location != NSNotFound || [formattedData rangeOfString:@"<html"].location != NSNotFound)
{
loginSuccessful = NO;
//*errorr = [NSError errorWithDomain:@"Server Issue" code:201 userInfo:nil];
err = [NSError errorWithDomain:@"Server Issue" code:201 userInfo:nil];
}
else
{
parsedData = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&err];
NSMutableDictionary *dict = [parsedData objectForKey:@"User"];
loginSuccessful = YES;
}
dispatch_semaphore_signal(semaphore);
}];
[task resume];
// but have the thread wait until the task is done
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return err;
}
您需要让编译器知道您将要修改 err
。它需要一些特殊的处理来在块的生命周期之后保存它。用 __block
:
声明
__block NSError *err = NULL;
有关详细信息,请参阅块编程主题中的 Blocks and Variables。
Rob 的回答告诉你如何正确地做,但没有告诉你犯了什么错误:
您有两个名为 err 的变量,它们完全不相关。看来你没有打开一些重要的警告,否则你的代码甚至都不会编译。
传递给完成块的参数 err 是来自 URL 请求的错误。您在不考虑超时错误的情况下替换了它 - 因此现在真正的错误丢失了。考虑到超时不是唯一的错误。
但是你设置的所有错误只会设置在完成块中传递给你的局部变量err;他们根本不会触及调用者中的变量 err 。
PS。 JSON 处理中的几个严重错误。 JSON 可以采用 UTF-16 或 UTF-32,在这种情况下,formattedData 将为 nil,并且您会错误地打印 "Server Issue"。如果数据不是 JSON,则无法保证它包含 DOCTYPE 或 html,该测试绝对是垃圾。绰号为 JoeSmith 的用户会讨厌你。
将NSJSONReadingAllowFragments传给NSJSON序列化是废话。 dict 是不可变的;如果您尝试修改它,您的应用程序将会崩溃。您不检查解析器是否返回字典,不检查键 "User" 是否有值,也不检查该值是否为字典。这就是您的应用程序崩溃的多种方式。
我建议快刀斩乱麻:您不应该使用信号量使异步方法同步运行。采用异步模式,例如使用完成处理程序:
- (void)loginWithEmail:(NSString *)email password:(NSString*)password completionHandler:(void (^ __nonnull)(NSDictionary *userDictionary, NSError *error))completionHandler
{
NSString *post = ...; // build your `post` here, making sure to percent-escape userid and password if this is x-www-form-urlencoded request
NSURL *url = [NSURL URLWithString:urlString];
NSData *postData = [post dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:@"POST"];
// [request setValue:postLength forHTTPHeaderField:@"Content-Length"]; // not needed to set length ... this is done for you
[request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"]; // but it is best practice to set the `Content-Type`; use whatever `Content-Type` appropriate for your request
[request setValue:@"text/json" forHTTPHeaderField:@"Accept"]; // and it's also best practice to also inform server of what sort of response you'll accept
[request setHTTPBody:postData];
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.TLSMinimumSupportedProtocol = kTLSProtocol11;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:nil];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *err) {
if (!data) {
dispatch_async(dispatch_get_main_queue(), ^{
completionHandler(nil, [NSError errorWithDomain:@"Connection Timeout" code:200 userInfo:nil]);
});
} else {
NSError *parseError;
NSDictionary *parsedData = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&parseError];
dispatch_async(dispatch_get_main_queue(), ^{
if (parsedData) {
NSDictionary *dict = parsedData[@"User"];
completionHandler(dict, nil);
} else {
completionHandler(nil, [NSError errorWithDomain:@"Server Issue" code:201 userInfo:nil]);
}
});
}
}];
[task resume];
}
然后这样调用它:
[self loginWithEmail:userid password:password completionHandler:^(NSDictionary *userDictionary, NSError *error) {
if (error) {
// do whatever you want on error here
} else {
// successful, use `userDictionary` here
}
}];
// but don't do anything reliant on successful login here; put it inside the block above
注:
我知道您会反对将其恢复为异步方法,但将其设为同步是一个非常糟糕的主意。首先,这是一个可怕的用户体验(应用程序会冻结,用户不知道它是否真的在做某事,或者它是否已经死了),如果你在一个慢速网络上,你可能会遇到各种各样的问题(例如,看门狗进程可能会终止如果你在错误的时间这样做,你的应用程序)。
所以,保持异步。理想情况下,在开始异步登录之前显示UIActivityIndicatorView
,并在completionHandler
中关闭它。 completionHandler
还将启动流程中的下一步(例如 performSegueWithIdentifier
)。
我懒得测试 HTML 内容;尝试解析 JSON 并查看它是否成功会更容易。您还将通过这种方式捕获更广泛的错误。
就我个人而言,我不会 return 我自己的错误对象。我会继续 return OS 给我的错误对象。这样,如果调用者必须区分不同的错误代码(例如,无连接与服务器错误),您可以。
如果您使用自己的错误代码,我建议您不要更改 domain
。 domain
应该涵盖整个错误类别(例如,可能是您应用程序自身的所有内部错误的一个自定义域),而不是因错误而异。将 domain
字段用于错误消息之类的内容不是好的做法。如果您想在 NSError
对象中添加更多描述性内容,请将错误消息的文本放入 userInfo
字典中。
我可能会建议 method/variable 名称符合 Cocoa 命名约定(例如 类 以大写字母开头,变量和方法名称以及参数以小写字母开头字母)。
不需要设置 Content-Length
(已经为您完成),但最好设置 Content-Type
和 Accept
(虽然不是必需的) .
我需要使用NSURLSession
进行网络通话。基于某些事情,我收到响应后,我需要return一个NSError
对象。
我正在使用信号量使异步调用同步运行。 问题是,err 在调用中正确设置,但是一旦信号量结束(在
dispatch_semaphore_wait(信号量,DISPATCH_TIME_FOREVER);
),err
变为零。
请帮忙
代码:
-(NSError*)loginWithEmail:(NSString*)email Password:(NSString*)password
{
NSError __block *err = NULL;
// preparing the URL of login
NSURL *Url = [NSURL URLWithString:urlString];
NSData *PostData = [Post dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
// preparing the request object
NSMutableURLRequest *Request = [[NSMutableURLRequest alloc] init];
[Request setURL:Url];
[Request setHTTPMethod:@"POST"];
[Request setValue:postLength forHTTPHeaderField:@"Content-Length"];
[Request setHTTPBody:PostData];
NSMutableDictionary __block *parsedData = NULL; // holds the data after it is parsed
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.TLSMinimumSupportedProtocol = kTLSProtocol11;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:nil];
NSURLSessionDataTask *task = [session dataTaskWithRequest:Request completionHandler:^(NSData *data, NSURLResponse *response1, NSError *err){
if(!data)
{
err = [NSError errorWithDomain:@"Connection Timeout" code:200 userInfo:nil];
}
else
{
NSString *formattedData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@", formattedData);
if([formattedData rangeOfString:@"<!DOCTYPE"].location != NSNotFound || [formattedData rangeOfString:@"<html"].location != NSNotFound)
{
loginSuccessful = NO;
//*errorr = [NSError errorWithDomain:@"Server Issue" code:201 userInfo:nil];
err = [NSError errorWithDomain:@"Server Issue" code:201 userInfo:nil];
}
else
{
parsedData = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&err];
NSMutableDictionary *dict = [parsedData objectForKey:@"User"];
loginSuccessful = YES;
}
dispatch_semaphore_signal(semaphore);
}];
[task resume];
// but have the thread wait until the task is done
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return err;
}
您需要让编译器知道您将要修改 err
。它需要一些特殊的处理来在块的生命周期之后保存它。用 __block
:
__block NSError *err = NULL;
有关详细信息,请参阅块编程主题中的 Blocks and Variables。
Rob 的回答告诉你如何正确地做,但没有告诉你犯了什么错误:
您有两个名为 err 的变量,它们完全不相关。看来你没有打开一些重要的警告,否则你的代码甚至都不会编译。
传递给完成块的参数 err 是来自 URL 请求的错误。您在不考虑超时错误的情况下替换了它 - 因此现在真正的错误丢失了。考虑到超时不是唯一的错误。
但是你设置的所有错误只会设置在完成块中传递给你的局部变量err;他们根本不会触及调用者中的变量 err 。
PS。 JSON 处理中的几个严重错误。 JSON 可以采用 UTF-16 或 UTF-32,在这种情况下,formattedData 将为 nil,并且您会错误地打印 "Server Issue"。如果数据不是 JSON,则无法保证它包含 DOCTYPE 或 html,该测试绝对是垃圾。绰号为 JoeSmith 的用户会讨厌你。
将NSJSONReadingAllowFragments传给NSJSON序列化是废话。 dict 是不可变的;如果您尝试修改它,您的应用程序将会崩溃。您不检查解析器是否返回字典,不检查键 "User" 是否有值,也不检查该值是否为字典。这就是您的应用程序崩溃的多种方式。
我建议快刀斩乱麻:您不应该使用信号量使异步方法同步运行。采用异步模式,例如使用完成处理程序:
- (void)loginWithEmail:(NSString *)email password:(NSString*)password completionHandler:(void (^ __nonnull)(NSDictionary *userDictionary, NSError *error))completionHandler
{
NSString *post = ...; // build your `post` here, making sure to percent-escape userid and password if this is x-www-form-urlencoded request
NSURL *url = [NSURL URLWithString:urlString];
NSData *postData = [post dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:@"POST"];
// [request setValue:postLength forHTTPHeaderField:@"Content-Length"]; // not needed to set length ... this is done for you
[request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"]; // but it is best practice to set the `Content-Type`; use whatever `Content-Type` appropriate for your request
[request setValue:@"text/json" forHTTPHeaderField:@"Accept"]; // and it's also best practice to also inform server of what sort of response you'll accept
[request setHTTPBody:postData];
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.TLSMinimumSupportedProtocol = kTLSProtocol11;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:nil];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *err) {
if (!data) {
dispatch_async(dispatch_get_main_queue(), ^{
completionHandler(nil, [NSError errorWithDomain:@"Connection Timeout" code:200 userInfo:nil]);
});
} else {
NSError *parseError;
NSDictionary *parsedData = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&parseError];
dispatch_async(dispatch_get_main_queue(), ^{
if (parsedData) {
NSDictionary *dict = parsedData[@"User"];
completionHandler(dict, nil);
} else {
completionHandler(nil, [NSError errorWithDomain:@"Server Issue" code:201 userInfo:nil]);
}
});
}
}];
[task resume];
}
然后这样调用它:
[self loginWithEmail:userid password:password completionHandler:^(NSDictionary *userDictionary, NSError *error) {
if (error) {
// do whatever you want on error here
} else {
// successful, use `userDictionary` here
}
}];
// but don't do anything reliant on successful login here; put it inside the block above
注:
我知道您会反对将其恢复为异步方法,但将其设为同步是一个非常糟糕的主意。首先,这是一个可怕的用户体验(应用程序会冻结,用户不知道它是否真的在做某事,或者它是否已经死了),如果你在一个慢速网络上,你可能会遇到各种各样的问题(例如,看门狗进程可能会终止如果你在错误的时间这样做,你的应用程序)。
所以,保持异步。理想情况下,在开始异步登录之前显示
UIActivityIndicatorView
,并在completionHandler
中关闭它。completionHandler
还将启动流程中的下一步(例如performSegueWithIdentifier
)。我懒得测试 HTML 内容;尝试解析 JSON 并查看它是否成功会更容易。您还将通过这种方式捕获更广泛的错误。
就我个人而言,我不会 return 我自己的错误对象。我会继续 return OS 给我的错误对象。这样,如果调用者必须区分不同的错误代码(例如,无连接与服务器错误),您可以。
如果您使用自己的错误代码,我建议您不要更改
domain
。domain
应该涵盖整个错误类别(例如,可能是您应用程序自身的所有内部错误的一个自定义域),而不是因错误而异。将domain
字段用于错误消息之类的内容不是好的做法。如果您想在NSError
对象中添加更多描述性内容,请将错误消息的文本放入userInfo
字典中。我可能会建议 method/variable 名称符合 Cocoa 命名约定(例如 类 以大写字母开头,变量和方法名称以及参数以小写字母开头字母)。
不需要设置
Content-Length
(已经为您完成),但最好设置Content-Type
和Accept
(虽然不是必需的) .