使用信号量

时间:2015-07-23 13:44:55

标签: ios objective-c asynchronous nsurlsession nsurlsessiondatatask

我需要使用NSURLSession进行网络通话。在某些事情的基础上,在收到回复后,我需要返回一个NSError对象。

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

之后)

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

3 个答案:

答案 0 :(得分:3)

罗布的回答告诉你如何正确行事,但不是你犯了什么错误:

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

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

但是你设置的所有错误只设置了在完成块中传递给你的局部变量err;他们从不接触调用者中的变量err。

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

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

答案 1 :(得分:2)

我建议切断Gordian结:你不应该使用信号量来使异步方法同步运行。采用异步模式,例如使用完成处理程序:

- (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(尽管不是必需的)。

答案 2 :(得分:1)

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

__block NSError *err = NULL;

有关详细信息,请参阅Blocks Programming Topics中的Blocks and Variables