为什么我的线程在iOS上启动几个线程后似乎失败了?

Eth*_*len 3 iphone multithreading cocoa-touch grand-central-dispatch ios

我在- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath委托调用中有这个代码:

dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    AVPlayerItem *playerItem = [AVPlayerItem playerItemWithURL:[webUrls objectAtIndex:indexPath.row]];
    CMTime timeduration = playerItem.duration;
    float seconds = CMTimeGetSeconds(timeduration);
    NSString *duration = [NSString stringWithFormat:@"%f", seconds];

    dispatch_async( dispatch_get_main_queue(), ^{

        UITableViewCell *updatecell = [tblView cellForRowAtIndexPath:indexPath];
        updatecell.detailTextLabel.text = duration;
        [updatecell setNeedsLayout];
    });
});
Run Code Online (Sandbox Code Playgroud)

在背景中,每个细胞缓慢地加载secondsupdatecell.detailTextLabel.text细胞上.问题是在我滚动之后,在加载了大约3或4个单元格之后,其余的只是在detailTextLabel中快速显示0并且不加载.

任何想法为什么会这样?我没有正确地进行线程化吗?

Rob*_*Rob 8

几点想法:

  1. 许多服务器对从给定客户端接受的并发请求数量进行约束.我建议您使用NSOperationQueue来限制您对服务器的并发请求数量为4或5,而不是使用调度队列.

  2. 您可能会使问题变得更糟,因为如果您向下滚动表格视图然后备份,当您重新显示前几个单元格时,您将重新下载AVPlayerItem并尝试进行其他并发请求你的服务器.您确实应该保存以前下载的结果,以消除对相同数据的冗余重新请求的需要.

  3. 在尝试更新UI之前,您目前尚未检查刚刚下载的单元格是否仍然可见.你真的应该检查一下.

