-->

Why does my threading seem to fail after a few thr

2020-07-23 05:48发布

问题:

I have this code in the - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath delegate call:

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];
    });
});

Each cell slowly, in the background, loads up the seconds into the updatecell.detailTextLabel.text on the cell. The problem is after I scroll, after about 3 or 4 cells loaded, the rest just show 0 in the detailTextLabel quickly and don't load.

Any ideas why this is? Am I not doing my threading correctly?

回答1:

A couple of thoughts:

  1. Many servers impose a constraint of how many concurrent requests they accept from a given client. I'd suggest you use NSOperationQueue to constrain how many concurrent requests you make of your server to 4 or 5, rather than using dispatch queues.

  2. You're potentially making the issue worse than it needs to be, because if you scroll the table view down and then back up, when you redisplay the first few cells, you're re-downloading the AVPlayerItem and trying to make additional concurrent requests of your server. You really should save the results of your previous downloads, to eliminate the need for redundant re-requests of the same data.

  3. You're not currently checking to see if the cell that just downloaded is still visible before trying to update the UI. You really should check that.

So, I might suggest the following:

  1. In your view controller's viewDidLoad, create the NSOperationQueue that we'll use for downloading. Also specify how many concurrent operations your server will permit:

    downloadQueue = [[NSOperationQueue alloc] init];
    downloadQueue.maxConcurrentOperationCount = 4; // replace this with an appropriate value for your server
    
  2. Before, you had an array, webUrls, which was an array of NSURL objects. In point #4, below, we'll discuss retiring that array, and create a new array of row objects. But before we can do that, we should create this new RowData object.

    Each row object will have not only the webURL, but also other things, such as the durationText and perhaps even the AVPlayerItem itself. (By keeping these other object properties, when a cell scrolls back into view, we don't need to re-download the data.) So the public interface for this new class might look like:

    //
    //  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
    

    By the way, I'm not crazy about the class name, RowData. It's a little too ambiguous. But I don't know enough about the nature of your model data to suggest a better name. Feel free to call this class whatever you think is appropriate.

  3. Your new RowData class can have an instance method, called downloadInQueue, that performs the download, sets the durationText appropriately, etc. By moving the download logic here, we successfully isolate cellForRowAtIndexPath from some of the gory details involved with downloading. Just as importantly, though, this downloadInQueue method won't update the user interface itself, but rather it has completion block provided by cellForRowAtIndexPath (demonstrated in point #5, below), so this downloadInQueue method doesn't have to worry about UI considerations. Anyway, the implementation of downloadInQueue might look like:

    //
    //  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
    
  4. In your main view controller, rather than creating your old array of webUrls, create a new array of these RowData objects called, for example, objects. Set the webURL property for each of those RowData objects, of course. (Again, I'm not crazy about the ambiguous name of objects, but I don't know enough about your app to make a more specific suggestion. Call this whatever you want. But my code below will use objects.)

  5. Finally, modify your cellForRowAtIndexPath to use this new RowData object and it's downloadInQueue method. Also, note, the completion block checks to make sure the cell is still visible:

    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];
            }
        }];
    }
    
  6. If using iOS 6, if you want to cancel pending downloads when a cell scrolls off the screen, you can use the didEndDisplayingCell method of the UITableViewDelegate protocol.

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

    If supporting earlier versions of iOS, you'd have to use UIScrollViewDelegate protocol methods, such as scrollViewDidScroll, determine which cells have scrolled off the screen (e.g. are not included in indexPathsForVisibleRows) manually, but the idea is the same.

By the way, in my sample RowData above, I'm saving the AVPlayerItem. You should only do that if you need the AVPlayerItem later. We've saved the duration, which achieves all we need for the UITableViewCell, but I assume you might later want to do something with the AVPlayerItem, so I save that, too. But if you're not going to need that AVPlayerItem later, then don't save it in the RowData object. Also, I don't know how big those are, but you might want to write a didReceiveMemoryWarning that will iterate through your objects and set each item's playerItem object to nil.