How can I improve UICollectionView efficiency? - ios

I have an iOS app in which there is a collection view that that can have up to a couple hundred cells in it. Each cell has 5 views in it, 4 UILabels and 1 UIImageView. When I run the app normally the app uses absurd amounts of memory whenever I scroll through a couples of rows. As in about 5Mb of memory for 3 rows. I tried removing all the code in the cellForItemAtIndexPath such that the collection view controller looked like this:
#import "CollectionView.h"
#implementation CollectionView
- (NSInteger)collectionView:(UICollectionView *)view numberOfItemsInSection:(NSInteger)section {
return 100;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Story_Cell_Small";
Cell *cell = [cv dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];
return cell;
}
#end
Yet the app still uses absurd amounts of data upon scrolling. When I opened the storyboard view for the cell and removed all the views in the cell and again measured my apps memory usage in Instruments there was no memory usage upon scrolling through the empty cells. I could see that were in fact cells based on the scroll bar moving.
So the point is that the views in the cells are using up huge amounts of memory without me doing anything accept placing them in the cell in the apps storyboard.
My question is then, how can I fix this? Am I doing something absurdly wrong?

How big are the images in your cells? Images are what eat memory up. How are you loading the images? Using imageNamed: will cache the images in memory. If you use imageWithContentsOfFile:, this will not cache the contents.

Related

UICollectionView doesn't reuse cell

I'm trying to imitate UITableView layout using UICollectionView.
layout.itemSize = CGSizeMake(CGRectGetWidth(self.view.bounds), 44.0f);
I register the reusable cell class.
[self.collectionView registerClass:[SampleCell class]
forCellWithReuseIdentifier:NSStringFromClass([SampleCell class])];
Note: SampleClass is just a subclass of UICollectionViewCell which contains nothing.
And conformed to the data source:
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 28;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass([SampleCell class])
forIndexPath:indexPath];
return cell;
}
I found that the SampleCell is not reused. To validate it, we can simply log the number of subviews in the UICollectionView.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
NSLog(#"number of subviews in collection view is: %li", (long)self.collectionView.subviews.count);
}
And after scrolling, I got this log:
number of subviews in collection view is: 30
number of subviews in collection view is: 30
number of subviews in collection view is: 30
number of subviews in collection view is: 30
Notice that there are 30 subviews (2 of those are the scrollview indicator).
That means that all of the 28 items are displayed without the invisible cells removed from superview. Why does this happen?
To make it easier for you, I made a sample project available on Github.
https://github.com/edwardanthony/UICollectionViewBug
Update:
I also checked the memory allocation using the memory graph hierarchy debugger and it's allocated 28 times.
I does work, it's just keeping a little more in memory due to more aggressive caching. If you try changing the number of items from 28 to 100 you will see that it stays at 33 subviews when you scroll.
Try adding the following code to your SampleCell class and you will see it gets called, but maybe not quite as you expect.
- (void)prepareForReuse {
[super prepareForReuse];
NSLog(#"prepareForReuse called");
}
UICollectionView has a more advanced caching scheme than UITableView (or at least as it used to have), which is the reason you see what you do. According to docs it says Cell prefetching is enabled by default:
UICollectionView provides two prefetching techniques you can use to
improve responsiveness:
Cell prefetching prepares cells in advance of
the time they are required. When a collection view requires a large
number of cells simultaneously—for example, a new row of cells in grid
layout—the cells are requested earlier than the time required for
display. Cell rendering is therefore spread across multiple layout
passes, resulting in a smoother scrolling experience. Cell prefetching
is enabled by default.
Data prefetching provides a mechanism whereby
you are notified of the data requirements of a collection view in
advance of the requests for cells. This is useful if the content of
your cells relies on an expensive data loading process, such as a
network request. Assign an object that conforms to the
UICollectionViewDataSourcePrefetching protocol to the
prefetchDataSource property to receive notifications of when to
prefetch data for cells.
You can turn off cell prefetching by adding this line to setupCollectionView function in your sample:
self.collectionView.prefetchingEnabled = NO;
Doing so will make your sample work as you expected. The subview count will drop to 18 in my case.
I suspect that counting subviews does not reflect cell reuse. It could be that subviews contains more than one reference to the same cell. To count the number of cells used, you could log how many times the UICollectionViewCell subclass gets initialised. Just override it's init method and put a print statement in there.
One other thing to note (sorry if it's aleady obvious), if all cells are visible on screen no reuse will occur. Cell reuse occurs when cells go off screen during scrolling.

UITableView & UICollectionView performance optimisation

Hello colleagues Apple developers!
I am creating an iOS application for both iPhone & iPad. The performance of UITableView and UICollectionView is very important feature in my application.
After long time spent for optimising, human eye can't tell the difference while scroll happens. Although, profiler still finds some issues.
The first inefficient thing is dequeuing. I have a UITableViewCell object, which user interface is created by using .xib files and auto layout constraints. Although, the time profiler instrument complains about the performance while dequeuing this specific cell.
One more problem I can't understand is a NSString setting as UILabel's text. This setter method is executed in method - tableView:cellForRowAtIndexPath:.
One more problem, that might be related with previous one, is a UIImage setting as UIImageView's image. This method is also executed in method - tableView:cellForRowAtIndexPath: and doesn't trigger any download or etc.
UITableViewCell objects, described above, are default size & really simple. The content view of the cell contains only 2 subviews: UILabel and UIImageView.
Images are downloaded according to Apple's example.
// Asks the data source for a cell to insert in a particular location of the table view.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// Dequeues & initializes category table view cell.
CategoryTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CategoryTableViewCellIdentifier];
APICategory *category = [self.categories objectAtIndex:indexPath.row];
if (category.thumbnail)
{
[cell updateThumbnailWithCategory:category];
}
else
{
if (!tableView.dragging && !tableView.decelerating)
{
[category thumbnailWithSuccess:^(UIImage *thumbnail)
{
if ([tableView.visibleCells containsObject:cell])
{
[cell updateThumbnailWithCategory:category];
}
}
failure:^(NSError *error)
{
// Handle thumbnail receive failure here!
}];
}
[cell updateThumbnailWithPlaceholder];
}
cell.category = [self.categories objectAtIndex:indexPath.row];
return cell;
}
Specific images used in this case are .png images 300x300 resolution and 24 kb of size.
These are the problems related with the performance of UITableView and UICollectionView.
Can anyone explain me, what could be the reasons for any of those issues?
Also, is there a way to improve it?

