Reusing UITableViewCell with GCD

☆樱花仙子☆ 提交于 2019-12-22 06:30:18

问题


I'm using Grand Central Dispatch to load images of a UITableViewCell asynchronously. This works well except in some border cases in which the cell is reused, and a previous block loads the wrong image.

My current code looks like this:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (!cell) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
    }

    NSString *imagePath = [self imagePathForIndexPath:indexPath];         
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);

    dispatch_async(queue, ^{
        UIImage *image = [UIImage imageWithContentsOfFile:imagePath];

        dispatch_sync(dispatch_get_main_queue(), ^{
            cell.imageView.image = image;
            [cell setNeedsLayout];
        });
    });

    return cell;
}

As far as I know GCD queues cannot be stopped. How can this border case be prevented then? Or should I be using something else instead of GCD to solve this problem?


回答1:


what i've done is added an NSOperation ivar to the cell. the operation is responsible for getting, loading, and creating the image. when completed, it pings the cell. when/if the cell is dequeued, the operation is cancelled and destroyed if it has not finished. the operation test for cancellation when in -main. after the image is passed to the cell, the cell drops the operation and uses the image.




回答2:


You may try to force a reset of your image before you start your async request.

As user scroll the table, he may see the old image before your async method changes it with the right one

just add this line:

cell.imageView.image = nil; // or a placeHolder image
dispatch_async(queue, ^{
    UIImage *image = [UIImage imageWithContentsOfFile:imagePath];

    dispatch_sync(dispatch_get_main_queue(), ^{
        cell.imageView.image = image;
        [cell setNeedsLayout];
    });
});

EDIT:

@hgpc:

I don't think this solves the problem. A previous block can still set the wrong image before a new block. – hgpc 9 mins ago

You're right, if user scroll fast this may be a problem...

the only way i see is to make a custom cell with a counter property where to store (in a sync way) the number of operationn in queue of each cell, and then in the async method check that the counter is == 1 before to change the image:

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        static NSString *CellIdentifier = @"Cell";

        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
        if (!cell) {
    // MyCustomUITableViewCell has a property counter
            cell = [[[MyCustomUITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
            cell.counter = 0;
        }

        NSString *imagePath = [self imagePathForIndexPath:indexPath];         
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);

        cell.imageView.image = nil; // or a placeHolder image
        cell.counter = cell.counter + 1;
        dispatch_async(queue, ^{
            UIImage *image = [UIImage imageWithContentsOfFile:imagePath];

            dispatch_sync(dispatch_get_main_queue(), ^{
                if (cell.counter == 1){
                    cell.imageView.image = image;
                   [cell setNeedsLayout];
                }
                cell.counter = cell.counter - 1;

            });
        });

    return cell;
}



回答3:


You could set a cell identifier before launching the asynchronous task, and check it's the same before updating the UI.




回答4:


You have a couple of possibilities here.

a) Use NSOperationQueue instead of direct GDC.
NSOperation is based on GDC and enabled a lot of management like stoping threads.
Take a look at the concurrency guide from apple.

b) Use a ready async image loader.
There are enough free and open source project available on the internetz.
I personally recommend AFNetworking (particularly the AFNetworking+UIImageView category) for that.

If you want to do it on your own (research, knowledge, etc.) you should stay with a), but the more convenient method is b).

Update
I just noticed, that you are loading the image from a file and not from network.
In this case, you should handle it differently and take care at following points:

  • Avoid imageWithContentsOfFile: and rather use imageNamed:. This is because imageNamed: caches your images once they where loaded and imageWithContentsOfFile: don't. If you need to use imageWithContentsOfFile: preload all images in a NSCache and populate the table with images from this cache.

  • TableView images shouldn't be large in size (dimension and filesize). Even a 100x100 px retina image doesn't have more than a couple of kb which definitely don't take so long to load that you would need an async load.




回答5:


What about only loading the images when the table is not scrolling? The problem with cellForRowWithIndexPath: is that you're doing a lot of work for cells that are being scrolled past.

Instead of loading images whenever cells are dequeued, you could do it on viewDidLoad (or whenever your data model is initialized), and on scrollViewDidEndDecelerating:.

-(void)viewDidLoad {
    [super viewDidLoad];
    [self initializeData];
    [self loadVisibleImages];
}

-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    if (scrollView == self.tableView) {
        [self loadVisibleImages];
    }
}

-(void)loadVisibleImages {
    for (NSIndexPath *indexPath in [self.tableview indexPathsForVisibleRows]) {
        dispatch_async(queue, ^{
            NSString *imagePath = [self imagePathForIndexPath:indexPath]; 
            UIImage *image = [UIImage imageWithContentsOfFile:imagePath];        
            dispatch_sync(dispatch_get_main_queue(), ^{
                UITableViewCell *cell = [self tableView:self.tableView cellForRowAtIndexPath:indexPath];
                cell.imageView.image = image;
                [cell setNeedsLayout];
            });
        });
    }
}


来源:https://stackoverflow.com/questions/11930687/reusing-uitableviewcell-with-gcd

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!