问题
I have a UITableView
that loads images from a URL into cells asynchronously using GCD. Problem is if a user flicks past 150 rows, 150 operations queue up and execute. What I want is to dequeue/cancel the ones that blew past and went off screen.
How do I do this?
My code at this point (pretty standard):
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
// after getting the cell...
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if (runQ) {
NSString *galleryTinyImageUrl = [[self.smapi getImageUrls:imageId imageKey:imageKey] objectForKey:@"TinyURL"];
NSData *imageData = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:galleryTinyImageUrl]];
dispatch_async(dispatch_get_main_queue(), ^{
if (imageData != nil) {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.imageView.image = [UIImage imageWithData:imageData];
}
});
}
});
runQ is a BOOL
ivar I set to NO
on viewWillDisappear
, which (I think) has the effect of flushing out the queue rapidly when this UITableView
pops off the navigation controller.
So, back to my original question: how do I cancel the image fetch operations for cells that have gone off screen? Thanks.
回答1:
First, don't queue operations while scrolling. Instead, load images for just the visible rows in viewDidLoad
and when the user stops scrolling:
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
for (NSIndexPath *indexPath in [self.tableView indexPathsForVisibleRows]) {
[self loadImageForCellAtPath:indexPath];
}
}
If you still want to be able to cancel loading for invisible cells, you could use NSBlockOperation
instead of GCD:
self.operationQueue = [[[NSOperationQueue alloc] init] autorelease];
[self.operationQueue setMaxConcurrentOperationCount:NSOperationQueueDefaultMaxConcurrentOperationCount];
// ...
-(void)loadImageForCellAtPath:(NSIndexPath *)indexPath {
__block NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
if (![operation isCancelled]) {
NSString *galleryTinyImageUrl = [[self.smapi getImageUrls:imageId imageKey:imageKey] objectForKey:@"TinyURL"];
NSData *imageData = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:galleryTinyImageUrl]];
dispatch_async(dispatch_get_main_queue(), ^{
if (imageData != nil) {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.imageView.image = [UIImage imageWithData:imageData];
}
});
}
}];
NSValue *nonRetainedOperation = [NSValue valueWithNonretainedObjectValue:operation];
[self.operations addObject:nonRetainedOperation forKey:indexPath];
[self.operationQueue addOperation:operation];
}
Here operations
is an NSMutableDictionary
. When you want to cancel an operation, you retrieve it by the cell's indexPath
, cancel it, and remove it from the dictionary:
NSValue *operationHolder = [self.operations objectForKey:indexPath];
NSOperation *operation = [operationHolder nonretainedObjectValue];
[operation cancel];
回答2:
You cannot cancel GCD operations after they've been dispatched. You can however check in the dispatch blocks to see if the code needs to continue on or not. In -tableView:cellForRowAtIndexPath: you can do something like this
UIImage *image = [imageCache objectForKey:imageName];
if(image) {
cell.imageView.image = image;
else {
dispatch_async(globalQueue, ^{
//get your image here...
UIImage *image = ...
dispatch_async(dispatch_get_main_queue(), ^{
if([cell.indexPath isEqual:indexPath){
cell.indexPath = nil;
cell.imageView.image = image;
[cell setNeedsLayout];
}
});
[imageCache setObject:image forKey:imageName];
});
}
Basically use a image cache (NSMutableDictionary) and try to grab the image. If you don't have it then dispatch a block and get it. Then dispatch back to the main thread, check the index path and then set the image and finally cache the image. This example uses a custom tableview cell that has the index path stored with it as a property.
So its a little extra work, to make this work well, but its worth it.
回答3:
If you are using dequeued cells the best approach is to cancel GCD in
-[UITableViewCell prepareForReuse]
来源:https://stackoverflow.com/questions/10408611/in-a-uitableview-best-method-to-cancel-gcd-operations-for-cells-that-have-gone