I have a scrollview loaded with lots of subviews, and for reasons I won't go into, I opted against cell re-use and a UITableView. It works well, save for a little initial main thread lag when the view is populated. However, every time a VC "on top" is dismissed (e.g. after presenting a modal overtop), there is a significant delay (2-3 seconds). I ran it through the profiler and the stack trace delves very deeply into IOS / Autolayout, as seen in the attached image.
The code seems to go into
NSLayoutConstraint("+0x06 calll "-[NSLayoutConstraint _addToEngine:]+0x0b").
Is there anything I can do to bypass this apparent re-application of constraints?
Why your scroll view's performance is poor
From looking at your trace, the root of your problem begins when transitioning back: you spend 80%+ of your time in -[UITransitionView transition:fromView:toView:removeFromView:]. This method immediately adds the view you transition to back into the view hierarchy (the -addSubview: call following), which necessitates a full Auto Layout pass.
I'm not aware of any way to force the normal iOS view controller transition system into keeping your old view around in the background, so to speak, so as to avoid this Auto Layout pass. (Such an approach might actually run into its own set of problems, if multiple views stack up offscreen and start to cause memory pressure.)
Speeding up Auto Layout in your existing structure
Despite the above, you can try to optimize your Auto Layout pass in a couple ways. First, rework your scroll view to have fewer subviews. Apple's developer tech support has also sometimes recommended flattening your view hierarchy; causing Auto Layout to do less recursion can sometimes be helpful. (See, for example, the performance tests performed by Florian Kugler and Martin Pilkington.)
You should also always ensure that a constraint is installed on the deepest possible view within your hierarchy, while still being on the common ancestor of the two views it relates to. For example, if view A has subviews B and C, you would want a constraint referencing only view C installed on view C, not view A; however, a constraint relating B and C would need to exist on A. This is a good guideline for all Auto Layout work, not just in this particular case; the deeper a constraint is installed in the hierarchy, the faster the underlying solver can work at any particular level.
Switching your approach
UITableView is able to avoid this problem by providing a sort of break in the Auto Layout system: the table view can position properly with constraints, then use its own delegate methods (such as -tableView:heightForRowAtIndexPath:) and some internal layout magic to run Auto Layout individually for each cell, without requiring the entire view to be laid out all at once. Regular UIScrollViews do not have the luxury of this optimization.
I know you said you have reasons for not adopting a UITableView here, but you might consider revisiting those reasons. You can see some very significant speed improvements from switching to UITableView and implementing the delegate method -tableView:estimatedHeightForRowAtIndexPath:, which lets you short-circuit a lot of Auto Layout calculations at view load time and defer them until the point you're about to scroll a new cell onscreen. There has been some fairly significant experimentation done in this direction, both here on StackOverflow and by others in the community. (A quick search for "UITableView Auto Layout height" will turn up dozens of results.)
Good luck!
Related
I'm seeing some performance degradation in my application after some time and I'm trying to figure out what's going exactly.
I have a complex view controller (VC1) which contains scroll view, few table views inside, some custom cells with horizontal scrolling and custom drawing etc.
After several (around 10) refreshes of all these objects (reloading tables, reposition subviews etc) when I try to call presentViewController to push another view controller above VC1 I can see about 2 seconds delay between viewWillDisappear and viewDidDissapear
I tried to profile the app to see if there are memory leaks but couldn't find any. Memory usage grows when view refreshes and switches between different modes, but then it become more or less stable in around 30m.
Works fine in Simulator, but visible slower on iPhone5. And this slowness is visible only when I try to switch from that view controller.
I ran a profiler and recorded where these 2 seconds are spent. Here is link to trace file: https://dl.dropboxusercontent.com/u/6402890/trace.trace.zip
Majority of the time spent by UIKit doing layout as I can see.
What can I do to optimize it? Is there way to take may be a snapshot of a view and use it for "leaving view" animation and restore view hierarchy when we're coming back?
UPDATE: Adding screenshot for the profiler (click for full resolution):
UPDATE2:
After analyzing output from recursiveDescription I can see the following:
In the easiest case I have ~200 lines in the output. And performance is ok.
When I switch to more complex scenario hierarchy of views growth to ~500 lines, but still performs ok.
After multiple refreshes this number goes to ~2000 and this is where it become slow. Analyzing output with 2000 views I can see that ~1500 of them belong to hidden cells that are not even displayed in this mode anymore. When I'm refreshing table views cell types change too, and I'm utilizing different cells, but why the cells that are not used anymore are still being in subviews of table views?
Any recommendations?
From your stack, I suspect you've added a large number of views you didn't mean to add. Since it's related to reloads, I would check your reload logic and make sure it doesn't re-add all the views in your hierarchy without removing the previous views. You can write a quick debug routine use -recursiveDescription to recursively walk the -subviews of each view and print them out to see what's in the hierarchy.
It's possible that your issue is in the layer hierarchy rather than the view hierarchy, but the symptoms you describe make me think views.
EDIT: From your update, you probably have one of two things going on. Most likely, if these are actual UITableViewCells that shouldn't even exist anymore, then you have a retain loop somewhere. Alternately, your cellForRowAtIndexPath: may be incorrect and may be adding new views to an existing cell when it should just be reconfiguring the cell.
In either case, though, 200 views seems a lot of views for a "best case." You may be overusing views in places that you should be doing custom drawing. If the performance is ok, then… ok, but I'd test carefully on your slowest supported devices.
From the Instruments Time Profiler output, you can see that NSISEngine is eating up a ton of CPU. That class is responsible for doing the Auto Layout constraint evaluation and layout calculation.
So it looks like you are using Auto Layout, at least for some of the views.
Are you by any chance removing and re-adding constraints anywhere at runtime? I've seen this exact problem caused by that (can explain more if this is relevant).
If you aren't removing constraints, it sounds like you might have a fairly complex view hierarchy, and if you're using Auto Layout throughout, it's likely that you have a lot of constraints. As you may know, Auto Layout degrades in performance pretty quickly above a certain point due to super-linear time complexity of solving constraints. Check the output of po [[UIWindow keyWindow] recursiveDescription] from the debugger to see what your view hierarchy looks like.
I'm not sure what your view controller transition looks like, but you could try removing the disappearing view controller's view from its superview before doing the present. That should prevent it from doing layout calculations as it transitions. If that solves the performance issue, you could quickly snapshot the view hierarchy and then replace it with a single new UIImageView of the snapshot to display during the transition animation.
(One final thing: are any of your table views using Auto Layout in their cells? do any of these table views have more than ~20 cells?)
When presenting another controller your original controller has to be animated out of the window, which causes the view's frame to change and probably triggers all layoutSubviews methods and your manual adjustments.
You could try to avoid this by deactivating autoresizesSubviews in viewWillDisappear.
It is not very clean, but then probably all the calculations you are making may not be as well!
Try to optimize them:
Don't call layoutSubviews directly and call setNeedsLayout only if really needed.
Try to replace your manual resizing code with autoresizingMask or autoLayout.
Adjust views lazily and only if they are visible and if their size and not origin really has changed.
Don't reload your tables entirely but try to change only individual rows.
Try to use only a single table view.
Make sure to reuse cells.
The problem may have something to do with that you are placing UITableView instances inside a UIScrollView. That is explicitly prohibited by Apple's documentation for UIWebView (surprise!):
Important: You should not embed UIWebView or UITableView objects in UIScrollView objects. If you do so, unexpected behaviour can result because touch events for the two objects can be mixed up and wrongly handled.
I suspect that may also mess up table view cell reuse mechanism. Anyway, I'd also recommend to check if you are not 'leaking' any views at all. Keep in mind that even invisible views participate in layout if they are in the view hierarchy.
Edit: in response to Update 2
It's evident that cell reuse mechanism is not functioning properly. Try to make sure you are using correct instances of UITableView when dequeueing table view cells from reuse queue (check your data sources).
I have to implement a view controller (on iPhone, portrait only, full screen view) where the upper part of the view must have an horinzontal, paged scrolling behavior, potentially infinite.
I already used for similar purposes UIPageViewControllers, to take advantage of the datasource and delegate protocols, which are very helpul for manage memory and other stuff (keeping only 3 view controllers in memory, providing delegates to handle actions exactly when a transition is done and so on): so I think that in this case too this component is the best choice.
But here comes my problem. In the view I'm realizing, I have to let the user understand that he can swipe left and right to move to another view: a page control is not a good choice, since the scroll could be potentially infinite, so I would like to let a small portion of the views of the left and right view controllers to be visible.
Something like that:
link to the image (sorry I cannot include images in my posts yet)
Up to now I have not been able to figure out how to realize this. In the options during initialization, UIPageViewControllerOptionSpineLocationKey can be specified to set (from documentation) "Space between pages, in points": but this seems to work only with positive value, so that the space increases, while it ignores negative values that could reduce the space.
I hope there might be a solution using page view controllers, since at the same time I need to refresh a table view in the lower part of the screen when a transition is complete, and the delegate method of page controllers is ideal for this aim.
Maybe a collection view can be an alternate solution, but it is more complicated and I'm not sure how to obtain a behavior like the one I described to refresh the table view.
If needed I can attach some code and a screenshot of the prototype
Ok, I understand that this is not possible and why it is.
The datasource methods load, when needed, the view controllers that are before and after the current one. Making these view controllers' views always visible, as I desired, will require that the datasource loads more than one view controllers after (or before, depends on the direction of scrolling) the current one, to be ready for the pan actions (potentially, before the animation is ended by the user lifting up its finger, two view controllers "after" or "before" could become visible it my desired configuration), and this is not intended by UIPageViewController on iPhone, especially in portrait mode.
So actually, the only way to achieve that more than one view is visible in an horizontal-scrolling component at any time, is to implement a UIScrollview with horizontal paging and calculate the contentSize and other sizes accordingly.
I'm wondering which is the better way to implement a view which is designed to have complicated subview hierarchy, say a view with one sub view on left and one sub view on right, the left subview has X number of sub-subviews in one column, the right subview has Y number of sub-subviews in a row.
(X and Y varies)
Two ways to implement it:
Custom left and right views (ie. UIView subclass), custom left sub-subview, custom right sub-subviews, the root view only deals with custom left and right views, and they configure their sub-subviews
Only one view with a column of views (ie left view's sub-subview) on left and a row of views (ie right view's sub-subview) on right
First approach:
pros:
clean hierarchy means better maintainability.
responsibilities distributed over subviews, so less complication in each view
cons:
nested subviews hierarchy
may have worse performance due to auto-layout
delegation chain is more difficult, consider each subview as a button which need to perform certain action, the custom view need to delegate the action all the way back to root view
Second approach:
pros:
less subviews
may have better performance
easy delegation chain compared to 1st approach
cons:
hard to maintain / modify, as all subviews are in one level, especially with auto-layout
messy code base since all views are in one base view
Looks like 1st approach is better, but it still has several cons, is there a completely new way to implement it which copes with all the cons?
Both subviews (left column and right column) must have something in common, otherwise you would not be showing them at the same time. Because they have a general relation I would have no issue going with option 2. With that said, I would still prefer option 1 for the following reasons:
A clean hierarchy is easier to understand and maintain.
View logic that is distributed is easier to understand and will keep your classes smaller (which also means it will be easier to reuse sub views elsewhere).
"May have worse performance" is a big MAYBE. You should take actual measurements with Instruments or by using NSDate and timeIntervalSinceNow. As long as the constraints are always installed at the nearest common ancestor you should be fine.
Delegation and target/action won't be so bad self.firstView.subView.button.target = self.
Acceptable performance on all supported hardware should be the primary deciding factor. Maintainability should be a close second.
Go with the first approach.
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:
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.
I feel that it should be able to handle ~100 simple opaque cells at a time without the jitter. Am I wrong?
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!
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:
Each cell is opaque.
Cell content clips to bounds.
Cell coordinate positions do not contain fractional values (e.g. always calculate to be whole pixels)
Try to avoid overlapping cells.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
Greetings! I'm trying to borrow the view flip concept from Apple's TheElements sample app. This sample employs a container UIView in which you can swap between two subviews. The flip is achieved using setAnimationTransition:forView:cache: and removing/adding each subview.
In general, the flip works and I can swap between my two views (a UITableView with headers/footers, and a MKMapView). However, various (consistently chosen) areas of my table view are obliterated (using the table view's background color) before and after the table view is flipped, and I don't understand why.
I can't find anything unusual about the table view in terms of drawing. In the case of the sample app, the view is drawn from scratch, but I would hope that doesn't factor in to it! I'm really hoping it's something simple - maybe a UITableView property setting?
Clues appreciated. Thanks!
Update: When I slow down the animation, I begin to get an understanding of what is being disturbed (though I still don't know why). My table header view contains an image view and label view. Now, imagine the CGRects for those two views (only without any visible content, just a background color) being redrawn further down the table view, right over the table rows.
This also happens with another chunk of real estate that appears to come from a table view cell that was set with a custom height (to size the text within it).
I dropped a gratuitous number of breakpoints throughout my code (where things are sized, created and whatnot), and not one of them is hit during the transition.
I even tried placing my table view inside a UIView and targeting that for the transition instead. No difference.
From the API docs:
Caching can improve performance but if
you set this parameter to YES, you
must not update the view or its
subviews during the transition.
Updating the view and its subviews may
interfere with the caching behaviors
and cause the view contents to be
rendered incorrectly (or in the wrong
location) during the animation. You
must wait until the transition ends to
update the view.
That's all well and good, but I'm not updating any views or subviews during the transition. (At least not on purpose! Again, see the note about the breakpoints. Nothing got hit.)
Amazing.
Answer: In this case, it appears to only happen in ... the Simulator! On the device, it's fine.
I was thinking that such a run-of-the-mill transition would be the sort of thing that would render identically on the Simulator and the device.
I was wrong. So if you ever see animation transition glitches on the simulator, hang in there. It might be fine after all.