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.
Related
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.
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.
I have an UICollectionViewController and my custom cells, and in my cellForRowAtIndexPath: method I set the cells based on indexPath.row.
But I am getting wrong results, this cell appears even after first position, and if you scroll back and forth, it pops up in random places. How do i fix that?
Here is the code:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
DVGCollectionViewCell *cell;
cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:#"Cell" forIndexPath:indexPath];
if (indexPath.row == 0)
{
cell.imageView.image = [UIImage imageNamed:#"something1.png"];
cell.buyLabel.text = #"170";
cell.textLabel.text = #"11.2011";
}
return cell;
}
Cell in both UITableView and UICollectionView are recycled, that means that when one goes off screen it is put in an NSSet until you need it again. When it's need it's removed from the set ad added again at UICollectionView views hierarchy. If you do not clean the value inside the cell or set them again, the cell will show the same data when it was created.
This is made for performance reason creating cell takes more time instead of value them again.
If your problem is in layout check the layout flow object, which size did you set?
I have found the problem, once the cell contents was set it was never cleaned. So I added cleaning every cell properties as additional clause and it works fine.
You can perform any clean up necessary to prepare the view for use again if you override prepareForReuse in your custom cell implementation.
One of the answers in this SO post helped: override prepareForReuse and reset the cell to its default state. Don't forget to call super.prepareForReuse.
I noticed that UICollectionView calls collectionView:cellForItemAtIndexPath: on its data source quite a few times. For example, on each layoutSubviews all cells are "reconfigured", no matter if they were already visible or not.
When collectionView:cellForItemAtIndexPath: is called, we're expected to:
Your implementation of this method is responsible for creating,
configuring, and returning the appropriate cell for the given item.
You do this by calling the
dequeueReusableCellWithReuseIdentifier:forIndexPath: method of the
collection view and passing the reuse identifier that corresponds to
the cell type you want. That method always returns a valid cell
object. Upon receiving the cell, you should set any properties that
correspond to the data of the corresponding item, perform any
additional needed configuration, and return the cell.
The problem is that configuring a cell is not always a cheap operation, and I don't see why I should reconfigure cells that are already configured.
How can we avoid redundant configuration of the cells? Or is there something I'm not understanding correctly?
I don't think you can avoid this. The problem is that UICollectionView is so general and FlowLayout isn't the only layout. Since you are allowed to make crazy layouts, any layout change, like a layoutsubviews, could completely change the layout you want -- a grid in portrait and a triangular arrangement in landscape... The only way to know what the layout should be is to find out the location and size of each cell.
UICollection view is not cheap for lots and lots of elements.
Simply after configuring a cell just use cell.tag = 1; , then next time you can avoid reconfiguring same cell by using if(!cell.tag).
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:YourCellReuseIdentifier forIndexPath:indexPath];
if(!cell){
//Create cell ...
}
if(!cell.tag){
//Do things you have to do only once
}
else{
//DO things you have to do every time.
}
return cell;
}
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.