1

我必须进行单独的 api 调用,为我想要获取信息的每部电影返回一些 JSON 数据。我正在尝试遍历电影 ID 数组,并在我的 viewDidLoad 方法中对每个电影 ID 调用 populateAssetObject。

如果我进入调试模式并逐步执行 for 循环,它将使用所有 5 个标题正确填充电影,但如果我正常运行它,我的电影数组只是前 2 个对象。我认为这可能是由一些多线程引起的?我不是这方面的专家,有谁知道我的问题可能是什么?

viewDidLoad:

_movies = [[NSMutableArray alloc] init];

for (NSString *curr in assetIDs) {
    [self populateAssetObject:curr];
}

这是 populateAssetObject 方法

-(void)populateAssetObject:(NSString *)videoID {
    NSString *urlString = [NSString stringWithFormat:@"[api url]", videoID];
    NSURL *url = [NSURL URLWithString:restURLString];

    NSData *data = [[NSData alloc] initWithContentsOfURL:url];
    NSError *error = nil;

    NSDictionary *contents = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];

    OVDAsset *newAsset = [[OVDAsset alloc] init];
    [newAsset setTitle:[contents valueForKey:@"title"]];
    [newAsset setDescription:[contents valueForKey:@"longDescription"]];

    [self.movies addObject:newAsset];
}
4

1 回答 1

1

您的方法有一个重要问题:

您的方法populateAssetObject:同步方法,它将访问远程资源。此方法将在线程上执行,因此会阻塞 UI。

您在循环中调用它只会使情况变得更糟。

真正需要的是一个在后台线程中执行所有操作的异步方法,以及一个在整个操作完成时通知调用站点的完成块:

typedef void (^completion_t)();
- (void) populateAssetsWithURLs:(NSArray*) urls completion:(completion_t)completionHandler;

在你的viewDidLoad你会这样做:

- (void) viewDidLoad {
    [super viewDidLoad];
    [self populateAssetsWithURLs:urls ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.tableView reloadData];
        });
    }];
}

棘手的部分是实现方法populateAssetsWithURLs:completion:

一种快速而肮脏的方法,如下所示:

- (void) populateAssetsWithURLs:(NSArray*) urls
                     completion:(completion_t)completionHandler
{
    NSUInteger count = [urls count];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (NSUInteger i = 0; i < count; ++i) {
            [self synchronousPopulateAssetObject:urls[i]];
        }
        if (completionHandler) {
            completionHandler();
        }
    });
}

- (void) synchronousPopulateAssetObject:(NSString*)url {
   ...
}

这种方法有几个问题:

它至少阻塞一个线程(但可能更多)只是为了等待结果。这种方法在系统资源方面效率低下 - 但它可能有效。

异步循环

更好的方法是采用异步设计。这种方法的棘手之处在于您有一个异步任务列表( ),每个任务都需要异步启动,并且最终结果也只能异步获得。具有 a非常不适合使循环异步。asynchronousPopulateAssetObjectfor loop

因此,您可以想象这样的 API,它可能是以下类别NSArray

类别 NSArray:

typedef void (^completion_t)(id result);

-(void) forEachPerformTask:(task_t)task completion:(completion_t)completionHandler;

注意,这task是一个异步块类型task_t,有自己的完成处理程序作为参数:

typedef void (^task_t)(id input, completion_t);

该任务将异步应用于数组中的每个元素。当所有元素都处理完毕后,将通过调用forEachPerformTask方法中传递的完成处理程序来通知客户端。

您可以在 GitHub Gist 上找到完整的实现和简短示例:transform_each.m

很快,我将编辑我的答案并演示一种更优雅的方法,它利用一个帮助库,特别适合解决像这样的异步模式。

但在此之前,我将演示另一种方法,利用NSOperationQueue

NSOperationQueue

NSOperationQueue具有可以取消正在运行和挂起的异步任务的宝贵优势。实际上,执行无法取消的操作列表的异步解决方案是一个不完整的解决方案,并且在大多数情况下可能根本不适合。

此外,NSOperationQueue可以同时执行其任务。可以使用属性设置发生任务的数量maxConcurrentOperationCount

使用 a 时有一些变化NSOperationQueue,最简单的基本思想是:

NSOperationQueue* queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 2;

for (NSString* url in urls) {
    [queue addOperationWithBlock:^{
        [self synchronousPopulateAssetObject:url];
    }];
}

这里,操作队列被配置为并行运行两个任务。将使用从块动态创建对象addOperationWithBlock:的便捷方法创建操作。NSOperation

任务将立即在 for 循环中排队。这与 Gist 上显示的“调度方法”不同。在“调度方法”中,只有在前一个任务完成后才会将新任务加入队列。这对系统资源非常友好。

这里的缺点是,无法异步确定所有任务何时完成。不过,有一个“阻塞”解决方案,使用 method waitUntilAllOperationsAreFinished。由于此方法阻塞了调用线程,并且由于我们想要一个异步方法populateAssetsWithURLs:completion:,因此我们需要将同步方法包装成异步方法,如下所示:

- (void) populateAssetsWithURLs:(NSArray*) urls
                     queue:(NSOperationQueue*)queue
                     completion:(completion_t)completionHandler
{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (NSString* url in urls) {
            [queue addOperationWithBlock:^{
                [self synchronousPopulateAssetObject:url];
            }];
        }
        [queue waitUntilAllOperationsAreFinished];
        if (completionHandler) {
            completionHandler();
        }
    });
}

注意:客户端提供队列。这是因为客户端可以随时发送cancelAllOperations到队列以停止执行挂起和正在运行的任务。

这里的缺点是,我们需要一个额外的线程,它被阻塞只是为了传递最终结果(可以作为完成处理程序中的参数传递)。

另一个缺点是使用方便的方法时,addOperationWithBlock:我们没有机会为异步任务指定完成处理程序。

使用方便方法时的另一个缺点addOperationWithBlock:是我们没有得到NSOperation设置对其他对象的依赖关系所需的方法(参见NSOperation 官方文档中的 操作依赖关系)。NSOperation

如果我们想充分利用NSOperationsNSOperationQueue我们将不得不更加精细。例如,有一个完成处理程序在操作队列处理完所有任务时通知调用站点,这是可行的 - 但它需要设置依赖关系,这需要一个我们需要子类的NSOperation 对象,并且需要执行代码什么以前是简单的异步任务。

尽管如此,依赖功能是无价的,我强烈建议您尝试一下。

于 2013-11-11T20:42:08.637 回答