【问题标题】:sendAsynchronousRequest makes UI freezessendAsynchronousRequest 使 UI 冻结
【发布时间】:2013-11-17 13:34:45
【问题描述】:

downloadImages 是一个按钮,当我按下它时,微调器应该开始滚动,异步请求应该 ping Google(以确保有连接),在收到响应后,我开始同步下载图像。

不知何故微调器不会走,看起来好像请求是同步而不是异步的。

- (IBAction)downloadImages:(id)sender {

    NSString *ping=@"http://www.google.com/";

    GlobalVars *globals = [GlobalVars sharedInstance];
    [self startSpinner:@"Please Wait."];
    NSURL *url = [[NSURL alloc] initWithString:ping];
    NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:5.0];
    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
        if (data) {
            for(int i=globals.farmerList.count-1; i>=0;i--)
            {
            //Definitions
            NSString * documentsDirectoryPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];

            //Get Image From URL
                NSString *urlString = [NSString stringWithFormat:@"https://myurl.com/%@",[[globals.farmerList objectAtIndex:i] objectForKey:@"Image"]];
            UIImage * imageFromURL = [self getImageFromURL:urlString];



            //Save Image to Directory
            [self saveImage:imageFromURL withFileName:[[globals.farmerList objectAtIndex:i] objectForKey:@"Image"] ofType:@"jpg" inDirectory:documentsDirectoryPath];
            }
            [self stopSpinner];

        }
    }];
}

微调器代码:

//show loading activity.
- (void)startSpinner:(NSString *)message {
    //  Purchasing Spinner.
    if (!connectingAlerts) {
        connectingAlerts = [[UIAlertView alloc] initWithTitle:NSLocalizedString(message,@"")
                                                     message:nil
                                                    delegate:self
                                           cancelButtonTitle:nil
                                           otherButtonTitles:nil];
        connectingAlerts.tag = (NSUInteger)300;
        [connectingAlerts show];

        UIActivityIndicatorView *connectingIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
        connectingIndicator.frame = CGRectMake(139.0f-18.0f,50.0f,37.0f,37.0f);
        [connectingAlerts addSubview:connectingIndicator];
        [connectingIndicator startAnimating];

    }
}
//hide loading activity.
- (void)stopSpinner {
    if (connectingAlerts) {
        [connectingAlerts dismissWithClickedButtonIndex:0 animated:YES];
        connectingAlerts = nil;
    }
    // [self performSelector:@selector(showBadNews:) withObject:error afterDelay:0.1];
}

如问:getImageFromURL 代码

