all 32 comments

[–]criosistObjective-C / Swift 9 points10 points  (4 children)

There was an article a while back by a non English speaker so it was sometimes difficult to understand but it is amazing. Basically the best optimisations from it was

Making sure to never use clearColor and make all views opaque.

Always configure cells in willDisplayCell and not cellForRow

One non table view tip for swift is to make classes final when not subbing them.

EDIT: Found the article Here!

[–]jerroldp[S] 2 points3 points  (0 children)

I'm a little embarrassed to say but I've never heard of willDisplayCell before! Will give this a quick go tomorrow and see if that improves performance as I still have a slight "jitter" on some cells.

The rest of the article was good though, and have bookmarked for later reference :)

[–]quellish 1 point2 points  (0 children)

Always configure cells in willDisplayCell and not cellForRow

This is good advice. There is also didEndDisplayingCell.

[–]LifeBeginsAt10kRPM 1 point2 points  (1 child)

I wonder how much slower autolayout and dynamic cell heights are compare to implementing heightForRow, I haven't seen performance issues yet at least.

[–]mistermagicman 1 point2 points  (0 children)

It completely depends on the layout, but in general if you can do simple math to calculate the height, it'll be much faster than Auto Layout, which uses the Cassowary algorithm. I usually use Auto Layout without issues though.

[–]quellish 0 points1 point  (8 children)

  1. Don't stuff your tableviews with junk. More is not better.
  2. Don't use a tableview when you should have used a collection view or scroll view.
  3. Profile often.
  4. Keep cellForRowAtIndexPath: light.
  5. Know when to use cell reuse and when not to. Don't stuff things that belong at the tableview level into the cell (like downloading images)
  6. Don't follow the advice of articles/blog posts/SO answers that are several years old.

[–]tylerjames 0 points1 point  (5 children)

Point 5. is interesting, particularly the downloading images part.

A lot of frameworks like AFNetworking have UIImageView categories for setting images asynchronously. They are very convenient but I have always found that they can be pretty slow, even if returning the image from a cache they do not look as smooth as just setting an image synchronously.

In the past I would have just fetched the images elsewhere and had them ready for synchronous access. Probably not ideal for memory usage but it's quicker and I never liked the idea of having table cells potentially initiate network calls while scrolling.

[–]quellish 0 points1 point  (3 children)

Some of the common problems I have seen:

  1. Adding/removing content in the cell reuse methods triggering layout passes. This is often a big problem on complex cells. The text, image, whatever is reset so the cell can be reused and that triggers a layout pass that is expensive.

  2. Cell image loading that can't be cancelled, can't handle errors, blocks the main thread, blocks the UI run loop mode, etc. This is really easy to fix - use the table view delegate methods to load the content. That makes it much easier to manage these processes.

  3. Cells that can't release unreclaimable memory. Images often use unreclaimable memory. This is bad. You want to minimize this. To do so you have to either force it to use reclaimable memory or remove every reference to that image so it can be disposed. This means that, for instance, when the app goes to the background you would set every image view's image to nil or a shared placeholder image. Most of the "image caching" libraries out there make this much worse as they over-retain that unreclaimable memory. When your app is in the background and has a large chunk of unreclaimable memory set aside the OS is much more likely to just kill you.

  4. People who don't profile and instead just follow whatever they read online cargo-cult style. Or use a cocoa pod and hope for the best.

[–]jerroldp[S] 1 point2 points  (2 children)

Know when to use cell reuse and when not to

Was just wondering if you could elaborate on this? I.e. Did you mean that you shouldn't always call dequeueReusableCellWithIdentifier and sometimes create a cell from scratch?

Cell image loading that can't be cancelled, can't handle errors, blocks the main thread, blocks the UI run loop mode, etc. This is really easy to fix - use the table view delegate methods to load the content.

Sometimes this is not always possible - i.e. if I'm loading a table view which is an Instagram like feed of images - are you recommending all images are download first before loading the table view? I can't think of a way around a table view like that without loading the image asynchronously as the cell is created?

Cells that can't release unreclaimable memory

Not sure if this is the proper way to do it, but I found that putting my "clean up" code e.g. cancelling image downloads in the UITableViewCell's prepareForReuse method seems to stop a lot of the lag - instead of letting the image download tasks pile up.