所以,我可能会建议如下:

  1. 在视图控制器中viewDidLoad,创建NSOperationQueue我们将用于下载的内容.还要指定服务器允许的并发操作数:

    downloadQueue = [[NSOperationQueue alloc] init];
    downloadQueue.maxConcurrentOperationCount = 4; // replace this with an appropriate value for your server
    
    Run Code Online (Sandbox Code Playgroud)
  2. 之前,你有一个数组,webUrls它是一个NSURL对象数组.在下面的第4点中,我们将讨论退出该数组,并创建一个新的行对象数组.但在我们能够做到这一点之前,我们应该创建这个新RowData对象.

    每个行对象不仅包含webURL其他内容,还包含其他内容,例如durationText甚至AVPlayerItem自身.(通过保留这些其他对象属性,当单元格滚动回视图时,我们不需要重新下载数据.)因此,这个新类的公共接口可能如下所示:

    //
    //  RowData.h
    //
    
    #import <Foundation/Foundation.h>
    
    @class AVPlayerItem;
    
    @interface RowData : NSObject
    
    @property (nonatomic, strong) NSURL *webURL;
    @property (nonatomic, strong) NSString *durationText;
    @property (nonatomic, strong) AVPlayerItem *playerItem;
    @property (nonatomic, getter = isDownloaded, readonly) BOOL downloaded;
    @property (nonatomic, getter = isDownloading, readonly) BOOL downloading;
    
    - (void)downloadInQueue:(NSOperationQueue *)queue completion:(void (^)(BOOL success))block;
    - (void)cancelDownload;
    
    @end
    
    Run Code Online (Sandbox Code Playgroud)

    顺便说一句,我并不是因为班级名字而疯狂RowData.这有点太模糊了.但我对模型数据的性质知之甚少,无法提出更好的名称.无论您认为合适,都可以随意拨打本课程.

  3. 您的新RowData类可以有一个名为的实例方法,downloadInQueue用于执行下载,设置durationText相应的等等.通过在此处移动下载逻辑,我们成功地隔离cellForRowAtIndexPath了下载所涉及的一些血腥细节.同样重要的是,此downloadInQueue方法不会更新用户界面本身,而是具有completion提供的块cellForRowAtIndexPath(在下面的第5点中演示),因此此downloadInQueue方法不必担心UI注意事项.无论如何,执行downloadInQueue可能看起来像:

    //
    //  RowData.m
    //
    
    #import "RowData.h"
    #import <AVFoundation/AVFoundation.h>
    
    @interface RowData ()
    
    @property (nonatomic, getter = isDownloaded) BOOL downloaded;
    @property (nonatomic, getter = isDownloading) BOOL downloading;
    @property (nonatomic, weak) NSOperation *operation;
    
    @end
    
    @implementation RowData
    
    - (void)downloadInQueue:(NSOperationQueue *)queue completion:(void (^)(BOOL success))completion
    {
        if (!self.isDownloading)
        {
            self.downloading = YES;
    
            NSOperation *currentOperation = [NSBlockOperation blockOperationWithBlock:^{
                BOOL success = NO;
    
                self.playerItem = [AVPlayerItem playerItemWithURL:self.webURL];
                if (self.playerItem)
                {
                    success = YES;
                    CMTime timeduration = self.playerItem.duration;
                    float seconds = CMTimeGetSeconds(timeduration);
                    self.durationText = [NSString stringWithFormat:@"%f", seconds];
                }
                self.downloading = NO;
                self.downloaded = YES;
    
                [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                    completion(success);
                }];
            }];
    
            [queue addOperation:currentOperation];
            self.operation = currentOperation;
        }
    }
    
    - (void)cancelDownload
    {
        if ([self isDownloading] && self.operation)
        {
            self.downloading = NO;
            [self.operation cancel];
        }
    }
    
    @end
    
    Run Code Online (Sandbox Code Playgroud)
  4. 在主视图控制器中,不是创建旧的数组,而是创建webUrls这些RowData对象的新数组,例如,objects.当然,webURL为每个RowData对象设置属性.(同样,我对这个模糊的名字并不感到骄傲objects,但我对你的应用程序的了解不多,无法提出更具体的建议.无论你想要什么,都可以打电话.但我的代码将会使用objects.)

  5. 最后,修改你cellForRowAtIndexPath的使用这个新RowData对象及其downloadInQueue方法.另外,请注意,completion块检查以确保单元格仍然可见:

    RowData *rowData = self.objects[indexPath.row];
    
    if ([rowData isDownloaded])
    {
        cell.detailTextLabel.text = rowData.durationText;
    }
    else
    {
        cell.detailTextLabel.text = @"..."; // you really should initialize this so we show something during download or remove anything previously there
    
        [rowData downloadInQueue:self.downloadQueue completion:^(BOOL success) {
            // note, if you wanted to change your behavior based upon whether the 
            // download was successful or not, just use the `success` variable
    
            UITableViewCell *updateCell = [tblView cellForRowAtIndexPath:indexPath];
    
            // by the way, make sure the cell is still on screen
    
            if (updateCell)
            {
                updateCell.detailTextLabel.text = rowData.durationText;
                [updateCell setNeedsLayout];
            }
        }];
    }
    
    Run Code Online (Sandbox Code Playgroud)
  6. 如果使用iOS 6,如果要在单元格滚动屏幕时取消挂起的下载,则可以使用协议的didEndDisplayingCell方法UITableViewDelegate.

    - (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
    {
        RowData *rowData = self.objects[indexPath.row];
    
        if ([rowData isDownloading])
            [rowData cancelDownload];
    }
    
    Run Code Online (Sandbox Code Playgroud)

    如果支持iOS的早期版本,则必须使用UIScrollViewDelegate协议方法,例如scrollViewDidScroll,确定哪些单元格已从屏幕滚动(例如,未包含在内indexPathsForVisibleRows),但想法是相同的.

顺便说一句,在RowData上面的示例中,我正在保存AVPlayerItem.你应该只在你需要的时候才这样做AVPlayerItem.我们已经保存了duration,它实现了我们所需要的一切UITableViewCell,但我想你以后可能会想要做一些事情AVPlayerItem,所以我也保存了.但是如果你AVPlayerItem以后不再需要它,那就不要把它保存在RowData对象中.另外,我不知道它们有多大,但你可能想写一个didReceiveMemoryWarning会遍历你objects并将每个项目的playerItem对象设置为的nil.

  • 我实际上已经保存了持续时间,所以我没有经常点击服务器,我只是没有在我发布的代码中显示它,因为我想专注于返回0的线程的问题.无论如何,我今天会尝试你的想法尽快给您回复.谢谢! (2认同)
  • 操作队列的另一个优点是,因为您可以取消单个操作.因此,在启动下载时,而不是`addOperationWithBlock`,使用`blockOperationWithBlock`创建一个`NSBlockOperation`,然后执行`[queue addOperation:operation]`.但是也将指向该操作的指针维护为`RowData`的`weak`属性.然后,您现在可以检查下载是否正在进行,如果是,则取消该操作.顺便说一句,取消下载正在运行,但可以取消挂起的下载. (2认同)