https://medium.com/ios-os-x-development/perfect-smooth-scrolling-in-uitableviews-fd609d5275a5
Difficulty and the depth of material will increase from the beginning to the end of story, so I’ll start with things which are familiar to many of you. Deeper aspects of the iOS drawing system and UIKit will be covered at the end.
Built-In Tools
I really believe that most people reading this story know about this, but some people, even after using these tools, are not using it in the right way.
The first one is reusing single instance of cell/header/footer even if we need to show more. This is most obvious way to optimize UIScrollView (which is a parent of UITableView), just built by engineers at Apple. For correct use you should have only cell/header/footer classes, initialize their one time, and return to the UITableView reused instances.
I think that detailed review is not necessary since the workflow with reusing cells is described in documentation.
But very important thing is still there: tableView:cellForRowAtIndexPath:method, which should be implemented in the dataSource of UITableView, called for each cell and should work fast. So you must return reused cell instance as quickly as possible.
Don’t perform data binding at this point, because there’s no cell on screen yet. For this you can use tableView:willDisplayCell:forRowAtIndexPath:method which can be implemented in the delegate of UITableView. The method called exactly before showing cell in UITableView’s bounds.
The second one is not as difficult to understand, but there’s one thing which should be explained.
This tool absolutely doesn’t make sense with fixed row height of theUITableView, but if your context requires dynamic height of cells for some reason, you may get choppy scrolling very easily.
As we know, UITableView is just child of UIScrollView, which enables user to interact with areas whose real size is bigger then visible. AnyUIScrollView instance uses such things as contentSize, contentOffset and many others for displaying correct rect to user.
But what is wrong at UITableView? As has been explained, UITableViewdoesn’t hold all cell instances simultaneously. Instead, only required cells are shown to user.
So, how does UITableView knows about its contentSize? It just calculates this value by summing all cell heights.
The tableView: heightForRowAtIndexPath: method of the delegate ofUITableView is called for each cell (even if it isn’t displayed!), so you should return height values very fast.
Most people make huge mistakes by laying out initialized instance of cell with bound data to fetch their height after this. You should not use this way for calculating height of cell if you want to improve scrolling performance, ‘cause all this things are incredibly slow and 60 FPS which are standard for iOS devices will be dropped to 15–20, and your scrolling becomes laggy even with low speed.
How to calculate further cell height if we don’t have an instance of this cell? It is example of cell code, which using class method for returning height based on passed width and data for displaying (it’s adapter of cell):
And it’s used for returning value to the UITableView by this way:
How much pleasure do you get in implementing all of this? Most of you will say that it does not exist here. But I didn’t promise that it will be easy. Of course, we can build our own classes for manual layout and height calculation, but sometimes we haven’t enough time for this. You can find an example of this approach in Telegram’s iOS application code.
Since iOS 8, we can use automatic height calculation without implementing mentioned method at the delegate of UITableView. To achieve this you may use AutoLayout tool and rowHeight variable set to UITableViewAutomaticDimension. More detailed information can be found in great explanation on StackOverflow.
Despite the ability to use these tools, I strongly recommend you do not. And more?—?I recommend not using even complex math calculations on your way to define further height of a cell, only addition, subtraction, multiplication and division (if possible).
But what about AutoLayout? Is it really so slow as I was talking about? Probably you will be surprised but it’s truth. Incredibly slow if you want see perfect smooth scrolling in your app on all actual devices which have long lifecycle (especially compared to Android devices). And more subviews you have, less quickly AutoLayout works.
The reason of relative low performance of AutoLayout in constraint solving system named “Cassowary” which hidden under the hood. More subviews you have to layout and constraints you have to solve, more time you spend for returning cell to the UITableView.
What is faster: performing some base math calculations with small number of values, or solving systems of tons linear equalities and inequalities? And now imagine that user wants to scroll very fast, and for each cellAutoLayout performs all of this crazy calculations.
Correct way to use the built-in tools to optimize the UITableView:
- Reuse cell instances: for specific type of cell you should have only oneinstance, no more.
- Don’t bind data at cellForRowAtIndexPath: method ‘cause at this time cell is not displayed yet. Instead usetableView:willDisplayCell:forRowAtIndexPath: method in the delegate ofUITableView.
- Calculate further cell heights faster. It’s routine process for engineers, but you will be awarded for your patience by increased smooth scrolling on sets of complex cells.
We need to go deeper
Of course, mentioned points are not enough to implement really smooth scrolling, and especially it becoming noticeable when you have task to implement complex cell with lots of gradients, views, interactive elements, some decorative elements and more.
At this moment it easy to get laggy UITableView, even if all of above points are done. More views you have into UITableViewCell, more FPS reduction will be when scrolling. But now, with manual layout and optimized height calculation, problem is not layout, but is rendering.
Let’s switch attention to property of UIView named “opaque”. Documentation says that it helps drawing system to define is UIViewtransparent or not. If not?—?drawing system can make some optimizations when rendering this view and increase performance.
We needed performance, or it is not? Users may scroll tables very intensive, use scrollsToTop feature, and they not necessary have latest iPhone therefore cells are must render very quickly. More quickly than “usual” views.
One of slowest rendering operation is blending. It performs with support of device GPU since exactly this hardware was developed for blending (not only blending).
As you might guess, the approach to increase performance is reduction of number of blending operations. But before reduce something you should find it. Let’s try.
Run your app on iOS Simulator, highlight item “Debug” and then pick item “Color Blended Layers”. Since this point iOS Simulator will displaying all areas into two colors: green and red.
Green places are not have a blending but red places are.
As you see, there is at least two places in cell where blending has been performed, but you can not really see difference (and this blending operation is unnecessary!).
Each of this case should be researched closely, and in different cases you should use different ways to escape blending. In my case setting upbackgroundColor to non-transparent is all that I should do for achieving this.
But sometimes we have more complicated things. Look at this: we have a gradient, but blending is absent.
If you want to use CAGradientLayer to implement this, you’ll be disappointed: FPS will be reduced to 25–30 on iPhone 6 and fast scrolling will become impossible.
It’s happens exactly ‘cause we have blending contents of two different layers: CATextLayer of UILabel and our imagined CAGraidentLayer.
With correct utilizing CPU & GPU resources, they are loaded evently, FPS keeps about 60. It seems like this:
Problems begins when device needs to perform a lot of blending operations: GPU will be fully loaded, but CPU will keeps low loading and will be useless.
Most of engineers are faced with this problem at end of summer 2010, right after releasing iPhone 4. Then Apple had presented revolutionary Retina display and… completely ordinary GPU. However, it had enough power at common, but problem described above had become more frequent.
Echoes of this decision you can see at current iPhone 4 behaviour under iOS 7?—?there all applications became laggy, even most simple. Anyway, by applying all suggestions from this story you will be able to achieve 60 FPS even in this conditions, although with difficulty.
So what to do with that? In fact, solution is right here: let’s render using CPU! It will unload GPU what will enable it to perform blending at places where no another way. For example, at places where you have CALayers for performing some animations.
We can perform rendering at CPU by using CoreGraphics operations indrawRect: method of UIView by this way:
Is this code nice? Even I will say you that not really. Even more?—?by this way you perform undoing all cache optimizations implemented at some UIViews(in any cases they are unnecessary). But exactly this approach disables some of blending operations, unloads GPU and makes the UITableView more smoothly.
But remember: this increases render performance not ‘cause CPU is faster than GPU! It enables us to unload GPU by loading CPU for some rendering tasks ‘cause CPU will may not be loaded at 100% in many cases.
Key to optimize blending operations is balance of loading CPU & GPU.
Shortly about your actions on way to optimize drawing your data in theUITableView:
- Reduce areas where iOS performs useless blending: don’t use transparent backgrounds, check this out using iOS Simulator or Instruments; gradient will be done better without blending if you can do this.
- Perform code optimizations to achieve balance of loading CPU & GPU. You should clearly know which part of rendering must be done by GPU, and which one?—?by CPU for keeping balance.
- Write specific code for specific cell types.
Pixel hunting
Do you know how pixels look? I mean, how look physical pixels in screen? I sure you know but I will show you:
Different screens are different made, but there’s one common thing. In fact, each physical pixel made by three colored subpixels: red, green and blue.
Since this fact each pixel isn’t atomic unit, although it is true for application. Or still it isn’t?
Until iPhone 4 with Retina display was released any physical pixel could be described with integer point coordinates. Since Retina display times we have screen points instead of pixels in our Cocoa Touch environment, and they can be float.
In perfect world (which we try to build), screen points always are addressed into integer coordinates of physical pixels. But in real life it may be float, for example, line may starts from 0.25 by X. And from this moment iOS will perform subpixel rendering.
This technology does make sense when applied to specific type of content (text, for example). But it unnecessary when we drawing smooth line by design.
If all your smooth lines are rendered with subpixel rendering (which will not be visible, ‘cause your lines are smooth by design), you make iOS to do unnecessary job and you will get FPS decreasing.
How to get problems with unnecessary subpixel antialiasing? Most frequently happened cases are code-calculated views coordinates which becomes float or incorrect images assets where image sizes are not aligned to physical pixels of screen (for example, if you have image with size 60x61 for Retina display insted of 60x60).
As at previous time, before reducing something we should to find it. Run your application on iOS Simulator and pick menu item “Color Misaligned Images” at “Debug” menu.
At this time there’s two highlighted areas: magenta?—?areas where’s performing subpixel rendering and yellow?—?where’s image sizes that rendered are not aligned to area where they are.
How to find place in your code where it’s happens? I always use manual layout with partly custom drawing, so usually I find it without any problems. If you’re using Interface Builder, then I’m really sympathize you.
In common, to solve this you simply should use ceilf, floorf andCGRectIntegral for rounding your coordinates. That’s all!
By hunting results I would like to suggest you following:
- Perform rounding of all pixel-relating data: point coordinates, heights/widths of UIViews and many others.
- Track your graphical resources: images must be pixel-perfect, else when they will be rendered on Retina displays, it will be doing with unnecessary antialiasing.
- Periodically recheck your situation with this problem ‘cause it can changes very quickly.
Asynchronous UI
Probably it will be look little bit strange, but it’s very effective way to makeUITableView scrolling more smoothly if you know what you’re doing.
Now we will talk about things you should do, and after?—?about you may.
Each application with medium level and above necessarily uses cells with custom media content: text, images, animations, even videos sometimes.
And all this stuff is decorated: avatars are rounded, text has a hashtags, usernames, etc.
We are mindful for the need to returning cells as quickly as possible, and at this point we have some troubles: clipsToBounds is slow, images should be loaded from network, hashtags needs to be located at string, and many others.
Optimization seems like this: you should performing those operations which will not allow you to returning cells quickly if performed in main thread.
Load images at background, round their corners at the same place, and then assign processed image to UIImageView.
Display text at once, but locate hashtags in background and then refresh displayed text with attributed string.
Specific actions depends on specific content in your cell, but main idea it’s performing huge operations at background. It may not only be network code and you should use Instruments to find them all.
Remember about need to returning cells quickly.
Sometimes we have situations when all the techniques above do not help. When the GPU is still not done with its work (iPhone 4 + iOS 7), when there’s a lot of content in cells, when we should realize animations with support of CALayers ‘cause of it’s really hard to implement with drawRect:.
In this case we should render in background all the rest. Besides it very effective way for increasing FPS when user scrolls UITableView too fast.
For example look at Facebook application which doing exactly this. For detecting this you may scroll down enough and then tap on status bar. List instantly will scrolling up, so you can clearly see that cells are not rendered at this moment. If to be more precisely?—?can’t get in time.
You can do it by yourself ‘cause it’s simple enough. For this you should set value of drawsAsynchronously at CALayers to YES.
But we can check the necessity of this actions. Run the application in iOS Simulator, select item “Color Offscreen-Rendered” at “Debug” menu. Now all areas which are rendered in background wil be highlighted in yellow.
If you enabled this mode for some layer, but it hasn’t become highlighted, then this one is not slow enough.
For finding bottlenecks in face of CALayers and further reducing it you can use Time Profiler in XCode Instruments.
And here’s list of actions for implementation of asynchronous UI:
- Find render bottlenecks which don’t allow you to return cells very fast.
- Move operations to background thread and refresh displayed content on the main thread.
- Last resort is setting up your CALayers for asynchronous displaying mode (even if they are about simple text or images)?—?this will help you to increase FPS.
Results
I’ve tried to explain main ideas of iOS drawing system (without using of OpenGL ‘cause it’s more rare cases). Of course some of this seems blurred, but in fact there’s only directions where you should research your code to find all troubles with scroll performance.
In different cases there can be different ways to optimization but principlesare never changes.
And the key to achieve perfect smooth scrolling is the very specific code, which allows you to use all available power of iOS devices to make really smooth applications.
Subscribe, recommend the story, discuss and enjoy! Thanks for your time.