Odd threading issue with UITableView images loaded from URL - ios

First up I'm a UI designer getting into Xcode, so please bear with me if i'm a little vague in my description here.
Basically I have a UITableView full of contacts, and each contact has a photo image to the left of the name, pulled from the web via url.. I have implemented lazy loading with the images, but when you scroll through the list, i seem to be getting odd flickering and a really 'laggy' (ie. not smooth) scroll.
I have tried playing around with the setNeedsDisplay calls (i know there's a lot of them), but it still seems to wig out.
Have been trying to google some 'best practices' for this, but not sure what's going on here... Any help would be greatly appreciated! Thanks.
Target: iOS8 in Xcode 6.1
Here's my code for cellForRowAtIndexPath:
if([peep getImage].length > 4) // if URL smaller than 4, it's probably not a valid URL
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if([peep getImage])
{
if(![imageCache objectForKey:indexPath])
{
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[peep getImage]]]];
if(image != nil) {
[imageCache setObject:image forKey:indexPath];
[cell.peepImage setImage:[imageCache objectForKey:indexPath]];
[[cell peepImage] setNeedsDisplay];
}
});
}
else
{
dispatch_async(dispatch_get_main_queue(), ^{
[[cell peepImage] setImage:[imageCache objectForKey:indexPath]];
[[cell peepImage] setNeedsDisplay];
});
}
}
[cell.peepImage setNeedsDisplay];
});
}
else
{
cell.peepImage.image = [UIImage imageNamed: #"defaultuser.jpg"];
}
[cell setNeedsDisplay];
return cell;
}

