【问题标题】:returning a value from asynchronous call using semaphores使用信号量从异步调用中返回一个值
【发布时间】:2015-07-23 13:44:54
【问题描述】:

我需要使用NSURLSession 进行网络调用。基于某些事情,我收到响应后,需要返回一个NSError对象。

我正在使用信号量来使异步调用同步运行。 问题是,错误是在调用内部正确设置的,但是一旦信号量结束(在

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

),err 变为 nil。

请帮忙

代码:

-(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;
}

【问题讨论】:

    标签: ios objective-c asynchronous nsurlsession nsurlsessiondatatask


    【解决方案1】:

    Rob 的回答告诉你如何正确地做,而不是你犯了什么错误:

    您有两个名为 err 的变量,它们完全不相关。看来您还没有打开一些重要的警告,否则您的代码甚至都不会编译。

    传递给完成块的参数 err 是来自 URL 请求的错误。您在没有考虑超时错误的情况下替换它 - 所以真正的错误现在丢失了。考虑到超时不是唯一的错误。

    但是您设置的所有错误仅设置了在完成块中传递给您的局部变量 err;他们根本不会触及调用者中的变量 err 。

    附言。您的 JSON 处理中有几个严重错误。 JSON 可以采用 UTF-16 或 UTF-32,在这种情况下,formattedData 将为 nil,并且您错误地打印“服务器问题”。如果数据不是 JSON,则无法保证它包含 DOCTYPE 或 html,则该测试绝对是垃圾。昵称 JoeSmith 的用户会讨厌你。

    将 NSJSONReadingAllowFragments 传递给 NSJSONSerialization 是无稽之谈。 dict 是不可变的;如果您尝试修改它,您的应用程序将崩溃。您不检查解析器是否返回了字典,您不检查键“User”是否存在值,并且您不检查该值是否是字典。这是您的应用程序崩溃的多种方式。

    【讨论】:

    • 优秀的捕获。没看到他重新定义了err
    • 非常感谢.. 那真是我做的一件傻事。那解决了我的问题..但是..现在我不再使用信号量了。将接受异步行为,因为它意味着.. +1 给你.. 非常感谢..
    【解决方案2】:

    我会建议你快刀斩乱麻:你不应该使用信号量来使异步方法同步运行。采用异步模式,例如使用完成处理程序:

    - (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
    

    注意:

    1. 我知道您会反对将此恢复为异步方法,但将其设为同步是一个非常糟糕的主意。首先,这是一个糟糕的用户体验(应用程序会死机,用户不知道它是否真的在做某事或者它是否已经死了),如果你的网络速度很慢,你可能会遇到各种各样的问题(例如,看门狗进程可能会杀死如果您在错误的时间执行此操作,您的应用程序)。

      所以,保持异步。理想情况下,在开始异步登录之前显示UIActivityIndicatorView,并在completionHandler 中将其关闭。 completionHandler 也将启动流程中的下一步(例如 performSegueWithIdentifier)。

    2. 我不费心测试 HTML 内容;尝试解析 JSON 并查看它是否成功更容易。您还可以通过这种方式捕获更广泛的错误。

    3. 就我个人而言,我不会返回自己的错误对象。我会继续返回操作系统给我的错误对象。这样,如果调用者必须区分不同的错误代码(例如,无连接与服务器错误),您可以。

      如果您使用自己的错误代码,我建议不要更改 domaindomain 应该涵盖一整类错误(例如,可能为您的应用程序自己的所有内部错误提供一个自定义域),而不是从一个错误到另一个错误。将domain 字段用于错误消息之类的内容并不是一个好习惯。如果您想在 NSError 对象中添加更多描述性内容,请将错误消息的文本放入 userInfo 字典中。

    4. 我可能会建议方法/变量名称符合 Cocoa 命名约定(例如,类以大写字母开头,变量和方法名称以及参数以小写字母开头)。

    5. 无需设置Content-Length(已为您完成),但最好设置Content-TypeAccept(尽管不是必需的)。

    【讨论】:

    • 非常感谢您指出我的代码中的弱点。我很感激..!!现在将更改我的代码以适应异步行为...
    【解决方案3】:

    您需要让编译器知道您将修改err。它需要一些特殊的处理来保护它超出块的生命周期。用__block声明它:

    __block NSError *err = NULL;
    

    有关详细信息,请参阅块编程主题中的 Blocks and Variables

    【讨论】:

    • 试过了..还是不行..即使那样它也返回nil
    猜你喜欢
    • 1970-01-01
    • 2017-10-18
    • 2012-03-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-29
    • 1970-01-01
    相关资源
    最近更新 更多