viewDidLoad多线程问题

时间:2013-11-11 19:18:03

标签: ios json multithreading

我必须进行单独的api调用,为每个我想获取相关信息的电影返回一些JSON数据。我试图循环遍历电影ID数组,并在我的viewDidLoad方法中的每一个上调用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];
}

1 个答案:

答案 0 :(得分:1)

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

您的方法populateAssetObject:同步方法,它将访问远程资源。此方法将在 main 线程上执行,因此会阻止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 {
   ...
}

这种方法有一些问题:

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

异步循环

更好的方法是采用异步设计。这种方法的棘手部分是你有一个列表异步任务(asynchronousPopulateAssetObject),每个任务都需要异步启动,最终结果只是也是异步的。拥有for loop非常不适合使循环异步。

因此,您可以想象一个像这样的API,它可以是NSArray的类别:

类别NSArray:

typedef void (^completion_t)(id result);

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

注意,tasktask_t类型的异步块,它有自己的完成处理程序作为参数:

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

将为阵列中的每个元素异步应用任务。处理完所有元素后,将通过调用方法forEachPerformTask中传递的完成处理程序来通知客户端。

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

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

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

NSOperationQueue

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

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

使用NSOperationQueue时有一些变化,最简单的基本概念是:

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

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

此处,操作队列配置为并行运行两个任务。将使用方便的方法addOperationWithBlock:创建一个操作,该方法可以从块中动态创建NSOperation对象。

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

这里的缺点是,一个人不能异步确定所有任务何时完成。但是,使用方法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官方文件中的Operation Dependencies)。

如果我们想充分利用NSOperationsNSOperationQueue的全部力量,我们必须更加精细。例如,拥有一个完成处理程序,当操作队列处理完所有任务时通知调用站点是可行的 - 但它需要设置依赖项,这需要NSOperation object 我们需要一个子类,并且需要执行以前简单的异步任务的代码。

尽管如此,依赖功能非常宝贵,我强烈建议您尝试使用它。