53

I'm working on a custom UICollectionViewLayout that displays cells organized by day/week/month.

It is not scrolling smooth, and it looks like the lag is caused by [UICollectionView _updateVisibleCellsNow] being called on each rendering loop.

Performance is OK for < 30 items, but at around 100 or more, its terribly slow. Is this a limitation of UICollectionView and custom layouts, or am I not giving the view enough information to perform correctly?

Source here: https://github.com/oskarhagberg/calendarcollection

Layout: https://github.com/oskarhagberg/calendarcollection/blob/master/CalendarHeatMap/OHCalendarWeekLayout.m

Data source and delegate: https://github.com/oskarhagberg/calendarcollection/blob/master/CalendarHeatMap/OHCalendarView.m

Time Profile: Time profile - Custom layout

Update

Maybe its futile? Some testing with a plain UICollectionViewController with a UICollectionViewFlowLayout that is given approximately the same amount of cells/screen results in a similar time profile.

Time profile - Standard flow layout

I feel that it should be able to handle ~100 simple opaque cells at a time without the jitter. Am I wrong?

Oskar
  • 1,047
  • 2
  • 12
  • 19
  • No you're not wrong. My tests with a vanilla UICollectionView object using Flowlayout and simple array of statically allocated cells containing image view objects, show you can easily reach 100 cells without performance problems. With a vanilla set-up on an iPad 3, at around 700+ cells expect performance problems to set-in. – TheBasicMind May 08 '13 at 09:58
  • 1
    In my case, the NSDate related functions take too much time... – gdm Sep 13 '13 at 19:34
  • Can you explain how you created your vanilla cell collection view? On iPad 3, cells only contain 1 image (same for all cells), even with 50 cells it's not completely smooth, and completely unusable past 150 cells. Also, when I say 150 cells, I mean visible at once on the screen, not in the entire data set. With 30 cells on screen I can easily keep it smooth even with huge datasets (100K+). – Fabien Warniez Dec 11 '13 at 20:19

10 Answers10

93

Also don't forget to try to rasterize the layer of the cell:

cell.layer.shouldRasterize = YES;
cell.layer.rasterizationScale = [UIScreen mainScreen].scale;

I had 10 fps without that, and boom 55fps with! I'm not really familiar with GPU and compositing model, so what does it do exactly is not entirely clear to me, but basically it flatten the rendering of all subviews in only one bitmap (instead of one bitmap per subview?). Anyway I don't know if it has some cons, but it is dramatically faster!

Altimac
  • 1,332
  • 7
  • 10
  • i needed to add this when using iOS7, wasnt required for ios6, but my FPS was choppy (50), now 59 and smooth as silk! – DogCoffee Sep 12 '13 at 11:23
  • Additionally, I need to call [cell layoutSubviews] in cellForItemAtIndexPath after above code. – Itachi May 10 '16 at 02:14
  • The cons among others is that everything resides in memory instead. This should be benchmarked and can differ for different use-cases. As mentioned here : https://stackoverflow.com/questions/11521959/uiview-self-layer-shouldrasterize-yes-and-performance-issues –  Aug 22 '17 at 22:04
18

I have been doing considerable investigation of UICollectionView performance. The conclusion is simple. Performance for a large number of cells is poor.



EDIT: Apologies, just re-read your post, the number of cells you have should be OK (see the rest of my comment), so cell complexity may also be a problem.

If your design supports it check:

  1. Each cell is opaque.

  2. Cell content clips to bounds.

  3. Cell coordinate positions do not contain fractional values (e.g. always calculate to be whole pixels)

  4. Try to avoid overlapping cells.

  5. Try to avoid drop shadows.



The reason for this is actually quite simple. Many people don't understand this, but it is worth understanding: UIScrollViews do not employ Core Animation to scroll. My naive belief was that they involved some secret scrolling animation "sauce" and simply requested occasional updates from delegates every now and then to check status. But in fact scroll views are objects which don't directly apply any drawing behaviour at all. All they really are is a class which applies a mathematical function abstracting the coordinate placement of the UIViews they contain, so the Views coordinates are treated as relative to an abstract contentView plane rather than relative to the origin of the containing view. A scroll view will update the position of the abstract scrolling plane in accord with user input (e.g. swiping) and of course there is a physics algorithm as well which gives "momentumn" to the translated coordinate positions.

