I am using AFNetworking 3.0's UIImageView+AFNetworking to display images in a UICollectionView.
However as I keep scrolling, the memory usage of my app keeps growing forever. I used Instruments Allocations tool and am able to narrow down the issue to CG Raster Data which keeps growing for every image which is loaded. Looking at the details of the CG Raster Data, The responsible caller is cgdataprovidercreatewithcopyofdata and responsible library is CoreGraphics. For each cell loaded, a 240 KB memory is wasted.
There are a lot of similar issues on stack overflow but none really help/have a solution.
I thought this might be due to cache, so I enabled following but didn't help at all:
NSURLCache * sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:10 * 1024 diskPath:nil];
[NSURLCache setSharedURLCache:sharedCache];
I tried wrapping the setImageWithURLRequest inside an autoreleasepool but didn't help.
I tried searching for cgdataprovidercreatewithcopyofdata in the entire app but I find no hits in my app or the afnetworking. However if I remove the loading of the image, then I don't see this issue.
Also if I remove the setting of the image inside the completion handler, the memory still grows. Meaning that setImageWithURLRequest itself is the culprit and not the setting of the image inside the completion handler.
I have been struggling with this for a while, any help would be appreciated!
Here's my code for the setting of image:
[cell.thumbnail cancelImageDownloadTask];
__weak UIImageView *weakImageView = cell.thumbnail;
#autoreleasepool {
[cell.thumbnail setImageWithURLRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:#"http://myurl/image.png"]]
placeholderImage:[UIImage imageNamed:#"placeholder.png"]
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
UIImageView *strongImageView = weakImageView; // make local strong reference to protect against race conditions
if (!strongImageView) return;
[UIView transitionWithView:strongImageView
duration:0.3
options:UIViewAnimationOptionTransitionCrossDissolve
animations:^{
strongImageView.image = image;
}
completion:NULL];
}
failure:NULL];
Here's screenshots from instruments:
I used #ajay gabani response to use SDWebImage instead of AFNetworking's ImageView.
In SDWebImage it at least provided a way to clear the cache plus you can control whether the image is cached in disk or only memory.
I set the maxMemoryCountLimit to 10 but it doesn't seem to do much in my testing:
SDImageCache *imageCache = [SDImageCache sharedImageCache];
imageCache.maxMemoryCountLimit=10;
I clear the cache every few seconds:
[NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:#selector(simulateMemoryWarning) userInfo:nil repeats:YES];
- (void)simulateMemoryWarning{
[[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationDidReceiveMemoryWarningNotification object:nil];
SDImageCache *imageCache = [SDImageCache sharedImageCache];
NSLog(#"Simulated");
[imageCache clearMemory];
[imageCache clearDisk];
}
Related
I have a memory leak that seems to be coming from a retain cycle. The memory allocation size is increasing every time this code runs:
- (void)nextPhoto {
self.photoIndex++;
if (self.photoIndex >= [self.photos count]) {
self.photoIndex = 0;
}
__weak Photo *photo = [self.photos objectAtIndex:self.photoIndex];
[[SDWebImageManager sharedManager] downloadImageWithURL:[NSURL URLWithString:photo.thumbnailURLString] options:SDWebImageRetryFailed progress:nil
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
}];
}
The code is looping on a 2 second timer:
self.timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:#selector(nextPhoto) userInfo:nil repeats:YES];
The total memory use increases without bounds until I get a memory overuse event.
Why is this code causing a retain cycle? Is there a special way I need to handle self in this situation?
self.photos is an NSMutableArray
self.photoIndex is an NSInteger
SDWebImageManager is a well maintained library: https://github.com/rs/SDWebImage and I use it in numerous other locations with no issues
I don't see any problem involving a retain cycle here, even if you use self in the completion block. the block owner is SDWebImageManager so no problems here. a retain cycle could occur if you store your block in a property of your viewController, cause it then would own a block that retains it... It's not what is happening here imho.
Now your problem, i presume, comes from the UIImage. I depends of what you do in the block of course but if your storing the images then, yes every 2 seconds a new one is created and then it will fail eventually. You should keep a cache of images that has already been downloaded and try to download them only if needed... Add a NSDictionary with url as key and UIImage as value for example, this way you will only download your images once.
Ok I should have slept on this one... The function is actually working exactly as it should and it was self.photos that was increasing without bounds. Putting a limit on the size of that array fixed the "leak".
For some reason when I'm downloading my image using GCD, the image will randomly start flickering.
I'll reset the content settings in simulator and it'll work once, then it'll just start flickering again.
This is the code I am using. I've got the reloads in there because if I don't reload it, the image doesn't show until I tap on the cell.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^(void) {
NSURL *url = [NSURL URLWithString:self.entries.arrayimage];
NSData *imgData = [NSData dataWithContentsOfURL:url];
dispatch_sync(dispatch_get_main_queue(), ^(void) {
cell.imageView.image = nil;
UIImage *img = [UIImage imageWithData:imgData];
cell.imageView.image = img;
[self.tableView reloadData];
});
});
return cell;
[self.tableView reloadData];
That's because you are using dispatch_async(dispatch_get_global_queue to async load imageData from file. Using this style load image in cellForRow will make cell image should should previous image first. Then finish async load, will call dispatch_sync(dispatch_get_main_queue(), to load the image you want to. Therefore, whenever you reloadData or any other methods to call cellForRow, the cell image will flicker.
I know you want to load image without blocking main thread, but it's not a good way.
Check out apple sample code for Lazy Image loading. And also I checked your code and found that you always downloading image from URL. Instead of that its good to download and save image in caches and then load image from next time in cellForRowAtIndexPath method from local caches if available.
Is this code in cellForRowAtIndexPath:?
If that is the case, the problem here is that all the cells are infinitely reloading the table. You should not be calling reloadData in any of datasource methods that are triggered by reloadData.
What you have is basically an infinite loop of reloading. (reloadData triggers cellForRowAtIndexPath: which once again triggers reloadData).
My suggestion is to use an external component for this as aeskreis posted in his comment.
SDWebImage is probably the best one out there and will allow you to simplify all of the code you have there into simply:
NSURL *url = [NSURL URLWithString:self.entries.arrayimage];
[cell.imageView setImageWithURL:url];
Okay Guys I found out the problem. It was constantly reloading the data which caused te flickering. instead of [self.tableView reloadData] I replaced it with this method:
[cell setNeedsLayout];
I believe this method detects if anything has been changed, and then updates it (from my memory of a couple hours ago so it's probably not 100% accurate), but that fixed my problem.
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
NSURL *url = [NSURL URLWithString:self.entries.arrayimage];
NSData *imgData = [NSData dataWithContentsOfURL:url];
dispatch_sync(dispatch_get_main_queue(), ^{
UIImage *img = [UIImage imageWithData:imgData];
cell.imageView.image = img;
//[self.tableView reloadData];
[cell setNeedsLayout];
});
});
return cell;
At this point I'm really fed up. It's been nearly a week now trying to solve this issue so I can move ahead. I've read multiple threads and done multiple searches in regards to my slow loading choppy UICollectionView.
I've tried to do this without any libraries as well as with SDWebImage and AFNetwork. It still doesn't fix things. Images loading isn't really a problem. The problem arrives when I scroll to cells that aren't currently showing on the screen.
As of now I've deleted all the code and all traces of any libraries and would like to get help in order to implement this properly. I've made about 2 posts on this already and this would be my third attempt coming from a different angle.
Information
My backend data is stored on Parse.com
I have access to currently loaded objects by calling [self objects]
My cellForItemAtIndex is a modified version that also returns the current object of an index.
From what I understand in my cellForItemAtIndex I need to check for an image, if there isn't one I need to download one on background thread and set it so it shows in the cell, then store a copy of it in cache so that if the associated cell goes off screen when I do scroll back to it I can use the cached image rather than downloading it again.
My custom parse collectionViewController gives me all the boiler plate code I need to get access to next set of objects, current loaded objects, pagination, pull to refresh etc. I really just need to get this collection view sorted. I never needed to do any of this with my tableview of a previous app which had much more images. It's really frustrating spending a whole day trying to solve an issue and getting no where.
This is my current collectionView cellForItemAtIndex:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath object:(PFObject *)object
{
static NSString *CellIdentifier = #"Cell";
VAGGarmentCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier: CellIdentifier forIndexPath:indexPath];
// check for image
// if there is a cached one use that
// if not then download one on background thread
// set my cells image view with that image
// cache image for re-use.
// PFFile *userImageFile = object[#"image"];
[[cell title] setText:[object valueForKey:#"title"]]; //title set
[[cell price] setText:[NSString stringWithFormat: #"£%#", [object valueForKey:#"price"]]]; //price set
return cell;
}
I am also using a custom collectionViewCell:
#interface VAGGarmentCell : UICollectionViewCell
#property (weak, nonatomic) IBOutlet UIImageView *imageView;
#property (weak, nonatomic) IBOutlet UITextView *title;
#property (weak, nonatomic) IBOutlet UILabel *price;
#property (weak, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator;
#end
If there's any more information you'd like please ask. I'd just like a clear example in code of how to do this correctly, if it still doesn't work for me then I guess there is something wrong some where within my code.
I'm going to continue reading through various threads and resources I've come across in the last few days. I can say one benefit in this experience is that I have a better understanding of threads and lazy loading but it is still very frustrated that I have made any progress with my actual app.
Incase you wondered here is my previous post: In a UICollectionView how can I preload data outside of the cellForItemAtIndexPath to use within it?
I'd either like to do this quick and manually or using the AFNetwork as that didn't cause any errors or need hacks like SDWebImage did.
Hope you can help
Kind regards.
You can make use of the internal cache used by NSURLConnection for this.
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
VAGGarmentCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"VAGGarmentCell" forIndexPath:indexPath];
//Standard code for initialisation.
NSURL *url; //The image URL goes here.
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:5.0]; //timeout can be adjusted
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError)
{
if (!connectionError)
{
UIImage *image = [UIImage imageWithData:data];
//Add image as subview here.
}
}];
.
.
return cell;
}
This is for a table view, but same concept basically. I had the same issue you were having. I had to check for a cached image, if not, retrieve it from a server. The main thing to watch out for is when you retrieve the image back, you have to update it in the collection view on the main thread. You also want to check if the cell is still visible on the screen. Here is my code as an example. teamMember is a dictionary and #"avatar" is the key which contains the URL of the user's image. TeamCommitsCell is my custom cell.
// if user has an avatar
if (![teamMember[#"avatar"] isEqualToString:#""]) {
// check for cached image, use if it exists
UIImage *cachedImage = [self.imageCache objectForKey:teamMember[#"avatar"]];
if (cachedImage) {
cell.memberImage.image = cachedImage;
}
//else retrieve the image from server
else {
NSURL *imageURL = [NSURL URLWithString:teamMember[#"avatar"]];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
// if valid data, create UIImage
if (imageData) {
UIImage *image = [UIImage imageWithData:imageData];
// if valid image, update in tableview asynch
if (image) {
dispatch_async(dispatch_get_main_queue(), ^{
TeamCommitsCell *updateCell = (id)[tableView cellForRowAtIndexPath:indexPath];
// if valid cell, display image and add to cache
if (updateCell) {
updateCell.memberImage.image = image;
[self.imageCache setObject:image forKey:teamMember[#"avatar"]];
}
});
}
}
});
}
}
NSURLCache is iOS's solution to caching retrieved data, including images. In your AppDelegate, initialize the shared cache via:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSURLCache *cache = [[NSURLCache alloc] initWithMemoryCapacity:8 * 1024 * 1024
diskCapacity:20 * 1024 * 1024
diskPath:nil];
[NSURLCache setSharedURLCache:cache];
return YES;
}
-(void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
[[NSURLCache sharedURLCache] removeAllCachedResponses];
}
Then use AFNetworking's UIImageView category to set the image using:
[imageView setImageWithURL:myImagesURL placeholderImage:nil];
This has proven to load images the second time around incredibly faster. If you are worried about loading images faster for the first time, you will have to create a way to determine when and how many images you want to load ahead of time. It is very common to load data using paging. If you are using paging and still are having trouble, consider using AFNetworking's:
- (void)setImageWithURLRequest:(NSURLRequest *)urlRequest
placeholderImage:(UIImage *)placeholderImage
success:(void (^)(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image))success
failure:(void (^)(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error))failure;
This way you can create an array of UIImages and using this method to return the images for each cell before dequeuing the cell. So in this case you would have two parallel arrays; one holding your data and the other holding corresponding UIImages. Memory management will eventually get out of hand so keep that in mind. If someone scrolls quickly to the bottom of the available cells, there is honestly not much else you can do since the data depends on the network connection of the user.
After several days the issue was my images were far too large. I had to resize them and this instantly solved my issue.
I literally narrowed things down and checked my images to find they were not being resized by the method I thought was resizing them. This is why I need to get myself used to testing.
I learnt a lot about GCD and caching in the past few days but this issue could have been solved much earlier.
Can anyone please enlighten me how to clear cache while scrolling through images?
i m using SDwebImage package.It is working till 110 photos after that it giving memory receive warning and getting crash i have tried following method also but no success:
SDImageCache *imageCache = [SDImageCache sharedImageCache];
[imageCache clearMemory];
[imageCache clearDisk];
[imageCache cleanDisk];``
after this code it scroll 10 more photo after that get crash.
try [imageCache release]; or autorelease while initiating or try writing in
-(void)dealloc
{
[super dealloc];
[imageCache release];
}
I have an app i am building it works fine but the image source i am using is from a website and when I switch back to my initial view it takes quite some time for it to load. My question is would there be a way to get this done and have the speed be faster.
here is the code I use to pull my image source
////Loads UIImageView from URL
todaysWallpaper.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:#"http://www.inkdryercreative.com/daily/archive/mondays/images/062-mondays-960x640-A.jpg"]]];
any help or a shove in the proper direction. Soon the image will change every time day as well so any help/thoughts on that would be of great appreciation.
The probleme here is that dataWithContentsOfURL: is on the main thread, so it will block your UI when the image is downloaded.
You have to download it asynchronously. For that I personally use a great piece of code i found on the internet : SDWebImage. It does exactly what you want.
wrap the UIImage creation in a block running asynchronously (code assumes ARC) which in turn calls your callback in the main thread
#implementation Foo
...
Foo* __weak weakSelf=self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIImage *image = [UIImage imageWithData: [NSData dataWithContentsOfURL:
[NSURL URLWithString:#"http://www.inkdryercreative.com/..jpg"]]];
dispatch_sync(dispatch_get_main_queue(),^ {
//run in main thread
[weakSelf handleDelayedImage:image];
});
});
-(void)handleDelayedImage:(UIImage*)image
{
todaysWallpaper.image=image;
}
the weakSelf trick ensures that your Foo class is properly cleaned up even if the URL request is still running