当您感兴趣的任务完成时,是啥让完成处理程序执行该块?

Posted

技术标签:

【中文标题】当您感兴趣的任务完成时,是啥让完成处理程序执行该块?【英文标题】:What makes a completion handler execute the block when your task of interest is complete?当您感兴趣的任务完成时,是什么让完成处理程序执行该块? 【发布时间】:2014-01-21 21:53:08 【问题描述】:

我一直在询问并试图了解完成处理程序的工作原理。我用过很多,我读过很多教程。我将在这里发布我使用的那个,但我希望能够创建自己的而不使用其他人的代码作为参考。

我理解这个调用者方法的完成处理程序:

-(void)viewDidLoad
[newSimpleCounter countToTenThousandAndReturnCompletionBLock:^(BOOL completed)
        if(completed) 
            NSLog(@"Ten Thousands Counts Finished");
        
    ];

然后在被调用的方法中:

-(void)countToTenThousandAndReturnCompletionBLock:(void (^)(BOOL))completed
    int x = 1;
    while (x < 10001) 
        NSLog(@"%i", x);
        x++;
    
    completed(YES);

然后我根据许多 SO 帖子想出了这个:

- (void)viewDidLoad
    [self.spinner startAnimating];
    [SantiappsHelper fetchUsersWithCompletionHandler:^(NSArray *users) 
        self.usersArray = users;
        [self.tableView reloadData];
    ];

调用此方法后,将使用接收到的用户数据重新加载tableview:

typedef void (^Handler)(NSArray *users);

+(void)fetchUsersWithCompletionHandler:(Handler)handler 
    NSURL *url = [NSURL URLWithString:@"http://www.somewebservice.com"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:10];
    [request setHTTPMethod: @"GET"];
    **// We dispatch a queue to the background to execute the synchronous NSURLRequest**
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^
        // Perform the request
        NSURLResponse *response;
        NSError *error = nil;
        NSData *receivedData = [NSURLConnection sendSynchronousRequest:request
                                                     returningResponse:&response
                                                                 error:&error];
        if (error)  **// If an error returns, log it, otherwise log the response**
            // Deal with your error
            if ([response isKindOfClass:[NSHTTPURLResponse class]]) 
                NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
                NSLog(@"HTTP Error: %d %@", httpResponse.statusCode, error);
                return;
            
            NSLog(@"Error %@", error);
            return;
        
        **// So this line won't get processed until the response from the server is returned?**
        NSString *responseString = [[NSString alloc] initWithData:receivedData encoding:NSUTF8StringEncoding];

        NSArray *usersArray = [[NSArray alloc] init];
        usersArray = [NSJSONSerialization JSONObjectWithData:[responseString dataUsingEncoding:NSASCIIStringEncoding] options:0 error:nil];
        // Finally when a response is received and this line is reached, handler refers to the block passed into this called method...so it dispatches back to the main queue and returns the usersArray
        if (handler)
            dispatch_sync(dispatch_get_main_queue(), ^
            handler(usersArray);
            );
        
    );

我可以在反例中看到,被调用的方法(带有传递的块)在完成之前永远不会退出循环。因此“完成”部分实际上取决于被调用方法中的代码,而不是传递给它的块?

在这种情况下,“完成”部分取决于对 NSURLRequest 的调用是同步的这一事实。如果它是异步的呢?在我的数据被 NSURLResponse 填充之前,我如何才能推迟调用该块?

【问题讨论】:

【参考方案1】:

您的第一个示例是正确且完整的,并且是理解完成块的最佳方式。他们没有更多的魔法。它们永远不会自动执行。当某些代码调用它们时,它们会被执行。

正如您所注意到的,在后一个示例中,很容易在正确的时间调用完成块,因为一切都是同步的。如果它是异步的,那么您需要将块存储在实例变量中,并在异步操作完成时调用它。当操作完成(可能使用 its 完成处理程序)时,您可以安排收到通知。

在 ivar 中存储块时要小心。您的示例之一包括:

   self.usersArray = users;

self 的调用将导致块保留self(调用对象)。这可以很容易地创建一个保留循环。通常,您需要像这样对self 进行弱引用:

- (void)viewDidLoad
  [self.spinner startAnimating];
  __weak typeof(self) weakSelf = self;
  [SantiappsHelper fetchUsersWithCompletionHandler:^(NSArray *users) 
    typeof(self) strongSelf = weakSelf;
    if (strongSelf) 
      [strongSelf setUsersArray:users];
      [[strongSelf tableView] reloadData];
    
  ];