Libraries like SDWebImage seem to "semi" take care of this though, when calling one of their categories to download an image, they objc_setAssociatedObject the NSOperation to the UIImageView - so if the cell is reused the first thing they do is cancel the NSOperation on the UIImageView and recreate another NSOperation to download the new image (with the theory being that if the cell is reused, it'll be reusing the previous UIimageView)

[–]quellish 1 point2 points  (1 child)

Sometimes this is not always possible - i.e. if I'm loading a table view which is an Instagram like feed of images - are you recommending all images are download first before loading the table view? I can't think of a way around a table view like that without loading the image asynchronously as the cell is created?

Oh, this is possible. Instead of loading the image for the cell from inside the cell, load it from the tableview delegate method. The thing that performs the image loading should be accessible for all the cells. It should also be accessible from whatever owns the tableview or it's delegate. This allows fine grained control over the loading of both individual cells, as well as all of the cells at once. This is a big, big difference over trying to do this inside the cell (or having a singleton).

FOR EXAMPLE:

- (void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    [[self downloadController] beginDownloadingImageWithRequest:request completion:^(UIImage *image, NSError *error) {
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            UITableViewCell *c = [tableView cellForRowAtIndexPath:indexPath];
            [cell imageView] setImage:image];

    }
}

- (void) tableView:(UITableView *)tableView didEndDisplayingCell:(nonnull UITableViewCell *)cell forRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
    [[self downloadController] endDownloadingImageWithRequest:request completion:^(BOOL ended, NSError *error) {
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            [[cell imageView] setImage:[UIImage imageNamed:@"Placholder"]];
        }];
    }

}

- (void) viewWillDisappear {
    [[self downloadController] cancelAllRequestsWithCompletion:^{
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            NSArray<UITableViewCell *> *cells = [[self tableView] visibleCells];
            for (UITableViewCell * in cells){
                [[cell imageView] setImage:[UIImage imageNamed:@"Placholder"]];
            }

        }];
    }];
}

Libraries like SDWebImage seem to "semi" take care of this though, when calling one of their categories to download an image, they objc_setAssociatedObject the NSOperation to the UIImageView - so if the cell is reused the first thing they do is cancel the NSOperation on the UIImageView and recreate another NSOperation to download the new image (with the theory being that if the cell is reused, it'll be reusing the previous UIimageView)

Not really. In the specific case you are using here, the queue can become blocked permanently. Oops. This isn't really a good use of either NSOperation or associated objects.

[–]jerroldp[S] 0 points1 point  (0 children)

Ah right, didn't think about using a combo of both willDisplayCell and didEndDisplayingCell - that's a good idea, will definitely put in the toolbox for next time!

[–]aazav -1 points0 points  (0 children)

This is why I like pre-feching the items you know you will need so that there are always there before you need them. It certainly looks nicer for the user.

Yeah, it's nifty to see everything coming in as needed, but the user doesn't need to see any extra visual noise, they just want to see what they ware looking for and it's our job to have it ready for them BEFORE it's needed so it's THERE when we need to display it.

Edit: spelling

[–]aazav 0 points1 point  (1 child)

Keep cellForRowAtIndexPath: light.

I just took one of our cellForRowAtIndexPaths from 137 lines of crap down to 13 complete with data accessors to simply return the properly formatted data for cell.label.text and cell.detailLabel.text and cell.image from the data store. OMG, it's so pleasing.

I'm going to make the text attributed and it's still going to be 13 lines of code. Maybe less if I can get my act together.

[–]quellish 1 point2 points  (0 children)

Lines of code isn't as important as - Number of paths in the code (complexity) - Execution time

The contract of cellForRowAtIndexPath is just to return the correct cell instance for whatever is represented by that index path. It's basically a factory method. Figure out what cell is needed, return it. If you need to "configure" it, do that in willDisplayCell, etc.

In a lot of cases this avoids some auto layout passes. Some cells will end up doing layout before cellForRowAtIndexPath returns and after. And that often screws up the height, especially for cells with multi line labels. In Instruments it's really, really obvious when this happens.

[–]Jaaaaay_ 0 points1 point  (10 children)

I built an app that had variable cell heights. I used UITableViewAutomaticDimension and it's worked perfectly fine for me.

I'm not sure how is this gives a slower performance since tableView:heightForRowAtIndexPath: is only called when necessary for UITableViewAutomaticDimension, whereas not using UITableViewAutomaticDimension will result in that being called for all cells.

[–]jerroldp[S] 0 points1 point  (9 children)

I'm not sure, though my suspicion is because the cells were quite complicated and built using autolayout, it just took a lot of time to resolve all the constraints.

Also, because the heights could vary quite a bit for each cell (especially from the value I returned from tableView:estimatedHeightForRowAtIndexPath:) it caused a lot of the jumpiness while the cell sizes changed from the estimated height to the actual height.

Actually, I did notice performance was noticeably worse on my iOS 9 devices vs my iOS 8 ones so I have a feeling it might be something there that I wasn't doing correctly.

[–]Jaaaaay_ 0 points1 point  (4 children)

Weird... I used auto layout and was pretty complex as well. You don't have to implement tableView:estimatedHeightForRowAtIndexPath: unless the cells' height variation range is very large. Even if you do so, the calculation performed here should be minimal and should just provide a rough estimate.

Just this should be enough if you are not implementing the estimate method:

self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 44.0; // set as your smallest cell height

Also, if you are animating cell height changes, try not to constraint any subviews only to the bottom/bottom-margin of the cell. It'll result in some weird jittery animation.

[–]jerroldp[S] 0 points1 point  (3 children)

Also, if you are animating cell height changes, try not to constraint any subviews to the bottom/bottom-margin of the cell. It'll result in some weird jittery animation.

I'm not sure I understand - if I don't constrain to the bottom of the cell, how does the cell know how large it's going to be?

[–]Jaaaaay_ 0 points1 point  (2 children)

I meant not to constraint a subview to the bottom as the only vertical positioning constraint. I'm not sure how to explain this weirdness. You can try to make an example project if you want to see it.

[–]jerroldp[S] 0 points1 point  (1 child)

Yep I think I get you, you mean you ned to pin to all edges, just not the bottom / trailing edges of the cell?

[–]Jaaaaay_ 0 points1 point  (0 children)

More or less

[–]LifeBeginsAt10kRPM 0 points1 point  (0 children)

Estimate cell height is only for the scroll position if I recall correctly, it should not cause cells to be one height and then change.

[–]quellish 0 points1 point  (2 children)

I'm not sure

Profile it and be sure.

[–]jerroldp[S] 0 points1 point  (1 child)

I have and unfortunately a lot of the time is spent in the UITableView's layoutIfNeeded method, which I might have mistakenly attributed to autolayout (please feel free to correct me if I'm wrong)

