Reputation: 13429
I have this simple UITableView and each cell has an image corresponding to it. All I'm doing is displaying a title for the image and the image itself in each cell. Here is my cellForRowAtIndexPath:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// this is where the data for each cell is
NSDictionary *dataForThisCell = cachedData.posts[indexPath.row][@"data"];
// this creates a new cell or grabs a recycled one, I put NSLogs in the if statement to make sure they are being recycled, they are.
post *cell = (post *) [self.tableView dequeueReusableCellWithIdentifier:@"postWithImage"];
if (cell == nil) {
cell = [[[NSBundle mainBundle]loadNibNamed:@"postWithImage" owner:self options:nil]objectAtIndex:0];
[cell styleCell];
}
// if this cell has an image we need to stick it in the cell
NSString *lowerCaseURL = [dataForThisCell[@"url"] lowercaseString];
if([lowerCaseURL hasSuffix: @"gif"] || [lowerCaseURL hasSuffix: @"bmp"] || [lowerCaseURL hasSuffix: @"jpg"] || [lowerCaseURL hasSuffix: @"png"] || [lowerCaseURL hasSuffix: @"jpeg"]) {
// if this cell doesnt have an UIImageView, add one to it. Cells are recycled so this only runs several times
if(cell.preview == nil) {
cell.preview = [[UIImageView alloc] init];
[cell.contentView addSubview: cell.preview];
}
// self.images is an NSMutableDictionary that stores the width and height of images corresponding to cells.
// if we dont know the width and height for this cell's image yet then we need to know now to store it
// once the image downloads, and then cause our table to reload so that heightForRowAtIndexPath
// resizes this cell correctly
Boolean shouldReloadData = self.images[dataForThisCell[@"name"]] == nil ? YES : NO;
// download image
[cell.preview cancelImageRequestOperation];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString: dataForThisCell[@"url"]]];
[request addValue:@"image/*" forHTTPHeaderField:@"Accept"];
[cell.preview setImageWithURLRequest: request
placeholderImage: [UIImage imageNamed:@"thumbnailLoading.png"]
success: ^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
// if we indicated earlier that we didnt know the dimensions of this image until
// just now after its been downloaded, then store the image dimensions in self.images
// and tell the table to reload so that heightForRowAtIndexPath
// resizes this cell correctly
if(shouldReloadData) {
NSInteger imageWidth = image.size.width;
NSInteger imageHeight = image.size.height;
if(imageWidth > [ColumnController columnWidth]) {
float ratio = [ColumnController columnWidth] / imageWidth;
imageWidth = ratio * imageWidth;
imageHeight = ratio* imageHeight;
}
if(imageHeight > 1024) {
float ratio = 1024 / imageHeight;
imageHeight = ratio * imageHeight;
imageWidth = ratio* imageWidth;
}
self.images[dataForThisCell[@"name"]] = @{ @"width": @(imageWidth), @"height": @(imageHeight), @"titleHeight": @([post heightOfGivenText: dataForThisCell[@"title"]]) };
[self.tableView reloadData];
// otherwise we alreaady knew the dimensions of this image so we can assume
// that heightForRowAtIndexPath has already calculated the correct height
// for this cell
}else{
// assign the image we downloaded to the UIImageView within the cell
cell.preview.image = image;
// position the image
NSInteger width = [self.images[dataForThisCell[@"name"]][@"width"] integerValue];
NSInteger height = [self.images[dataForThisCell[@"name"]][@"height"] integerValue];
cell.preview.frame = CGRectMake( ([ColumnController columnWidth] - width)/2 , [self.images[dataForThisCell[@"name"]][@"titleHeight"] integerValue] + 10, width, height);
}
}
failure: ^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {}];
}
// set title of the cell
cell.title.text = [NSString stringWithFormat: @"%@\n\n\n\n\n", dataForThisCell[@"title"]];
`enter code here`// ask for a restyle
[cell setNeedsLayout];
// returns my customized cell
return cell;
}
What happens is that everything works exactly how I want it to, however once I scroll down past around 100 cells or so the background of my app goes black for a few seconds and then I see my homescreen (I've seen some people call this the HSOD - home screen of death). Sometimes in the console in xcode I see memory warnings before a crash and sometimes I do not.
I know for a fact that whatever the problem is, it has to do with putting images into the cells. If I comment out just this line:
cell.preview.image = image;
Then everything works fine and it doesn't crash any more (but then of course the images are not being displayed in the cells).
The cells are being reused and I know that's working, for good measure I set the UIImageView's image property to nil:
- (void) prepareForReuse {
[super prepareForReuse];
if(self.preview != nil)
self.preview.image = nil;
}
and in my appDelegate I also define this:
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
[UIImageView clearAFImageCache];
}
Which deletes the image cache but that doesn't fix the problem either (and anyway iOS should clear the image caches upon memory warnings automatically anyway).
I ran analyze on my project and it reports no memory leaks, and here is the profiler showing that, as well as showing the allocations at the time of the crash:
Other than the occasional memory warning in the console which appears about 2/3rds of the time the app crashes, there are no other errors that appear in the console, and I do not hit any breakpoints or exceptions.
Upvotes: 0
Views: 2017
Reputation: 13429
I've found a solution, albeit not a perfect one:
The AFNetworking library is brilliant and I assume the cause of my problem lies within my own code or my lack of understanding as to how NSCache works.
AFNetworking caches images using Apple's NSCache. NSCache is similar to NSMutableDictionary, but releases objects when memory is spread thin (see more here).
Within UIImageView+AFNetworking.m I located the definition of
+ (AFImageCache *)af_sharedImageCache
And altered it to resemble this:
+ (AFImageCache *)af_sharedImageCache {
static AFImageCache *_af_imageCache = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_af_imageCache = [[AFImageCache alloc] init];
_af_imageCache.countLimit = 35;
});
return _af_imageCache;
}
The important line here is
_af_imageCache.countLimit = 35;
This tells the NSCache object being used in AFNetworking to cache images that it must only cache up to a maximum of 35 things.
For reasons unknown to me, iOS was not automatically purging objects from the cache as it should, and calling removeAllObjects on the cache was not working either. This solution is hardly ideal because it may not utilize the cache to its full potential or may over use the cache, but for the meantime it atleast stops the cache from attempting to store an infinite number of objects.
Upvotes: 1
Reputation: 19544
All of those allocations are you creating new table view cells each time they're requested, rather than reusing existing ones. Without setting a reuseIdentifier
for cells created from UINib
, dequeueReusableCellWithIdentifier:
will always return `nil.
To fix this, add the following code (as referenced in this question):
[self.tableView registerNib:[UINib nibWithNibName:@"nibname"
bundle:nil]
forCellReuseIdentifier:@"cellIdentifier"];
Upvotes: 2