Now if you were to produce your own collection view layout object, in theory, you could produce one which 100% reverses the mathematical translation applied by the underlying scrollview. This would be interesting but useless, because it would then appear that the cells are not moving at all as you swipe. But I raise this possibility because it illustrates that the collection view layout object working with the collection view object itself does a very similar operation to the underlying scrollview. E.g. it simply provides an opportunity to apply an additional mathematical frame by frame translation of the attributes of the views to be displayed, and in the main this will be a translation simply of position attributes.

It is only when new cells are inserted or deleted moved or reloaded that CoreAnimation is involved at all; most usually by calling:

- (void)performBatchUpdates:(void (^)(void))updates
                 completion:(void (^)(BOOL finished))completion

UICollectionView requests cell layoutAttributes for each frame of scrolling and each visible view is laid out for each frame. UIView's are rich objects optimised for flexibility more than performance. Every time one is laid out, there are a number of tests the system does to check it's alpha, zIndex, subViews, clipping attributes etc. The list is long. These checks and any resulting changes to the view are being conducted for each collection view item for each frame.

To ensure good performance all frame by frame operations need to be completed within 17ms. [With the number of cells you have, that is simply not going to happen] bracketed this clause because I have re-read your post and I realise I had misread it. With the number of cells you have, there should not be a performance problem. I have personally found with a simplified test with vanilla cells, containing only a single cached image, the limit tested on an iPad 3 is about 784 onscreen cells before performance starts to drop below 50fps.

In practice you will need to keep it less than this.

Personally I'm using my own custom layout object and need higher performance than UICollectionView provides. Unfortunately I didn't run the simple test above until some way down the development path and I realised there are performance problems. I'm so I'm going to be reworking the open source back-port of UICollectionView, PSTCollectionView. I think there is a workaround that can be implemented so, just for general scrolling about, each cell item's layer is written using an operation which circumvents the layout of each UIView cell. This will work for me since I have my own layout object, and I know when layout is required and I have a neat trick that will allow the PSTCollectionView to fall back to its normal mode of operation at this time. I've checked the call sequence and it doesn't appear to be too complex, nor does it appear at all unfeasible. But for sure it is non-trivial and some further tests have to be done before I can confirm it will work.

Pang
  • 9,564
  • 146
  • 81
  • 122
TheBasicMind
  • 3,585
  • 1
  • 18
  • 20
  • My performance issues are on an iPhone5. Perhaps the iPad 3 can cram in way more cells/screen. You mention that the `UICollectionView` requests `layoutAttributes` for each frame, but in my case I find its not so. It requests an array of attributes in batches with a rect the size of the screen only a few times when scrolling. So I figure it should be able to lay out the cells on the scroll views content view, and then simply adjust the offset (exactly like you are saying). But even if I just nudge the view up and down a few pixels (no attributes requested) its still laggy. – Oskar May 09 '13 at 13:43
  • Interesting that you mention `PSTCollectionView` because thats what I'm afraid I'll have to fall back on as well. Feels like a huge undertaking though. Either that or grouping some `UIViews` together into fewer `UICollectionViewCell`s. Still the same total amount of `UIView`/screen, but way less cells. But with that solution I loose the ability to morph between layouts and do batch updates, and that would be a bummer. Or simply live with the lag, and hope that they improve it in iOS7. – Oskar May 09 '13 at 13:47
  • Re: PST Collection view, improving scrolling performance is certainly non-trivial and a much bigger undertaking if you have to build your own layout object to do it. However I have already done that (because I needed spreadsheet like x,y axis scrolling), so with a layout object in hand, potentially though it's non-trivial it's equally a quite nucleated surgical modification that's required. We shall see! – TheBasicMind May 11 '13 at 10:15
  • Removing shadow Color, Radius, offSet and Opacity from the siteCell layer seemed to improve scrolling performance on my end. word. – topwik Jul 24 '13 at 15:37
8

Some more observations that might be helpful:

I am able to reproduce the problem, using flow layout with around 175 items visible at once: it scrolls smoothly in the simulator but lags like hell on iPhone 5. Made sure they are opaque etc.

enter image description here

What ends up taking the most time seems to be work with a mutable NSDictionary inside _updateVisibleCellsNow. Both copying the dictionary, but also looking up items by key. The keys seems to be UICollectionViewItemKey and the [UICollectionViewItemKey isEqual:] method is the most time consuming method of all. UICollectionViewItemKey contains at least type, identifier and indexPath properties, and the contained property indexPath comparison [NSIndexPath isEqual:] takes the most time.