My guess is the creation of the image on the main thread. By moving it off the main thread your problem should be fixed.
if([peep getImage].length > 4) // if URL smaller than 4, it's probably not a valid URL
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if([peep getImage])
{
if(![imageCache objectForKey:indexPath])
{
// Move image creation here
__block UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[peep getImage]]]];
dispatch_async(dispatch_get_main_queue(), ^{
if(image != nil) {
[imageCache setObject:image forKey:indexPath];
[cell.peepImage setImage:[imageCache objectForKey:indexPath]];
[[cell peepImage] setNeedsDisplay];
}
});
}
else
{
dispatch_async(dispatch_get_main_queue(), ^{
[[cell peepImage] setImage:[imageCache objectForKey:indexPath]];
[[cell peepImage] setNeedsDisplay];
});
}
}
[cell.peepImage setNeedsDisplay];
});
}
else
{
cell.peepImage.image = [UIImage imageNamed: #"defaultuser.jpg"];
}
[cell setNeedsDisplay];
return cell;

Related

Refreshing UITableViewCell after fetching image data by dispatch_async

I'm writing app that shows TableView with entries that contains images.
I'm trying to fetch image by executing this line of code inside cellForRowAtIndexPath method:
cell.detailTextLabel.text = [artistData objectForKey:generesKey];
dispatch_async(backgroundQueue, ^{
NSURL *url_img = [NSURL URLWithString:[artistData objectForKey:pictureKey]];
NSData* data = [NSData dataWithContentsOfURL:
url_img];
cell.imageView.image = [UIImage imageWithData:data];
[self performSelectorOnMainThread:#selector(refreshCell:) withObject:cell waitUntilDone:YES];
});
After setting image I perform selector that contains:
-(void)refreshCell:(UITableViewCell*)cell{
[cell setNeedsDisplay];
[self.view setNeedsDisplay];
[self.tableViewOutlet setNeedsDisplay];
}
And image is not shown but when I click on cell or scroll entire list, images are shown. Why my View is not refreshing? Did I missed something?
It should be enough to call setNeedsLayout() on a cell.
In swift 4 it looks like this:
DispatchQueue.global().async {
let data = try? Data(contentsOf: URL(string: imageUrl)!)
DispatchQueue.main.async {
cell.imageView?.image = UIImage(data: data!)
cell.setNeedsLayout()
}
}
You'll could always reload the cell by calling [self.tableView reloadRowsAtIndexPaths#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
In order to prevent the infinite loop once you successfully download an image you'll want to cache the results. How long you cache is up to you.
NSCache *imageCache = [[NSCache alloc] init];
imageCache.name = #"My Image Cache";
UIImage *image = [imageCache objectForKey:url_img];
if (image) {
cell.imageView.image = image;
} else {
// Do your dispatch async to fetch the image.
// Once you get the image do
[imageCache setObject:[UIImage imageWithData:data] forKey:url_img];
}
You'll want the imageCache to be a property at the ViewController level. Don't create one each time incellForRowAtIndexPath
It might be related to interacting with the UI from a background queue. Try this:
dispatch_async(backgroundQueue, ^{
NSURL *url_img = [NSURL URLWithString:[artistData objectForKey:pictureKey]];
NSData* data = [NSData dataWithContentsOfURL:url_img];
dispatch_async(dispatch_get_main_queue(), ^{
cell.imageView.image = [UIImage imageWithData:data];
});
});

NSCache holds strong pointer to UIImage instantiated with imageWithData: and does not remove from memory on unload

I have a View Controller with a property galleryCache and when an image is downloaded using GCD and imageWithData: the image is added to the cache successfully with a key. However, when the view controller is dismissed it keeps strong pointers to those downloaded images causing them not to be removed from memory. Even if I use the removeAllObjects method on the cache in viewDidDisappear: memory does not clear up.
Does anyone know why this might be?
Here is the code for the method which downloads the images.
- (void)imageForFootageSize:(FootageSize)footageSize withCompletionHandler:(void (^)(UIImage *image))completionBlock
{
if (completionBlock) {
__block UIImage *image;
// Try getting local image from disk.
//
__block NSURL *imageURL = [self localURLForFootageSize:footageSize];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
image = [UIImage imageWithData:[NSData dataWithContentsOfURL:imageURL]];
dispatch_async(dispatch_get_main_queue(), ^{
if (image) {
completionBlock(image);
} else {
//
// Otherwise try getting remote image.
//
imageURL = [self remoteURLForFootageSize:footageSize];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
dispatch_async(dispatch_get_main_queue(), ^{
image = [UIImage imageWithData:imageData];
if (image) {
//
// Save remote image to disk
//
NSURL *photoDirectoryURL = [Footage localURLForDirectory];
// Create the folder(s) where the photos are stored.
//
[[NSFileManager defaultManager] createDirectoryAtPath:[photoDirectoryURL path] withIntermediateDirectories:YES attributes:nil error:nil];
// Save photo
//
NSString *localPath = [[self localURLForFootageSize:footageSize] path];
[imageData writeToFile:localPath atomically:YES];
}
completionBlock(image);
});
});
}
});
});
}
}
Methods which use the above class method to fetch and process the UIImage in the completionHandler.
Method inside UICollectionViewCell subclass.
- (void)setPhoto:(Photo *)photo withImage:(UIImage *)image
{
[self setBackgroundColor:[UIColor blackColor]];
[self.imageView setBackgroundColor:[UIColor clearColor]];
if (photo && !image) {
[photo imageForFootageSize:[Footage footageSizeThatBestFitsRect:self.bounds]
withCompletionHandler:^(UIImage *image) {
if ([self.delegate respondsToSelector:#selector(galleryPhotoCollectionViewCell:didLoadImage:)]) {
[self.delegate galleryPhotoCollectionViewCell:self didLoadImage:image];
}
image = nil;
}];
}
[self.imageView setImage:image];
BOOL isPhotoAvailable = (BOOL)(image);
[self.imageView setHidden:!isPhotoAvailable];
[self.activityIndicatorView setHidden:isPhotoAvailable];
}
Method in UICollectionView data source delegate
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
DIGalleryPhotoCollectionViewCell *photoCell = [collectionView dequeueReusableCellWithReuseIdentifier:photoCellIdentifier forIndexPath:indexPath];
[photoCell setDelegate:self];
Footage *footage = [self footageForIndexPath:indexPath];
Photo *photo = ([footage isKindOfClass:[Photo class]]) ? (Photo *)footage : nil;
if (photo) {
//
// Photo
//
[photoCell setPhoto:photo withImage:[self.galleryCache objectForKey:photo.footageID]];
}
return photoCell;
}
Here are the other relevant methods:
- (void)galleryPhotoCollectionViewCell:(DIGalleryPhotoCollectionViewCell *)cell didLoadImage:(UIImage *)image
{
NSIndexPath *indexPath = [self.galleryCollectionView indexPathForCell:cell];
Footage *footage = [self footageForIndexPath:indexPath];
if ([footage isKindOfClass:[Footage class]]) {
Photo *photo = (Photo *)footage;
UIImage *cachedImage = [self.galleryCache objectForKey:photo.footageID];
if (!cachedImage) {
cachedImage = image;
[self.galleryCache setObject:image forKey:photo.footageID];
}
[cell setPhoto:photo withImage:image];
}
}
And also my getter method for the NSCache property galleryCache
- (NSCache *)galleryCache
{
if (!_galleryCache) {
_galleryCache = [[NSCache alloc] init];
}
return _galleryCache;
}
EDIT
Here is a snapshot of Instruments showing the retain count history of one of the NSCache once its owner (a View Controller) is dismissed.
I'm not seeing anything obvious here, though I'd suggest putting a breakpoint where you purge the cache and make sure that's actually happening like you think it is.
If you still don't find it, you can run Allocations tool in Instruments and turn on "record reference counts" (see latter part of this answer, iOS app with ARC, find who is owner of an object), and you can find out precisely where your lingering strong reference is, at which point you can tackle the remediation.
The other obvious solution is to eliminate all of this code and use a proven image caching tool, like SDWebImage which does a lot of the memory and persistent storage caching for you. It's a pretty decent implementation.
OK, so after re examining my own code and re examining properties for the billionth x n time, it turns out my error was assigning the delegate property as a 'strong' type. Lesson learned: ALWAYS set delegates as WEAK.
I will definitely have to learn more about Instruments, however.

