问题
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:
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.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.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:
In your view controller's
viewDidLoad
, create theNSOperationQueue
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
Before, you had an array,
webUrls
, which was an array ofNSURL
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 newRowData
object.Each row object will have not only the
webURL
, but also other things, such as thedurationText
and perhaps even theAVPlayerItem
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.Your new
RowData
class can have an instance method, calleddownloadInQueue
, that performs the download, sets thedurationText
appropriately, etc. By moving the download logic here, we successfully isolatecellForRowAtIndexPath
from some of the gory details involved with downloading. Just as importantly, though, thisdownloadInQueue
method won't update the user interface itself, but rather it hascompletion
block provided bycellForRowAtIndexPath
(demonstrated in point #5, below), so thisdownloadInQueue
method doesn't have to worry about UI considerations. Anyway, the implementation ofdownloadInQueue
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
In your main view controller, rather than creating your old array of
webUrls
, create a new array of theseRowData
objects called, for example,objects
. Set thewebURL
property for each of thoseRowData
objects, of course. (Again, I'm not crazy about the ambiguous name ofobjects
, 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 useobjects
.)Finally, modify your
cellForRowAtIndexPath
to use this newRowData
object and it'sdownloadInQueue
method. Also, note, thecompletion
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]; } }]; }
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 theUITableViewDelegate
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 inindexPathsForVisibleRows
) 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
.
来源:https://stackoverflow.com/questions/14422802/why-does-my-threading-seem-to-fail-after-a-few-threads-have-started-on-ios