From that I'm guessing that the hash function of UICollectionViewItemKey might be lacking since isEqual: is called so often during dictionary lookup. Many of the items might be ending up with the same hash (or in the same hash bucket, not sure how NSDictionary works).

For some reason it is faster with all items in 1 section, compared to many sections with 7 items in each. Probably because it spends so much time in NSIndexPath isEqual and that is faster if the row diffs first, or perhaps that UICollectionViewItemKey gets a better hash.

Honestly it feels really weird that UICollectionView does that heavy dictionary work every scroll frame, as mentioned before each frame update needs to be <16ms to avoid lag. I wonder if that many dictionary lookups either is:

  • Really necessary for general UICollectionView operation
  • There to support some edge case rarely used and could be turned off for most layouts
  • Some unoptimized internal code that hasn't been fixed yet
  • Some mistake from our side

Hopefully we will see some improvement this summer during WWDC, or if someone else here can figure out how to fix it.

Andreas Karlsson
  • 283
  • 1
  • 2
  • 7
  • Great observations. Perhaps this is something to file a radar on. – Oskar May 09 '13 at 17:04
  • When you think about it, a dictionary lookup is required because, unlike a table view, there is no guaranteed order to the display of the cells. You can define the cells to follow any rules at any time and the collection view needs to know for any given frame which cells are on screen and where they should be placed (even if cells are visible from sections 1, 2 and 3, there is still no guarantee all cells from section 2 are on screen). This becomes very clear when you roll your own layout. An array won't do because the required index paths will often be discontinuous. – TheBasicMind May 10 '13 at 13:14
  • Andreas are you rolling your own Layout object? I too found NSDictionary performance issues but *i think* this was relieved somewhat when I started caching any generated layoutAttributes objects. I've noticed bad things happen if these are not cached so the exact same attributes object is returned for a given IndexPath when future requests for layout objects are made. I say *think* because unfortunately I didn't notice the improvement in that metric until some time later and haven't yet run specific tests to confirm. – TheBasicMind May 10 '13 at 13:18
  • Interesting thought, but then I think it leans towards unoptimized internal code. The collection view asks the layout for a full screen of `layoutAttributes` at a time and then saves them internally. There are no dataSource or delegate calls when scrolling that could cause the lag, only internal work. – Andreas Karlsson May 11 '13 at 09:07
  • When scrolling it should only have to lookup items which are in the newly visible rect, and which ones are only in the newly hidden rect. This should be able to do be done very efficiently with proper spatial data structures. Maybe not when considering everything the collectionview can do, but for a regular 2d grid with center, size and rotation it should be easy with AABB or something. I used the default flow layout btw. – Andreas Karlsson May 11 '13 at 09:08
  • In practice, when you roll your own layout, the requested rect is always the size of the scrollview. Thinking about it, it has too be. When you roll your own layout object you have the opportunity to adjust the positioning of any cells/items at you see fit. You might want to crunch all your items together towards the centre of the layout as you scroll right, or implement some other display behaviour. Apple don't know this in advance, so have to ask for all attributes for visible cells in the rect. The Apple flow layout optimises this process and only asks for the new "revealed" rect. – TheBasicMind May 11 '13 at 09:59
  • The collectionview asks for a full screen of content at a time, but if you scroll back to already "seen" areas it doesn't ask again. And it doesn't ask every frame that you scroll, just when a new screenful of content is about to become visible, it asks once. It then keeps all layout attributes internally of "already seen" area space. It is this internal data store and lookup that seems to be broken/inefficient. If you want it to ask the layout for attributes every frame I guess you have to invalidate the layout constantly, which would be even more laggy. – Andreas Karlsson May 11 '13 at 13:40
7

Here is Altimac's answer converted to Swift 3:

cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = UIScreen.main.scale

Also, it should be noted that this code goes in your collectionView delegate method for cellForItemAtIndexPath.

One more tip - to see an app's frames per second (FPS), open up Core Animation under Instruments (see screenshot).

enter image description here

Pang
  • 9,564
  • 146
  • 81
  • 122
Gene Loparco
  • 2,157
  • 23
  • 23
  • 1
    Should be UIScreen.mainScreen().scale instead of UIScreen.main.scale. – Mat0 Sep 06 '16 at 14:29
  • 3
    Mat0 what version of Swift are you using? With Swift 3, UIScreen.main.scale is the way to go... UIScreen.mainScreen().scale is now "old school" pre-Swift 3! ;-) – Gene Loparco Sep 14 '16 at 19:06