Caching with UIImage and downloaded images

I have a class method which fetches images with a completion block. This fetched UIImage is added to an NSCache with a relevant key. This seems to work as expected, however, in the method which fetches images I am using a UIImage's imageWithData: method, which I have discovered does not cache it's data, only imageNamed: does.
I am understandably getting memory warnings because of this, how do I make sure the images loaded with UIImage's imageWithData: method are removed from memory when not needed anymore?
EDIT
Here is the code for the method which downloads the images.
- (void)imageForFootageSize:(FootageSize)footageSize withCompletionHandler:(void (^)(UIImage *image))completionBlock
{
if (completionBlock) {
__block UIImage *image;
// Try getting local image from disk.
//
__block NSURL *imageURL = [self localURLForFootageSize:footageSize];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
image = [UIImage imageWithData:[NSData dataWithContentsOfURL:imageURL]];
dispatch_async(dispatch_get_main_queue(), ^{
if (image) {
completionBlock(image);
} else {
//
// Otherwise try getting remote image.
//
imageURL = [self remoteURLForFootageSize:footageSize];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
dispatch_async(dispatch_get_main_queue(), ^{
image = [UIImage imageWithData:imageData];
if (image) {
//
// Save remote image to disk
//
NSURL *photoDirectoryURL = [Footage localURLForDirectory];
// Create the folder(s) where the photos are stored.
//
[[NSFileManager defaultManager] createDirectoryAtPath:[photoDirectoryURL path] withIntermediateDirectories:YES attributes:nil error:nil];
// Save photo
//
NSString *localPath = [[self localURLForFootageSize:footageSize] path];
[imageData writeToFile:localPath atomically:YES];
}
completionBlock(image);
});
});
}
});
});
}
}
EDIT 2
Methods which use the above class method to fetch and process the UIImage in the completionHandler.
Method inside UICollectionViewCell subclass.
- (void)setPhoto:(Photo *)photo withImage:(UIImage *)image
{
[self setBackgroundColor:[UIColor blackColor]];
[self.imageView setBackgroundColor:[UIColor clearColor]];
if (photo && !image) {
[photo imageForFootageSize:[Footage footageSizeThatBestFitsRect:self.bounds]
withCompletionHandler:^(UIImage *image) {
if ([self.delegate respondsToSelector:#selector(galleryPhotoCollectionViewCell:didLoadImage:)]) {
[self.delegate galleryPhotoCollectionViewCell:self didLoadImage:image];
}
image = nil;
}];
}
[self.imageView setImage:image];
BOOL isPhotoAvailable = (BOOL)(image);
[self.imageView setHidden:!isPhotoAvailable];
[self.activityIndicatorView setHidden:isPhotoAvailable];
}
Method in UICollectionView data source delegate
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
DIGalleryPhotoCollectionViewCell *photoCell = [collectionView dequeueReusableCellWithReuseIdentifier:photoCellIdentifier forIndexPath:indexPath];
[photoCell setDelegate:self];
Footage *footage = [self footageForIndexPath:indexPath];
Photo *photo = ([footage isKindOfClass:[Photo class]]) ? (Photo *)footage : nil;
if (photo) {
//
// Photo
//
[photoCell setPhoto:photo withImage:[self.galleryCache objectForKey:photo.footageID]];
}
return photoCell;
}
Here are the other relevant methods:
- (void)galleryPhotoCollectionViewCell:(DIGalleryPhotoCollectionViewCell *)cell didLoadImage:(UIImage *)image
{
NSIndexPath *indexPath = [self.galleryCollectionView indexPathForCell:cell];
Footage *footage = [self footageForIndexPath:indexPath];
if ([footage isKindOfClass:[Footage class]]) {
Photo *photo = (Photo *)footage;
UIImage *cachedImage = [self.galleryCache objectForKey:photo.footageID];
if (!cachedImage) {
cachedImage = image;
[self.galleryCache setObject:image forKey:photo.footageID];
}
[cell setPhoto:photo withImage:image];
}
}
And also my getter method for the NSCache property galleryCache
- (NSCache *)galleryCache
{
if (!_galleryCache) {
_galleryCache = [[NSCache alloc] init];
}
return _galleryCache;
}
Instead of rolling your own image downloading and caching solution you might be better off using SDWebImage. Then you don't have to worry about the downloading, caching or anything. SDWebImage also using disk caching so you don't have to worry about freeing memory.
SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager downloadWithURL:imageURL options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize)
{
// progression tracking code
} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished)
{
if (image)
{
// do something with image
}
}];
I'm not sure but you also might have a retain cycle:
__weak typeof(self) weakSelf = self;
[photo imageForFootageSize:[Footage footageSizeThatBestFitsRect:self.bounds] withCompletionHandler:^(UIImage *image) {
if ([weakSelf.delegate respondsToSelector:#selector(galleryPhotoCollectionViewCell:didLoadImage:)])
{
[weakSelf.delegate galleryPhotoCollectionViewCell:weakSelf didLoadImage:image];
}
image = nil;
}];

SDWebImage thread hogging CPU

here is my issue :
I have a UITableView containing custom UITableViewCells. Each of those UITableViewCell (called HomePicCell) is associated to a Pic object which has a property pointing to an image URL.
As soon as my cell is displayed, I start downloading this image using SDWebImage manager.
Everything is working smoothly, but after 20 to 80 seconds, some threads start hogging the CPU. The device then become a perfect hand heater for those cold winter nights, but I'd rather skip this feature for now !
I can't really put my finger on what would cause this issue. I don't think a retain loop would be the problem as it would only hog the memory. An experimented opinion would really help.
Here is my code :
UITableView Datasource
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString* cellIdentifier = [#"HomePicCell" stringByAppendingString:[Theme sharedTheme].currentTheme];
HomePicCell* cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (!cell) {
cell = [[HomePicCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
}
if(self.pics.count>0){
Pic* pic = self.pics[indexPath.section];
[cell configureWithPic:pic];
}
return cell;
}
HomePicCell (configureWithPic:)
- (void)configureWithPic:(Pic*)pic
{
self.pic = pic;
// Reinit UI
[self.progressView setHidden:NO];
[self.errorLabel setHidden:YES];
[self.photoImageView setAlpha:0];
[self.progressView setProgress:0];
[self.pic downloadWithDelegate:self];
}
Pic
- (void) downloadWithDelegate:(id<PicDownloadDelegate>)delegate
{
SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager downloadWithURL:self.url options:0 progress:^(NSUInteger receivedSize, long long expectedSize) {
if(expectedSize>0){
float progress = [#(receivedSize) floatValue]/[#(expectedSize) floatValue];
[delegate pic:self DownloadDidProgress:progress];
}
} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished) {
self.isGif = #(image.images.count>1);
if(image){
if(cacheType == SDImageCacheTypeNone){
[delegate pic:self DownloadDidFinish:image fromCache:NO];
}else{
[delegate pic:self DownloadDidFinish:image fromCache:YES];
}
}else{
[delegate pic:self DownloadFailWithError:error];
}
}];
}
HomePicCell (delegate methods)
- (void)pic:(Pic*)pic DownloadDidFinish:(UIImage *)image fromCache:(BOOL)fromCache
{
if(![pic isEqual:self.pic]){
return;
}
[self.progressView setHidden:YES];
self.photoImageView.image = image;
[self updateUI];
}
- (void)pic:(Pic*)pic DownloadFailWithError:(NSError *)error
{
if(![pic isEqual:self.pic]){
return;
}
[self.errorLabel setHidden:NO];
[self.progressView setHidden:YES];
}
- (void)pic:(Pic*)pic DownloadDidProgress:(float)progress
{
if(![pic isEqual:self.pic]){
return;
}
[self.progressView setProgress:progress+.01f];
}
Thanks !
The issue is apparently fixed by switching to SDWebImage 3.0 (instead of 3.3).
I'll go ahead and declare an issue on the project github page to see if some people have had the same problem.

UIImageView Scrolling halts for a second

I am trying to use uiimage-from-animated-gif to load some GIFs into my UITableView Cells. But the problem is that everytime I try to load an image from my Cache, the cell halts a little before it scrolls.
This is the code I was using
postGif = (UIImageView *)[cell viewWithTag:104];
if ([[ImageCache sharedImageCache] DoesExist:post[#"gif"]] == true)
{
image = [[ImageCache sharedImageCache] GetImage:post[#"gif"]];
postGif.image = [UIImage animatedImageWithAnimatedGIFData:image];
}else
{
dispatch_queue_t backgroundQueue = dispatch_queue_create("cacheImage", 0);
dispatch_async(backgroundQueue, ^{
NSData *imageData = [[NSData alloc] initWithContentsOfURL: [NSURL URLWithString:post[#"gif"]]];
dispatch_async(dispatch_get_main_queue(), ^{
// Add the image to the cache
[[ImageCache sharedImageCache] AddImage:post[#"gif"] image:imageData];
NSIndexPath* ind = [NSIndexPath indexPathForRow:0 inSection:indexPath.section];
[self.feedTableView beginUpdates];
[self.feedTableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:ind] withRowAnimation:UITableViewRowAnimationNone];
[self.feedTableView endUpdates];
});
});
}
postGif.layer.cornerRadius = 2.0;
postGif.layer.masksToBounds = YES;
[cell.contentView addSubview:postGif];
I tried background queue as well. but the Image load is really slow with it.
if ([[ImageCache sharedImageCache] DoesExist:post[#"gif"]] == true)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
(unsigned long)NULL), ^(void) {
NSData *image = [[ImageCache sharedImageCache] GetImage:post[#"gif"]];
postGif.image = [UIImage animatedImageWithAnimatedGIFData:image];
});
}else
{
dispatch_queue_t backgroundQueue = dispatch_queue_create("cacheImage", 0);
dispatch_async(backgroundQueue, ^{
NSData *imageData = [[NSData alloc] initWithContentsOfURL: [NSURL URLWithString:post[#"gif"]]];
dispatch_async(dispatch_get_main_queue(), ^{
// Add the image to the cache
[[ImageCache sharedImageCache] AddImage:post[#"gif"] image:imageData];
NSIndexPath* ind = [NSIndexPath indexPathForRow:0 inSection:indexPath.section];
[self.feedTableView beginUpdates];
[self.feedTableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:ind] withRowAnimation:UITableViewRowAnimationNone];
[self.feedTableView endUpdates];
});
});
}
I am not sure what am I doing wrong. maybe I am not grasping a concept.
Maycoff suggested that I should not modify properties of user interface objects (like a UIImageView) from a background queue and that I need to create the instance of UIImage on a background queue, can someone please explain it to me what is it that I am not grasping?
Thanks for your help in advance.
The problem in your earlier code was that you were creating the UIImage from the GIF data on the main queue. Since decoding a GIF (especially one with many frames) can take some time, you were blocking the main thread, which is why the table view stopped scrolling momentarily each time it needed to display another image.
The problem in your later code is that you are modifying the image property of a UIImageView on a background queue. You must only modify user interface objects on the main queue.
What you need to do is create the UIImage on a background queue, then dispatch back to the main queue to update the UIImageView's image property. Thus:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
(unsigned long)NULL), ^(void) {
NSData *data = [[ImageCache sharedImageCache] GetImage:post[#"gif"]];
UIImage *image = [UIImage animatedImageWithAnimatedGIFData:data];
dispatch_async(dispatch_get_main_queue(), ^{
postGif.image = image;
}
});

Resources