Memory Warning in UICollectionView (with image) - memory-management

I am currently working on a UICollectionView with a lot of images. However, it sometimes crashes in this view with memory warning. I am using AFNetworking and UIImageView+AFNetworking category to set image through setImageWithURL: method. One issue can be caching. I am not sure if AFNetworking deals with image caching. Anyway, is there a way to optimize this code in terms of memory management? Or if I am to implement didReceiveMemoryWarning method in this view controller, what can be put in this method? I attach the code for cellForItemAtIndexPath for this collection view.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"RecipeCell" forIndexPath:indexPath];
// setting the image view for the cell using AFNetworking. Does this do caching automatically?
UIImageView *recipeImageView = (UIImageView *)[cell viewWithTag:6];
if (PRODUCTION) {
[recipeImageView setImageWithURL:[[self.recipes objectAtIndex:indexPath.row] objectForKey:#"recipe_image"] placeholderImage:[UIImage imageNamed:#"default_recipe_picture.png"]];
} else {
[recipeImageView setImageWithURL:[NSString stringWithFormat:#"http://localhost:5000/%#", [[self.recipes objectAtIndex:indexPath.row] objectForKey:#"recipe_image"]] placeholderImage:[UIImage imageNamed:#"default_recipe_picture.png"]];
}
// configure the back of the cell. fill all the info.
UITextView *recipeNameView = (UITextView *)[cell viewWithTag:8];
recipeNameView.text = [NSString stringWithFormat:#"%#", [[self.recipes objectAtIndex:indexPath.row] objectForKey:#"recipe_name"]];
UILabel *recipeNameLabel = (UILabel *)[cell viewWithTag:2];
recipeNameLabel.text = [NSString stringWithFormat:#"%#", [[self.recipes objectAtIndex:indexPath.row] objectForKey:#"recipe_name"]];
NSDictionary *user = [[self.recipes objectAtIndex:indexPath.row] objectForKey:#"user"];
UIButton *chefNameButton = (UIButton *)[cell viewWithTag:3];
[chefNameButton setTitle:[NSString stringWithFormat:#"%# %#", [user objectForKey:#"first_name"], [user objectForKey:#"last_name"]] forState:UIControlStateNormal];
NSMutableArray *missingIngredientsStringArray = [[NSMutableArray alloc] init];
NSArray *missingIngredients = [[self.recipes objectAtIndex:indexPath.row] objectForKey:#"missing_ingredients"];
for (NSDictionary *missingIngredient in missingIngredients) {
[missingIngredientsStringArray addObject:[missingIngredient objectForKey:#"name"]];
}
NSString *missingIngredientsString = [missingIngredientsStringArray componentsJoinedByString:#","];
UITextView *missingIngredientsView = (UITextView *)[cell viewWithTag:4];
missingIngredientsView.text = [NSString stringWithFormat:#"%u Missing Ingredients: %#", missingIngredients.count, missingIngredientsString];
// configure the front of the cell. chef name button and missing ingredients and likes on front view
UIButton *frontNameButton = (UIButton *)[cell viewWithTag:11];
[frontNameButton setTitle:[NSString stringWithFormat:#"%# %#", [user objectForKey:#"first_name"], [user objectForKey:#"last_name"]] forState:UIControlStateNormal];
[frontNameButton sizeToFit];
frontNameButton.frame = CGRectMake(160 - [frontNameButton.titleLabel.text sizeWithFont:[UIFont boldSystemFontOfSize:13]].width - 7, frontNameButton.frame.origin.y, frontNameButton.frame.size.width, frontNameButton.frame.size.height);
UILabel *likesLabel = (UILabel *)[cell viewWithTag:9];
likesLabel.text = [NSString stringWithFormat:#"%# likes", [[self.recipes objectAtIndex:indexPath.row] objectForKey:#"likes"]];
UIButton *missingIngredientsButton = (UIButton *)[cell viewWithTag:12];
[missingIngredientsButton setBackgroundImage:[UIImage imageNamed:#"badge_green.png"] forState:UIControlStateSelected];
if (missingIngredients.count == 0) {
missingIngredientsButton.selected = YES;
[missingIngredientsButton setTitle:#"" forState:UIControlStateNormal];
} else {
missingIngredientsButton.selected = NO;
[missingIngredientsButton setTitle:[NSString stringWithFormat:#"%u", missingIngredients.count] forState:UIControlStateNormal];
}
// make back view invisible.
UIView *backView = [cell viewWithTag:1];
UIView *frontView = [cell viewWithTag:5];
frontView.alpha = 1.0;
backView.alpha = 0;
// adding flip gesture recognizers
UIView *flipView1 = [cell viewWithTag:12];
UIView *flipView2 = [cell viewWithTag:1];
UITapGestureRecognizer *flipGestureRecognizer1 = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(flipCell:)];
UITapGestureRecognizer *flipGestureRecognizer2 = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(flipCell:)];
[flipView1 addGestureRecognizer:flipGestureRecognizer1];
[flipView2 addGestureRecognizer:flipGestureRecognizer2];
return cell;
}
[Edit] I attach a screenshot of my Instruments run.
You can see that memory allocation increases as I just push segue and press back button repeatedly. Things that just keep increasing are CFData, CALayer, CABackingStore, UITableView. I doubt these are things that are created after segue, and they are not being released... Please help!

You're probably going to want some sort of image caching strategy to avoid re-downloading images. And UIImageView+AFNetworking category does cache images for you. But you may also have the responses being cached in the in-memory URL cache, which in this case is somewhat redundant.
So you might consider reducing or turning off the in-memory URL cache. I had the issue you're describing and the following reduced my memory issues quite a bit:
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil];
[NSURLCache setSharedURLCache:sharedCache];

AFNetworking automatically stores images in an NSCache collection, which automatically removes some or all of the images from memory on a low memory warning. AFNetworking is probably not your issue.
In fact, I don't think displaying images is your issue unless you're downloading lots of very large images and displaying them simultaneously. (If this is the case, you should try optimizing your images for display on the device so they don't need to be resized.)
One issue I see is that you are adding a gesture recognizer to the cell every time it comes into the view, but cells are reused, so when a cell comes in again you are adding unnecessary gesture recognizers to it. You could resolve this by subclassing UITableViewCell and assigning the gesture recognizers as properties. You could also resolve this by checking flipView1 and flipView2 to see if they have gesture recognizers attached before adding them. (I'm not sure if this is enough to cause a memory warning though.)
I'd recommend going to Build -> Profile and selecting the Allocations instrument. On the left, select Objective C only, and hide system calls. Then, scroll through your collection view and look at the instrument to see what's taking up all the memory.
UPDATE
Here's a screenshot of the Allocations tool:

Related

Switched from tableview to colelctionview now didselectitematindexpath not called

So I switched from a UITableView to a UICollectionView. The cells are all loading just fine and look beautiful, but now I can no longer select any of them. The taps are recognized in the logs, but it doesn't take me to the view that it's supposed to take me to anymore.
The only things that changed really are the ones the ones you see commented out in the "cellForItem" section. Everything else is identical to how it was when it was working in the TableView. It's just supposed to take me to the ViewController specified when you tap. Instead the tap is recorded, but it does nothing. Any help would be appreciated.
Also I tried getting rid of the [collectionView deselectItemAtIndexPath:indexPath animated:YES];
...but it didn't make any difference.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
PlaceCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
// Configure the cell
//UINib * placeCell = [UINib nibWithNibName:#"Shops" bundle:nil];
// NSArray *topLevelItems = [placeCell instantiateWithOwner:self options:nil];
// cell = [topLevelItems objectAtIndex:0];
Place *p = [_entries objectAtIndex:indexPath.row];
cell.placeName.text = p.PName;
NSLog(#"p:%#",p.PName);
cell.placeName.textColor = [UIColor colorWithRed:3/255.0f green:6/255.0f blue:4/255.0f alpha:1.0f];
NSString *pIcon;
pIcon = typeName;
//NSLog(pIcon);
cell.placeImg.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:p.PImage]]];
NSLog(#"i:%#",p.PImage);
return cell;
}
-(void) collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
NSLog(#"Tap Recognized");
[collectionView deselectItemAtIndexPath:indexPath animated:YES];
Place *p = [_entries objectAtIndex:indexPath.row];
DetailPlaceViewController * pvc = [[DetailPlaceViewController alloc] init];
[pvc setPlace:p];
[self.navigationController pushViewController:pvc animated:YES];
}
Well this one was fun...
For some odd reason my navigation controller isn't working... so of course using the navigation controller to load a view wasn't going to work. Simple enough solution... load the view without the navigation controller. I was just going to delete this question, but maybe somebody might be in a similar situation one day:
[self presentViewController:pvc animated:YES completion:nil];

Memory not being released for UICollectionView of large images

I have an NSMutableArray of 15 elements, all of them strings. The strings refer to an image name that is stored locally in the app (images approximately 640x640 each). I am displaying the array of images in a UICollectionView.
When I reload the collectionView to display the images, my memory usage shoots up as you would expect displaying large png's (though its footprint is much larger than I expected).
The real problem is that memory used by these images is never freed. So despite me removing all objects from the array, removing the collectionView and setting everything to nil, none of the memory is freed.
Why is this? I would expect my memory to return to it's original level if I removed these objects and removed the ViewController.
UPDATED: Code snippet
ViewController.m
#implementation ViewController
static NSString *CellIdentifier = #"photoCell";
-(void)viewDidLoad{
[super viewDidLoad];
self.photoCollectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0,0, self.view.frame.size.width, 320) collectionViewLayout:self.photoSpringFlowLayout];
[self.photoCollectionView setDataSource:self];
[self.photoCollectionView setDelegate:self];
[self.view addSubview:self.photoCollectionView];
[self.photoCollectionView registerClass:[PhotoPreviewCell class] forCellWithReuseIdentifier:CellIdentifier];
self.imagesAray = [[NSMutableArray alloc] initWithObjects:#"photo1", #"photo2", #"photo3", #"photo4", #"photo5", #"photo6", #"photo7", #"photo8", #"photo9", #"photo10", #"photo11", #"photo12", #"photo13", #"photo14", #"photo15", nil];
[self.photoCollectionView reloadData];
}
#pragma mark - UICollectionView Methods
-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView{
return 1;
}
-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
return self.imagesArray.count;
}
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
PhotoPreviewCell *cell = (PhotoPreviewCell*)[collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];
NSString *imageName = self.imagesArray[indexPath.row];
[cell.photoImage setImage:[UIImage imageNamed:imageName]];
return cell;
}
PhotoPreviewCell.m
#implementation PhotoPreviewCell
-(id)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if(self){
self.photoImage = [[UIImageView alloc] init];
self.photoImage.frame = CGRectMake(0, 0, 160, 160);
self.photoImage.contentMode = UIViewContentModeScaleAspectFit;
[self.contentView addSubview:self.photoImage];
}
return self;
}
Ok so the reason for this is that loading images from flash takes some time and OS tries to avoid loading it on and on, so it's caching loaded images. They're cached only when you're using imageNamed: method. If you're testing on simulator release all objects and try simulate memory warning. This should force OS to remove not used images from cache.

Programmatically created UICollectionView: cells overlap after scrolling

I'm trying to implement a custom UICollectionView that shows all elements from the asset library. While displaying the items is not that big of a deal, I run into a weird bug which I think has to do something with the layout I'm using.
Let's start with some screenshots to indicate what's going on. I'll just post the links to these images to be easy on your scroll wheels :-)
1. Initial
https://dl.dropboxusercontent.com/u/607872/stackoverflow/01-uicollectionview-start.png
Everything ok.
2. Scrolled down
https://dl.dropboxusercontent.com/u/607872/stackoverflow/02-uicollectionview-scrolleddown.png
The images from the first row (which isn't visible anymore) repeat behind the images in the last row. The first one is invisible because it is entirely overlapped.
3. Scrolled up again
https://dl.dropboxusercontent.com/u/607872/stackoverflow/03-uicollectionview-scrollup.png
The first image (in the background) is from the same row, but should be the third. The 2nd and 3rd image are from the last row.
Here's some code. I hope I didn't miss any relevant stuff - if so, please let me know.
MultiImagePickerController.m (:UICollectionViewController, protocols: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout)
- (void)loadView {
[...] // layout stuff
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
[layout setScrollDirection:UICollectionViewScrollDirectionVertical];
_collectionView = [[UICollectionView alloc] initWithFrame:collectionRect collectionViewLayout:layout];
[_collectionView setDataSource:self];
[_collectionView setDelegate:self];
[_collectionView registerClass:[MultiImagePickerCell class] forCellWithReuseIdentifier:#"MultiImagePickerCellIdentifier"];
[_collectionView setBackgroundColor:[UIColor whiteColor]];
[self.view addSubview:_collectionView];
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
MultiImagePickerCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"MultiImagePickerCellIdentifier" forIndexPath:indexPath];
if ( !cell ) {
CGSize sizeCell = [MultiImagePickerCell defaultSize];
cell = [[MultiImagePickerCell alloc] initWithFrame:CGRectMake( 0, 0, sizeCell.width, sizeCell.height )];
}
NSString *groupname = [self getKeyFromMutableDictionary:assetList forIndex:indexPath.section];
[cell addImageUsingAsset:[[assetList objectForKey:groupname] objectAtIndex:indexPath.row]];
return cell;
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
CGSize sizeCell = [MultiImagePickerCell defaultSize];
return CGSizeMake( sizeCell.width + 10.0, sizeCell.height + 10.0 );
}
MultiImagePickerCell - (void)addImageUsingAsset:(ALAsset *)asset;
This method adds a UIImageView and a UIImage to the cell to show the asset. Nothing special, in my opinion. When I set breakpoints in cellForItemAtIndexPath, I can see that the correct asset is being read from the assetList and the views are calculated correctly. The drawRect method of the cell is NOT implemented.
I think I experienced the same problem a few years back when I tried to programmatically create a UITableViewController, but I can't remember how I solved it.
Any suggestions?
Got it - I had to remove the subviews from my cells.

Showing Activity indicator while loading sqlite local database

I have a local SQLite Database and need to return a large number of records. It takes a several seconds to load so I want to add an activity indicator. The activity indicator seems to be running as it should but the problem is the pool isn't allowing the arrays to return any value see below.
- (void)viewDidLoad
{
[super viewDidLoad];
activityIndicator = [[[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray] autorelease];
activityIndicator.frame = CGRectMake(0.0, 0.0, 48.0, 48.0);
activityIndicator.center = self.view.center;
[self.view addSubview:activityIndicator];
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
[activityIndicator startAnimating];
[self performSelectorInBackground:#selector(loadData) withObject:nil];
}
//What I need to load from SQLITE
-(void)loadData {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // Path to the database
//CODED HERE FOR DATABASE TO OPEN AND QUERY HERE TO BUILD ARRAYS
NSString *firstField = [NSString stringWithUTF8String:cFirst];
NSString *secondField = [NSString stringWithUTF8String:cSecond];
NSString *thirdField = [NSString stringWithUTF8String:cThird];
[FirstArray addObject:firstField];
[SecondArray addObject:secondField];
[ThirdArray addObject:thirdField];
//Checking to see if records are being added to the arrays
NSString *recordAdded = [NSString stringWithFormat:#"%# - %# - %#", firstField, secondField, thirdField];
NSLog(#"Song: %#", recordAdded);
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
[activityIndicator stopAnimating];
[pool release];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
NSString *firstValue = [firstArray objectAtIndex:indexPath.row];
NSString *secondValue = [secondArray objectAtIndex:indexPath.row];
NSString *thirdValue = [thirdArray objectAtIndex:indexPath.row];
NSString *details = [NSString stringWithFormat:#"%# - %#", secondValue, thirdValue];
cell.textLabel.text = cellValue;
cell.detailTextLabel.text = details ;
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
return cell;
}
When I'm looking at the debugger I see that the arrays are being built as the activity indicator is going. The problem is that I think the pool release is releasing my arrays too.
The code returns the query in the table view fine when I don't add the Pool and activity indicator.
Can anyone help to point me in the right direction to not release the arrays if that's what is happening here? Any kind of help would be much appreciated. :-)
---Also---
After looking around more I found that I have to pass the arrays back to the main thread. This is what I gather from searching online.
[self performSelectorOnMainThread:#selector(done:) withObject:cellData waitUntilDone:NO];
How would I go about loading the table with these arrays?
Not sure about your pool releasing the array, your code seems pretty direct. You may want to check whether you are really getting any data from your database.
However, I notice that you stop animating the activity indicator in the background thread. UI activity should be done in the main thread. I just did this coding last night and solved my problem using this thread. Put your stop animating code in the selector specified by performSelectorOnMainThread.
The strings are fine because they are being added to the array.
It looks like your array is being released, but you don't show the code for it.
How are you creating the array itself? If it is autoreleased, it will be deallocated when you drain the pool. You should "retain" it instead of "auotrelease"ing it.
Try to add [tableview reloadData] at the end of the -(void)loadData method

SDWebImage and UITableViewCell

I'm working with SDWebImage and UITableView
- (UITableViewCell *)tableView:(UITableView *)the_tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
NSString *MyIdentifier = [NSString stringWithFormat:#"MyIdentifier"];
NSDictionary *info = [tableData objectAtIndex:indexPath.row];
UITableViewCell *cell = [the_tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:MyIdentifier] autorelease];
if(!addImage) [addImage release];
addImage = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"placeholder.png"]];
[addImage setFrame:CGRectMake(7, 10, 50, 50)];
[cell setIndentationLevel:6];
[cell.contentView addSubview:addImage];
}
if(info != NULL) {
cell.textLabel.text = [info objectForKey:#"title"];
cell.detailTextLabel.text = [NSString stringWithFormat:#"Version: %#",[info objectForKey:#"version"]];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
NSString *imageURL = [NSString stringWithFormat:#"%#",[info objectForKey:#"imageURL"]];
[addImage setImageWithURL:[NSURL URLWithString:imageURL] placeholderImage:[UIImage imageNamed:#"placeholder.png"]];
}
return cell;
}
Which Works Great for the first 6 Results (The amount that can fit on the immediate view)
But as I scroll down the list, it seems like it's just re-using images from the first 6 cells, and on some cells images change depending on their location on the screen.
Also, if I call reloadData, the images from the previous UITableCells stay on screen!
Am I doing something wrong here? I've followed the example code on github..
Thanks!
(Answered by the OP in a question edit. Moved here as a community wiki answer. See Question with no answers, but issue solved in the comments (or extended in chat) )
The OP wrote:
OK, So I found the problem.
For anyone else out there with the Same issue, this is where you're doing it wrong!
Notice how I'm adding the Image into SubView while creating a cell:
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:MyIdentifier] autorelease];
addImage = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"placeholder.png"]];
[addImage setFrame:CGRectMake(7, 10, 50, 50)];
[cell.contentView addSubview:addImage];
Well, what SDWebImage was trying to do is constantly update that addImage variable, which wasn't a property of my cell class.
So, what I did to fix this problem is create my own UITableViewCell subclass, that init's with a ImageView and I get SDWebImage to setImage on that property!
I hope this helps someone!

Resources