6

The issue isn't the number of cells you're displaying in the collection view total, it's the number of cells that are on screen at once. Since the cell size is very small (22x22), you have 154 cells visible on screen at once. Rendering each of these is what's slowing your interface down. You can prove this by increasing the cell size in your Storyboard and re-running the app.

Unfortunately, there's not much you can do. I'd recommend mitigating the problem by avoiding clipping to bounds and trying not to implement drawRect:, since it's slow.

Ash Furrow
  • 12,391
  • 3
  • 57
  • 92
  • 1
    It was cells/screen I meant. Should have clarified :). And I've also had cells that are simple cells without children (and no drawRect), same issue there. From the profile it looks like the collection view performs a copy of all (visible) attributes on each render loop. That's where CPU time is spent, not drawing. – Oskar May 06 '13 at 15:20
  • 1
    The drawRect hint helped me with my issue. – Fostah Jun 14 '13 at 20:06
6

Big thumbs up to the two answers above! Here's one additional thing you can try: I've had big improvements in UICollectionView performance by disabling auto layout. While you will have to add some additional code to layout the cell interiors, custom code seems to be tremendously faster than auto layout.

NSSplendid
  • 1,957
  • 1
  • 13
  • 14
  • Unfortunately disabling auto layout doesn't help in my case. Same performance both for on and off. I turned it of on Storyboard level. Do you know if its possible to perhaps only turn it of for the cells? They are simple and doesn't require any fancy layout. – Oskar May 09 '13 at 13:30
  • I've never tied that, but this might help: http://stackoverflow.com/a/15242064/319019 – NSSplendid May 10 '13 at 09:48
5

Beside the listed answers (rasterize, auto-layout, etc.), you may also want to check for other reasons that potentially drags down the performance.

In my case, each of my UICollectionViewCell contains another UICollectionView (with about 25 cells each). When each of the cell is loading, I call the inner UICollectionView.reloadData(), which significantly drags down the performance.

Then I put the reloadData inside the main UI queue, the issue is gone:

DispatchQueue.main.async {
    self.innerCollectionView.reloadData()
}

Carefully looking into reasons like these might help as well.

Pang
  • 9,564
  • 146
  • 81
  • 122
RainCast
  • 4,134
  • 7
  • 33
  • 47
4

In few cases it is due to Auto-layout in UICollectionViewCell. Turn it off (if you can live without it) and scrolling will become butter smooth :) It's an iOS issue, which they havnt resolved from ages.

cirronimbo
  • 909
  • 9
  • 19
  • 1
    Thank you man! You saved my day!!!! Disabling auto-layout in cell xib file gives me 60fps instead of 5-10 on iPhone 4S iOS 8.1.4 in custom keyboard-extension. – imike Sep 08 '15 at 18:16
  • This helped a lot on my choppy iPhone 4S. For those doing this, you can disable auto-layout for only the View Controller with the UICollectionView by putting the controller in it's own storyboard. To easily put it in it's own storyboard use "Editor->Refactor to Storyboard..." in Interface Builder. – Vegard Jun 17 '16 at 09:30
3

If you are implementing a grid layout you can work around this by using a single UICollectionViewCell for each row and add nested UIView's to the cell. I actually subclassed UICollectionViewCell and UICollectionReusableView and overrode the prepareForReuse method to remove all of the subviews. In collectionView:cellForItemAtIndexPath: I add in all of the subviews that originally were cells setting their frame to the x coordinate used in the original implementation, but adjusting it's y coordinate to be inside the cell. Using this method I was able to still use some of the niceties of the UICollectionView such as targetContentOffsetForProposedContentOffset:withScrollingVelocity: to align nicely on the top and left sides of a cell. I went from getting 4-6 FPS to a smooth 60 FPS.

CWitty
  • 4,488
  • 3
  • 23
  • 40
0

Thought I would quickly give my solution, as I faced a very similar issue - image-based UICollectionView.

In the project I was working in, I was fetching images via network, caching it locally on device, and then re-loading the cached image during scrolling.

My flaw was that I wasn't loading cached images in a background thread.

Once I did put my [UIImage imageWithContentsOfFile:imageLocation]; into a background thread (and then applied it to my imageView via my main thread), my FPS and scrolling was a whole lot better.

If you haven't tried it yet, definitely give a go.

roycable
  • 301
  • 1
  • 9