-(UIImage *) getImageFromURL:(NSString *)fileURL {
    UIImage * result;

    NSData * data = [NSData dataWithContentsOfURL:[NSURL URLWithString:fileURL]];
    result = [UIImage imageWithData:data];

    return result;
}
-(void) saveImage:(UIImage *)image withFileName:(NSString *)imageName ofType:(NSString *)extension inDirectory:(NSString *)directoryPath {
    if ([[extension lowercaseString] isEqualToString:@"png"]) {
        [UIImagePNGRepresentation(image) writeToFile:[directoryPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", imageName, @"png"]] options:NSAtomicWrite error:nil];
    } else if ([[extension lowercaseString] isEqualToString:@"jpg"] || [[extension lowercaseString] isEqualToString:@"jpeg"]) {
        [UIImageJPEGRepresentation(image, 1.0) writeToFile:[directoryPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", imageName, @"jpg"]] options:NSAtomicWrite error:nil];
    } else {
        NSLog(@"Image Save Failed\nExtension: (%@) is not recognized, use (PNG/JPG)", extension);
    }
}

【问题讨论】:

    标签: ios xcode asynchronous xcode5 uiactivityindicatorview


    【解决方案1】:

    这是因为您正在创建一个异步操作,然后使用[NSOperationQueue mainQueue]; 告诉它在主线程上执行。

    相反,创建一个新的 NSOperationQueue 实例并将其作为参数传递。

    NSOperationQueue *myQueue = [[NSOperationQueue alloc] init];
    

    【讨论】:

    • 感谢您的评论!我添加了它,它的行为仍然相同! :(
    • @izzy 你用myQueue替换了[NSOperationQueue mainQueue]吗?
    • NSOperationQueue *myQueue = [[NSOperationQueue alloc] init]; NSURL *url = [[NSURL alloc] initWithString:ping]; NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:5.0]; [NSURLConnection sendAsynchronousRequest:request queue:myQueue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {}
    • 我在不同的视图中有相同的代码(应用程序启动时显示的第一个视图),它在那里完美运行。但在这个视图中它没有,任何想法?
    • 虽然我不认为这是问题所在,但我尝试从 for 循环中删除整个内部代码,但仍然需要 2 秒,请等待加载
    【解决方案2】:

    这是一个异步问题。异步是传染性的。这意味着,如果问题的任何小部分是异步的,那么整个问题就会变成异步的。

    也就是说,您的按钮操作将调用这样的异步方法(并且它本身也变为“异步”):

    - (IBAction)downloadImages:(id)sender 
    {
        self.downloadImagesButton.enabled = NO;
        [self asyncLoadAndSaveImagesWithURLs:self.urls completion:^(id result, NSError* error){
            if (error != nil) {
                NSLog(@"Error: %@", error);
            }
            dispatch_async(dispatch_get_main_queue(), ^{        
                self.downloadImagesButton.enabled = YES;
            };
        }];
    }
    

    所以,你的异步问题可以这样描述:

    给定一个 URL 列表,异步加载每个 URL,然后异步将它们保存到磁盘。当所有 URL 都加载并保存后,异步通过调用完成处理程序向调用站点传递结果数组(针对每个下载和保存操作)来通知调用站点。

    这是你的异步方法:

    typedef void (^completion_t)(id result, NSError* error);
    
    - (void) asyncLoadAndSaveImagesWithURLs:(NSArray*)urls 
                                 completion:(completion_t) completionHandler;
    

    只有找到合适的异步模式才能正确解决异步问题。这涉及到异步 每个部分的问题。

    让我们从您的getImageFromURL 方法开始。加载远程资源本质上是异步的,因此您的包装方法最终也将是异步的

    typedef void (^completion_t)(id result, NSError* error);
    
    - (void) loadImageWithURL:(NSURL*)url completion:(completion_t)completionHandler;
    

    我未定义该方法最终将如何实现。您可以使用 NSURLConnection 的异步便捷类方法、第三方帮助工具或您自己的HTTPRequestOperation 类。没关系,但它必须是异步的,才能实现 sane 方法。

    有意地,您可以并且应该使您的saveImage 方法也异步。使这个异步的原因是,这个方法可能会被并发调用,我们应该 *serialize* 磁盘绑定(I/O绑定)任务。这提高了系统资源的利用率,并使您的方法成为友好的系统公民。

    这是异步版本:

    typedef void (^completion_t)(id result, NSError* error);
    
    -(void) saveImage:(UIImage *)image fileName:(NSString *)fileName ofType:(NSString *)extension 
                                    inDirectory:(NSString *)directoryPath 
                                     completion:(completion_t)completionHandler; 
    

    为了序列化磁盘访问,我们可以使用专用队列disk_queue,我们假设它已被self正确初始化为串行队列:

    -(void) saveImage:(UIImage *)image fileName:(NSString *)fileName ofType:(NSString *)extension 
                                    inDirectory:(NSString *)directoryPath 
                                     completion:(completion_t)completionHandler
    {
        dispatch_async(self.disk_queue, ^{
            // save the image
            ...
            if (completionHandler) {
                completionHandler(result, nil);
            }
        });
    }
    

    现在,我们可以定义一个 异步 包装器来加载和保存图像:

    typedef void (^completion_t)(id result, NSError* error);
    
    - (void) loadAndSaveImageWithURL:(NSURL*)url completion:(completion_t)completionHandler
    {
        [self loadImageWithURL:url completion:^(id image, NSError*error) {
            if (image) {
                [self saveImage:image fileName:fileName ofType:type inDirectory:directory completion:^(id result, NSError* error){
                    if (result) {
                        if (completionHandler) {
                            completionHandler(result, nil);
                        }
                    }
                    else {
                        DebugLog(@"Error: %@", error);
                        if (completionHandler) {
                            completionHandler(nil, error);
                        }
                    }
                }];
            }
            else {
                if (completionHandler) {
                    completionHandler(nil, error);
                }
            }
        }];
    }
    

    这个loadAndSaveImageWithURL 方法实际上执行了两个异步任务的“延续”:

    首先,异步加载图片。 然后,如果成功,则异步保存图像。

    请务必注意,这两个异步任务是按顺序处理的。

    到这里为止,这一切都应该非常全面并且直截了当。接下来是棘手的部分,我们尝试以异步方式调用 number 个异步任务。

    异步循环

    假设,我们有一个 URL 列表。每个 URL 都应异步加载,当所有 URL 都加载完毕后,我们希望通知调用站点。

    传统的for 循环并不适合完成此任务。但是想象一下,我们会有一个 NSArray 的 Category 方法,如下所示:

    NSArray 的类别

    - (void) forEachApplyTask:(task_t)transform completion:(completion_t)completionHandler;
    

    这基本上是:对于数组中的每个对象,应用异步任务transform,当所有对象都被“转换”后,返回转换对象的列表。

    注意:此方法是异步的!

    使用适当的“转换”功能,我们可以将其“转换”为您的具体问题:

    对于数组中的每个 URL,应用异步任务 loadAndSaveImageWithURL,当所有 URL 都已加载并保存后,返回结果列表。

    forEachApplyTask:completion: 的实际实现可能看起来有点棘手,为简洁起见,我不想在这里发布完整的源代码。一种可行的方法需要大约 40 行代码。

    稍后我将提供一个示例实现(在 Gist 上),但让我们解释一下如何使用此方法:

    task_t 是一个“块”,它接受一个输入参数(URL)并返回一个结果。 由于一切都必须异步处理,因此这个块也是异步的,最终结果将通过完成块提供:

    typedef void (^completion_t)(id result, NSError* error);
    
    typedef void (^task_t)(id input, completion_t completionHandler);
    

    完成处理程序可以定义如下:

    如果任务成功,参数error 等于nil。否则,参数 errorNSError 对象。也就是说,一个有效的结果也可能是nil

    我们可以很容易地包装我们的方法loadAndSaveImageWithURL:completion: 并创建一个块:

    task_t task = ^(id input, completion_t completionHandler) {
        [self loadAndSaveImageWithURL:input completion:completionHandler];
    };
    

    给定一个 URL 数组:

    self.urls = ...;
    

    您的按钮操作可以实现如下:

    - (IBAction)downloadImages:(id)sender 
    {
        self.downloadImagesButton.enabled = NO;
    
        task_t task = ^(id input, completion_t completionHandler) {
            [self loadAndSaveImageWithURL:input completion:completionHandler];
        };
    
        [self.urls forEachApplyTask:task ^(id results, NSError*error){
            self.downloadImagesButton.enabled = YES;
            if (error == nil) {
                ... // do something
            }
            else {
                // handle error
            }
        }];
    }
    

    再次注意forEachApplyTask:completion: 方法是一个异步 方法,它会立即返回。调用站点将通过完成处理程序得到通知。

    downloadImages 方法也是异步的,但没有完成处理程序。此方法在按钮启动时禁用按钮,并在异步操作完成后再次启用。

    这个forEachApplyTask方法的实现可以在这里找到:(https://gist.github.com/couchdeveloper/6155227)。

    【讨论】:

      【解决方案3】:

      从您的代码中,我可以理解它不是由于异步调用加载 url。但是下面的代码可能很重。

      对于异步图像加载尝试https://github.com/rs/SDWebImage

      //Get Image From URL
                      NSString *urlString = [NSString stringWithFormat:@"https://myurl.com/%@",[[globals.farmerList objectAtIndex:i] objectForKey:@"Image"]];
                  UIImage * imageFromURL = [self getImageFromURL:urlString];
      
      
      
                  //Save Image to Directory
                  [self saveImage:imageFromURL withFileName:[[globals.farmerList objectAtIndex:i] objectForKey:@"Image"] ofType:@"jpg" inDirectory:documentsDirectoryPath];
      

      编码愉快:)

      【讨论】:

      • 虽然我感谢你的url,但仍然没有解决问题,代码可能很“沉重”,效率不高,但应该显示“请稍候”
      • 在发送请求之前添加一个加载指示器并将其隐藏在完成块中
      • 也许我不够清楚,我对为什么我的解决方案不起作用的问题很感兴趣,我希望找到解决问题的方法(或者至少了解我的代码为什么有问题)
      • 尝试一些优化技术。无需每次都获取 doc 目录路径。把它放在for循环外面。在后台写入文件等
      • 问题是点击后请稍候不会出现,而不是completionHandler的操作时间。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-25
      • 2015-01-16
      • 2019-09-16
      • 1970-01-01
      • 2015-04-20
      • 1970-01-01
      相关资源
      最近更新 更多