这是 weakSelf/strongSelf 模式的一个相当迂腐的版本,在这种情况下它可以做得更简单一些,但它展示了你可能需要的所有部分。您对self 进行了弱引用,这样您就不会创建保留循环。然后,在完全块中,您使用强引用,以便self 使其不会在您的块中间消失。然后确保self 确实仍然存在,然后才能继续。 (由于消息nil 是合法的,因此在这种特殊情况下,您可以跳过strongSelf 步骤,这也是一样的。)

【讨论】:

感谢@RobNapier,这正是我正在寻找的答案。我试图开始构建我自己的完成处理程序。我试图探索 UIKit 中的内置动画,比如 UIView 动画,但它们当然是私有的 :) 最后一件事,当你说,“如果它是异步的,那么你需要将块存储在实例变量中,然后调用当异步操作完成时“如果该操作没有完成处理程序,我怎么知道异步操作何时完成?喜欢处理图像? 大多数异步接口要么有一个完成块,要么有一个委托方法来指示它们何时完成。如果它没有提供一种方法来通知您它已经完成,那么它就不是一个设计良好的异步接口。 好的,那么也许我在考虑如何设计一个必须不断检查结果的方法,以便为该 API 的用户提供完成处理程序或回调。我想答案是,这取决于该方法正在等待什么,我必须创建适当的代码以确保已返回某些内容以提供完成处理程序,对吧?例如,我可以使用 NSTimer 每 5 秒检查一次以查看 NSURLResponse 是否已返回,然后提供完成处理程序?就像循环示例一样! 如果可能,您应该始终避免轮询(如果不可能,则设计中可能存在问题)。 NSURLConnection 提供委托回调,让您知道它的进展情况和完成时间。你不需要轮询任何东西。所有好的异步接口都会有这样的东西。 不,我明白。我只是在想,如果我要构建自己的 API 来检查流程是否完成,我需要添加完成部分,以便我的用户可以从完成处理程序中受益。因此我需要实现一些代码机制来不断检查一个值是否准备好,然后给我的用户一个完成处理程序?【参考方案2】:

您的第一个示例(countToTenThousandAndReturnCompletionBLock)实际上是一个同步方法。完成处理程序在这里没有多大意义:或者,您可以在假设的方法 countToTenThousand 之后立即调用该块(这基本相同,只是没有完成处理程序)。

您的第二个示例fetchUsersWithCompletionHandler: 是一个异步方法。然而,它实际上并不理想:

    它应该以某种方式向呼叫站点发出请求可能失败的信号。也就是说,或者为完成处理程序提供一个附加参数,例如" NSError* error 或我们单个参数 id result。在第一种情况下,errorarray 不是 nil,在第二种情况下,单个参数 result 可以是错误对象(类似于 NSError)或实际结果(类似于 NSArray)。

    如果您的请求失败,您将错过向呼叫站点发出错误信号。

    有代码异味:

    事实上,系统实现的底层网络代码是异步的。但是,使用的便捷类方法sendSynchronousRequest:同步的。这意味着,作为sendSynchronousRequest: 的实现细节,调用线程被阻塞,直到网络响应的结果可用。而 this_blocking_ 占用整个线程只是为了等待。创建线程的成本非常高,仅仅为此目的是一种浪费。这是第一个代码异味。是的,只是使用方便的类方法sendSynchronousRequest: 本身就是不好的编程实践!

    然后在您的代码中,您通过将这个同步请求分派到一个队列来再次异步处理它。

    因此,您最好对网络请求使用异步方法(例如sendAsynchronous...),这可能会通过完成处理程序发出完成信号。然后,此完成处理程序可能会调用 您的 完成处理程序参数,以处理您得到的是实际结果还是错误。

【讨论】:

是的,我知道 NSURLRequest 有一个异步方法。我在这里使用同步,因为这正是我试图理解的,当我必须以某种方式通知我数据已完成下载或图像已完成处理时如何创建完成处理程序。 @marciokoko 如果您使用异步网络请求,则基本上将需要“继续”的所有内容响应可用之后放入该异步网络方法的完成处理程序。 是的,但是如果我在 NSURLRequest 异步调用之后放入一行重新加载视图,它将在 NSURLRequest 被调用后立即被调用,但不会有 NSURLResponse。跨度> @marciokoko 这是真的,但无关紧要,因为你不这样做。同样,如果您需要在请求完成后“继续”,请将该代码放入该请求的完成处理程序中。

以上是关于当您感兴趣的任务完成时,是啥让完成处理程序执行该块?的主要内容,如果未能解决你的问题,请参考以下文章

是啥让 PHP 的 mail() 函数这么慢?

是啥让线程的执行顺序不可预测?

是啥让某些蓝牙设备在 iOS“我的设备”中列出?

Project loom:是啥让使用虚拟线程时性能更好?

编写完成处理程序的最佳方法是啥

代码未进入数据任务的完成处理程序