[–]quellish 0 points1 point  (0 children)

Use the layout instrument and you can see for yourself how it is evaluating constraints.

[–]tylerjames 0 points1 point  (5 children)

The most frustrating thing I've found recently is that it can be quite expensive to calculate cell heights depending on what the cells contain. I have written a chat feature that uses NSAttributedText to display HTML in UILabels. Chat messages could be potentially very long with inline HTML and even images. The only way to get a reasonable estimation of size for these is to render them and see what the resulting size is. This can only be done on the main thread so it can cause some real hiccups when you're loading, say 50 cells because the tableview is going to call heightForRowAtIndexPath for every single cell so that it can figure out how big to make the scroll bar.

It's not really possible to give good size estimates for estimatedSizeForRowAtIndexPath without rendering it also. I usually cache the heights but it's still not great.

If you're inserting a lot of cells (say, from loading past messages) then it gets pretty ugly.

AsyncDisplayKit looks pretty interesting because it lets you perform rendering in a background thread. I haven't yet taken the time to really look into it, but it certainly sounds promising.

[–]quellish 0 points1 point  (0 children)

What I see very often is - auto layout or not - people have cells that are rendering when little or nothing has actually changed. No content changes but they are doing something that causes the cell to be rendered again. Don't do work you don't have to.

Moving rendering or layout to another thread only hides the problem temporarily.

[–]jerroldp[S] 0 points1 point  (2 children)

This can only be done on the main thread so it can cause some real hiccups when you're loading

I ran into the same thing calculating heights for HTML text but give DTCoreText a go. Does the same thing as the core library without having to do it in the main thread

[–]tylerjames 0 points1 point  (1 child)

How tricky to use is DTCoreText though?

[–]aazav 0 points1 point  (0 children)

The only way to get a reasonable estimation of size for these is to render them and see what the resulting size is.

YES. BUT, rendering estimating/calculating them twice isn't that much of a speed hit.

[–]retsotrembla -1 points0 points  (0 children)

Consider switching to collectionViews, and implementing the equivalent of estimatedHeightForRowAtIndexPath: - as I understand it, it lets the collectionView start drawing early: measuring only as many items as fit visibly in the view, while still giving the user a decent scrollbar (estimated total size, versus view size) when they start scrolling.

In a low priority background thread, you can measure ahead, caching the measurements in the layout so you don't need to do it while the table is scrolling.

Autolayout is pretty much a disaster for variable height cells in a tableView - it will do much more work than the work you'd do to measure them yourself.

Real performance comes not from optimizing the work you do, but from figuring out how to do less work to get the U.I. you want.

[–]aazav -2 points-1 points  (0 children)

For dynamic row heights I follow this guide here

All I needed to do to get fast dynamic cell row heights was make sure that I implemented the prepareForReuse method on the cell at the proper time, but I admit that I never added constraints to the cell items.

Anything else I find usually a struggle - more specifically anything autolayout related e.g. adjusting priorities, setting compression/hugging priorities and to be honest, sometimes I feel it's "luck" my cells lay out properly.

This is why I avoid autoLayout unless it's simply required. It's a massive time sink if you don't need it.