UICollectionView reuse cells too frequent

I know how to reuse cells and I know how to not reuse cells, but that's not what I'm asking.
What I need is still reusing some cells but make it less frequent.
Let's say I've 10 cells presenting on the screen(each cell with animating GIF file in it, so setting up a cell is consuming), what the UICollectionView doing now is like allocate 10 blocks of memory for each of the cells that is being presented, and make reuse of the memory when new cells are coming. This has made the reuse too frequent, so it is setting up the cells all the time and has made the scrolling of the UICollectionView a bit slow.
Here is the code that I'm using:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
ItemView *cell = [collectionView dequeueReusableCellWithReuseIdentifier:ITEM_VIEW_REUSE_ID forIndexPath:indexPath];
ItemData *itemData = [[_items objectAtIndex:indexPath.section] objectAtIndex:indexPath.row];
[cell setItemData:itemData];
return cell;
}
I'm thinking, is there a way that I can modify the internal reuse system. Like allocate 30 blocks of memory for each of the cells that is being presented and some cells that are not even presented, so it is not setting up the cells all the time, and I believe that will accelerate the performance of my app.

What can I do to stop CollectionView refreshing the cell which was displayed when scrolling?

I am using UICollectionView to display images.
Here is my code:
- (WaterFallCollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
WaterFallCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:collectionViewCellIdentifier forIndexPath:indexPath];
NSURL *itemTBURL = [NSURL URLWithString:self.items[indexPath.item][#"image"][#"thumbnailLink"]];
[cell setNeedsLayout];
cell.imageView.imageURL = itemTBURL;
return cell;
}
My problem is, for example, it has totally 10 images in collectionView, first I scroll down to last one, which the first image in collectionView will be invisible, when I scroll up to the first one, it has the refreshing effect and the program will jump into the method I showed. It looks like when the cell turns from invisible to visible state, iOS will force to call the method. I want to store the previously displayed cell, so when you scroll up, it won't have the refresh effect. What should I do? I know Twitter, Pinterest, Facebook they looks like wont' refresh the previous cells when you scroll up. I know it is feasible, just don't know how to do that. Thanks in advance.
What is "refresh effect"? What do you mean?
Everytime you scroll to new UICollectionViewCell, UICollectionView call cellForItemAtIndexPath: to get this cell.
Every cell is reusable. So, you can initialize cell for further reuse it in "- (void) prepareForReuse()" method of cell.
As far as I understand, every time you set imageView.imageURL - image start loading and after that, you show it, right? I think, you should cache this images after first loading.
In any case, there is no way to skip calling cellForItemAtIndexPath.

iOS dequeueReusableCellWithIdentifier is always returning null

I'm really frustrated at this point. Dequeueing a reusable cell with identifier is always returning null.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(cell == nil) {
NSLog(#"INIT");
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
return cell;
}
What am i doing wrong here? Thanks.
You're doing everything right, everything is working as it should. iOS will create enough new cells to fill the screen (plus one). It will start reusing these cells only when your UITableView contains more rows than can fit on one screen and then the user scrolls.
You'll find that if you have a datasource will say, 100 items in it and then scroll, you'll only have your log message show probably 11 times (depends on how many cells fit on your screen) instead of 100 as iOS will start recycling cells as you scroll.
With large lists, it would use too much memory to create new views for every possible row in a UITableView. The alternative would be to allocate new views for rows as you scroll. However, this would create a performance bottleneck that would cause laggy scrolling in any UITableView.
Apple mention the performance bottleneck in their documentation on UITableViews.
Reuse cells. - Object allocation has a performance cost, especially if the allocation has to happen repeatedly over a short period—say, when the user scrolls a table view. If you reuse cells instead of allocating new ones, you greatly enhance table view performance.
Did you set your cell's reuse identifier? Init your cell with -initWithStyle:reuseIdentifier:, or set the identifier in IB.

Resources