TableView performance optimization
Cell reuse and identity reuse
Reusing an identity name with the static modifier ensures that the identity is created only once, improving performance. Then call dequeueReusableCellWithIdentifier: method to get the Cell in the buffer pool. If there is no call initWithStyle: ReusIdentifier: method to create a new Cell. Note that you need to call in advance registerNib/registerClass method for TableView register reuse identifiers.
Highly dynamic
We need the agent that implements it to give the height:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// return xxx
}
Copy the code
After this proxy method is implemented, the rowHeight Settings above will become invalid. In this approach, we need to improve the cell’s high computational efficiency to save time.
Since iOS8, self-sizing cell has been introduced, which can calculate the height of the cell itself. Using self-sizing cell has three requirements:
(1) Use Autolayout for UI layout constraints (require all four edges of cell. ContentView to have constraint relationships with internal elements).
(2) Specify the default value of TableView’s estimatedRowHeight property.
(3) appointed a UITableViewAutomaticDimension TableView rowHeight attributes.
- (void) viewDidload {self. MyTableView. EstimatedRowHeight = 44.0; self.myTableView.rowHeight = UITableViewAutomaticDimension; }Copy the code
In addition to improving the efficiency of cell height calculation, we need to cache the already calculated height, and there is no need to do a second calculation for already calculated height.
Reduce the number of views
When we add system controls to the cell, in fact, the system will call the underlying interface for drawing. Adding a large number of controls will consume a lot of resources and affect the performance of rendering. Using the default UITableViewCell and adding controls to its ContentView can be very performance consuming. So the best thing for now is to inherit the UITableViewCell and rewrite the drawRect method.
The redraw operation is still done in the drawRect method, but Apple doesn’t recommend calling the drawRect method directly, and it won’t work if you force it to do so. Apple asks us to call setNeedsDisplay in UIView, and the program will automatically call drawRect to redraw. Calling setNeedsDisplay automatically calls drawRect.
Hide the layer with hidden
Avoid adding layers dynamically. When initializing the cell, pre-create all layers and use the Hidden property to show or hide the sublayers, as it is much faster to show them than to create them.
Avoid off-control rendering.
The price of opening a new buffer and switching context many times during rendering is very performance consuming. The following conditions result in off-control rendering:
1. shadows
The reason for this is that it needs to be displayed below all layer content and therefore must be rendered first. However, the ontology of the shadow (layer and its sub-layer) has not been combined together, so we can only apply for another piece of memory, draw the ontology content first, and then add the shadow to the frame buffer according to the shape of the rendered result, and finally draw the content. However, if we can tell the CoreAnmation (via the shadowPath property) the geometry of the shadow in advance, then of course the shadow can be rendered independently, without relying on the Layer body, so there is no need for off-screen rendering.
2. Layer with group opacity set to YES and opacity not 1
The condition for generating off-screen rendering is layer.opacity! = 1.0 and has a sublayer or background. Alpha is not applied to each layer separately, but only after the whole layer tree is drawn, alpha is added uniformly, and finally combined with the pixels of other layers below. Obviously, you can’t get the final result in one iteration.
3. Company masks
We know that mask is applied on top of layer and any combination of its sub-layers, and may have opacity. In fact, the principle of group opacity is similar to that of group opacity, which has to be completed in an off-screen rendering.
4. cornerRadius+clipsToBounds
Because the parent container has rounded corners, the child layer of the container will also need to be clipped. At this time, they are still queuing in the rendering queue and have not been combined on a canvas, so they cannot be clipped uniformly. They have to open another memory to operate. Setting a cornerRadius will not trigger an off-screen rendering.
5. ShouldRasterize
If the layer is not static, we update the rasterized layer, resulting in a lot of off-screen rendering. UITableViewCell, for example, redraws frequently because of reuse. If rasterization is set at this time, it will cause a lot of off-screen rendering and reduce performance. Do not overuse it, the system limits the cache Size to 2.5 * Screen Size. Running out of cache also results in a lot of off-screen rendering. Off-screen rendering of cached content has a time limit, and rasterized images (i.e. cached content) that are not used for more than 100ms are discarded and cannot be reused. (So rasterization can only be used if the image content stays the same, and only cache images that are used continuously: to avoid redrawing complex effects of static content, such as UIBlurEffect to avoid redrawing complex views nested within multiple views.)
Edge antialiasing (anti-aliasing)
Paging load data, request data asynchronously before – Prefetching API
Fetching network data in viewDidLoad to fetch some initialization data and then preloading data using UITableView’s Preloading API to achieve seamless fetching.
UITableViewDataSourcePrefetching agreement
// this protocol can provide information about cells before they are displayed on screen. @protocol UITableViewDataSourcePrefetching <NSObject> @required // indexPaths are ordered ascending by geometric distance from the table view - (void)tableView:(UITableView *)tableView prefetchRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths; @optional // indexPaths that previously were considered as candidates for pre-fetching, but were not actually used; may be a subset of the previous call to -tableView:prefetchRowsAtIndexPaths: - (void)tableView:(UITableView *)tableView cancelPrefetchingForRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths; @endCopy the code
The first function Prefetch the following IndexPaths based on the direction and speed of the current scroll, where we usually implement the logic of preloading data.
The second function is an optional method that you can use to cancel any pending data loading operations when the user is scrolling so fast that some cells are not visible, which helps improve scrolling performance, as I’ll discuss below.
The logical code to implement these two functions is:
The extension ViewController: UITableViewDataSourcePrefetching {/ / page request func tableView (_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { let needFetch = indexPaths.contains { $0.row >= viewModel.currentCount} if needFetch { // 1. Meet the conditions for page request indicatorView. StartAnimating () the viewModel. FetchImages ()} for indexPath in indexPaths {if let _ = viewModel.loadingOperations[indexPath] { return } if let dataloader = viewModel.loadImage(at: Indepath.row) {print(" prefetch the image on line \(indepath.row) ") // 2 Preheat the image that needs to be downloaded ViewModel. LoadingQueue. AddOperation (dataloader) / / 3 will be added to the record in the array to the download thread according to an index lookup viewModel. LoadingOperations [indexPath] = dataloader } } } func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]){// Cancel prefetch when it is no longer needed. Avoid resource waste indexPaths. ForEach {if let dataLoader = viewModel. LoadingOperations [$0] {print (" in the \ ($0. Row) cancelPrefetchingForRowsAt ") dataLoader.cancel() viewModel.loadingOperations.removeValue(forKey: $0) } } } }Copy the code
Finally, add two useful methods to complete the function:
/ / used to calculate the tableview loading new data need to reload the cell func visibleIndexPathsToReload (intersecting indexPaths: [IndexPath]) -> [IndexPath] { let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows ?? [] let indexPathsIntersection = Set(indexPathsForVisibleRows).intersection(indexPaths) return Array(indexPathsIntersection)} func isLoadingCell(for indexPath: IndexPath) -> Bool { return indexPath.row >= (viewModel.currentCount) }Copy the code
When you slide the TableView, load the content on demand
In some cases, we might quickly swipe through the list, where a large number of cell objects are created and reused, but in fact we might just scan up and down the page where the list stops, and the information we quickly swipe through is useless to us. At this point we can use the proxy method of ScrollView
scrollViewWillEndDragging: withVelocity: targetContentoffset:
To load content on demand.
#pragma mark - UIScrollViewDelegate // Load on demand - If the target line differs from the current line by more than the specified number of lines, only specify 3 lines before and after the target scroll range. - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { NSIndexPath *targetPath = [_myTableView indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)]; NSIndexPath *firstVisiblePath = [[_myTableView indexPathsForVisibleRows] firstObject]; NSInteger skipCount = 8; if (labs(firstVisiblePath.row - targetPath.row)> skipCount) { NSArray *temp = [_myTableView indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, _myTableView.frame.size.width, _myTableView.frame.size.height)]; NSMutableArray *arr = [NSMutableArray arrayWithArray:temp]; if (velocity.y<0) { NSIndexPath *indexPath = [temp lastObject]; if (indexPath.row+33) { [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-3 inSection:0]]; [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-2 inSection:0]]; [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:0]]; } } [_dataList addObjectsFromArray:arr]; }}Copy the code
TargetContentOffset is where TableView slows down to stop, and velocity represents the velocity vector.
How to avoid stuttering during scrolling: asynchronize UI and don’t block main thread
When you run into an application that is stuck scrolling, usually because a task is running for a long time and blocking the UI update on the main thread, the first step in making the main thread free to respond to such update events is to delegate time-consuming tasks to child threads to avoid blocking the main thread while fetching data.
Apple offers many ways to implement concurrency for applications, such as GCD, which I’m using here to asynchronously load images on cells. The code is as follows:
class DataLoadOperation: Operation { var image: UIImage? var loadingCompleteHandle: ((UIImage?) - > ())? private var _image: ImageModel private let cachedImages = NSCache<NSURL, UIImage>() init(_ image: ImageModel) { _image = image } public final func image(url: NSURL) -> UIImage? { return cachedImages.object(forKey: url) } override func main() { if isCancelled { return } guard let url = _image.url else { return } downloadImageFrom(url) { (image) in DispatchQueue.main.async { [weak self] in guard let ss = self else { return } if ss.isCancelled { return } ss.image = image ss.loadingCompleteHandle? (ss.image) } } } // Returns the cached image if available, otherwise asynchronously loads and caches it. func downloadImageFrom(_ url: NSURL, completeHandler: @escaping (UIImage?) -> ()) { // Check for a cached image. if let cachedImage = image(url: Url) {DispatchQueue. Main. Async {print (" cache hit ") completeHandler (cachedImage)} return} URLSession. Shared. DataTask (with: url as URL) { data, response, error in guard let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200, let mimeType = response? .mimeType, mimeType.hasPrefix("image"), let data = data, error == nil, let _image = UIImage(data: data) else { return } // Cache the image. self.cachedImages.setObject(_image, forKey: url, cost: data.count) completeHandler(_image) }.resume() } }Copy the code
In willDisplayCell: forRowAtIndexPath: proxy method in data binding
That specific how to use! Don’t worry, listen to me explain, I give you a small suggestion, everyone knows the UITableView instantiation of Cell method is: tableView: cellForRowAtIndexPath: This method needs to be called once for each Cell. It should execute quickly and return an instance of the reused Cell. Do not perform data binding here. There are no cells on the screen yet. We can in the tableView: willDisplayCell: forRowAtIndexPath: this method of data binding, this method is called before displaying the cell.
The implementation code to perform the download task for each Cell is as follows:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: "PreloadCellID") as? ProloadTableViewCell else { fatalError("Sorry, could not load cell") } if isLoadingCell(for: indexPath) { cell.updateUI(.none, orderNo: "\(indexPath.row)") } return cell } func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {// preheat image, Guard let cell = cell as? ProloadTableViewCell else {return} // updateCellClosure: (UIImage?) -> () = { [unowned self] (image) in cell.updateUI(image, orderNo: "\(indexPath.row)") viewModel.loadingOperations.removeValue(forKey: indexPath) } // 1. First of all determine whether already exists to create good download thread if let dataLoader = viewModel. LoadingOperations [indexPath] {if let image = dataLoader. Image {/ / 1.1 UpdateUI (image, orderNo: "\(indexpath.row)")} else {// 1.2 If the image has not been downloaded, ., wait for after the images are downloaded updates cell dataLoader loadingCompleteHandle = updateCellClosure}} else {/ / 2. Print (" create a new image download thread on line \(indexpath.row) ") if let dataloader = viewModel.loadimage (at: IndexPath. Row) {/ / 2.1 add images download after the callback dataloader. LoadingCompleteHandle = updateCellClosure / / 2.2 to start the download ViewModel. LoadingQueue. AddOperation (dataloader) / / 2.3 to download the thread into the records in the array to consult the viewModel. According to the index loadingOperations [indexPath] = dataloader } } }Copy the code
Asynchronously download preloaded images (to warm up) :
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { let needFetch = indexPaths.contains { $0.row >= viewModel.currentCount} if needFetch { // 1. Meet the conditions for page request indicatorView. StartAnimating () the viewModel. FetchImages ()} for indexPath in indexPaths {if let _ = viewModel.loadingOperations[indexPath] { return } if let dataloader = viewModel.loadImage(at: Indepath.row) {print(" prefetch the image on line \(indepath.row) ") // 2 Preheat the image that needs to be downloaded ViewModel. LoadingQueue. AddOperation (dataloader) / / 3 will be added to the record in the array to the download thread according to an index lookup viewModel. LoadingOperations [indexPath] = dataloader } } }Copy the code
When canceling Prefetch, cancel the task to avoid resource waste
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]){// Cancel prefetch when it is no longer needed. Avoid resource waste indexPaths. ForEach {if let dataLoader = viewModel. LoadingOperations [$0] {print (" in the \ ($0. Row) cancelPrefetchingForRowsAt ") dataLoader.cancel() viewModel.loadingOperations.removeValue(forKey: $0) } } }Copy the code
Reference links:
IOS handles web data gracefully. Do you really? Why don’t you read this one.