In recent years, apps have become more and more experience-oriented, and it’s hard to buy a product by writing a random sentence. A smooth list is one of the most important things. If the scrolling of an App is always slow, it can be regarded as a negative textbook to ridicule or set off “our App Balabala…” , or directly uninstall. I’ve been working on this one lately, so let’s make a summary.

If there is a good blog post to recommend, ibireme’s iOS tips for keeping the interface smooth is a classic, and wall Crack recommends reading it again and again. This article explains a lot of optimization points, and I summarize the two optimization points with the biggest benefits:

  • Avoid calculating the cell row height repeatedly
  • Asynchronous text rendering

The data is captured by the iPhone6 with instruments. The data on the left is analyzed by using Auto Layout to draw the interface. Normally, if you want smooth scrolling, the FPS should be at least around 55. In the case of no cache line height and asynchronous rendering, FPS is the lowest, which can be said to be relatively slow, at least it can be perceived by the naked eye, can meet the requirements of smooth scrolling only in the case of cache line height and asynchronous rendering; On the right is the data analysis of using frame to draw the interface directly without Auto Layout. It can be found that even without asynchronous rendering, it can barely meet the requirements of smooth scrolling. If asynchronous rendering is enabled, it can be said that it is quite smooth.

Avoid calculating the cell row height repeatedly

Row height calculation of TableView is a cliche. HeightForRowAtIndexPath is a fairly frequently called method, and doing too many things in it can cause congestion. In iOS 8, we can easily achieve high adaptability by setting the following two properties:

self.tableView.estimatedRowHeight = 88;
self.tableView.rowHeight = UITableViewAutomaticDimension;
Copy the code

While this is convenient, it is not recommended if your page face has certain performance requirements. For details, see sunnyxx for optimizing UITableViewCell for high computation. This paper provides a cache library uitableView-fdTemPlatelayOutCell for Auto Layout, which can help us avoid the problem of multiple calculation of cell row height.

If Auto Layout is not used, we can calculate the frame height and cell height of each control on the page in advance after receiving the data, and cache it in memory. When using it, we can directly fetch the calculated value in heightForRowAtIndexPath:, and the process is as follows:

  • Mock request data callback:
- (void)viewDidLoad {
[super viewDidLoad];

[self buildTestDataThen:^(NSMutableArray <FDFeedEntity *> *entities) {
self.data = @[].mutableCopy;
@autoreleasepool {
for (FDFeedEntity *entity in entities) {
FrameModel *frameModel = [FrameModel new];
frameModel.entity = entity;
[self.data addObject:frameModel]; }} [self.tvFeed reloadData];
}];
}
Copy the code
  • A simple way to calculate the row height of frame and cell:
//FrameModel.h

@interface FrameModel : NSObject

@property (assign.nonatomic.readonly) CGRect titleFrame;
@property (assign.nonatomic.readonly) CGFloat cellHeight;
@property (strong.nonatomic) FDFeedEntity *entity;

@end
Copy the code
//FrameModel.m

@implementation FrameModel

- (void)setEntity:(FDFeedEntity *)entity {
if(! entity)return;

_entity = entity;

CGFloat maxLayout = ([UIScreen mainScreen].bounds.size.width - 20.f);
CGFloat bottom = 4.f;

//title
CGFloat titleX = 10.f;
CGFloat titleY = 10.f;
CGSize titleSize = [entity.title boundingRectWithSize:CGSizeMake(maxLayout, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading attributes:@{NSFontAttributeName : Font(16.f)} context:nil].size;
_titleFrame = CGRectMake(titleX, titleY, titleSize.width, titleSize.height);

//cell Height
_cellHeight = (CGRectGetMaxY(_titleFrame) + bottom);
}

@end
Copy the code
  • Row height value:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
FrameFeedCell *cell = [tableView dequeueReusableCellWithIdentifier:FrameFeedCellIdentifier forIndexPath:indexPath];
FrameModel *frameModel = self.data[indexPath.row];
cell.model = frameModel;
return cell;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
FrameModel *frameModel = self.data[indexPath.row];
return frameModel.cellHeight;
}
Copy the code
  • Control assignment:
- (void)setModel:(FrameModel *)model {
if(! model)return;

_model = model;

FDFeedEntity *entity = model.entity;

self.titleLabel.frame = model.titleFrame;
self.titleLabel.text = entity.title;
}
Copy the code

The advantages and disadvantages

While uitableView-fdTemPlatelayOutCell is already well handled, Auto Layout is still a bit of a performance drain. Manual calculation of all positions in frame mode requires calculation, which is cumbersome. In addition, in the case of a large amount of data, a large amount of calculation will partially affect the data display time, and the corresponding reward is better performance.

Asynchronous text rendering

When a lot of text is displayed, the CPU becomes very stressed. There is only one solution, and that is a custom text control that asynchronously draws text using TextKit or the low-level CoreText. Once the CoreText object is created, it can directly obtain the width and height of the text, avoiding multiple calculations (once when the UILabel size is adjusted, and again when the UILabel is drawn). CoreText objects take up less memory and can be cached for later multiple renders.

Fortunately, there is a library called YYText that you can use to support asynchronous text rendering. Here’s how to use it to maximize our silky needs:

Frame with asynchronous rendering

The basic idea is similar to calculating frame, except that the boundingRectWithSize: and sizeWithAttributes of the system are replaced with the methods in YYText:

  • Configure the frame Model:
//FrameYYModel.h

@interface FrameYYModel : NSObject

@property (assign.nonatomic.readonly) CGRect titleFrame;
@property (strong.nonatomic.readonly) YYTextLayout *titleLayout;

@property (assign.nonatomic.readonly) CGFloat cellHeight;

@property (strong.nonatomic) FDFeedEntity *entity;

@end
Copy the code
//FrameYYModel.m

@implementation FrameYYModel

- (void)setEntity:(FDFeedEntity *)entity {
if(! entity)return;

_entity = entity;

CGFloat maxLayout = ([UIScreen mainScreen].bounds.size.width - 20.f);
CGFloat space = 10.f;
CGFloat bottom = 4.f;

//title
NSMutableAttributedString *title = [[NSMutableAttributedString alloc] initWithString:entity.title];
title.yy_font = Font(16.f);
title.yy_color = [UIColor blackColor];

YYTextContainer *titleContainer = [YYTextContainer containerWithSize:CGSizeMake(maxLayout, CGFLOAT_MAX)];
_titleLayout = [YYTextLayout layoutWithContainer:titleContainer text:title];

CGFloat titleX = 10.f;
CGFloat titleY = 10.f;
CGSize titleSize = _titleLayout.textBoundingSize;
_titleFrame = (CGRect){titleX,titleY,CGSizeMake(titleSize.width, titleSize.height)};

//cell Height
_cellHeight = (CGRectGetMaxY(_titleFrame) + bottom);
}

@end
Copy the code

YYTextLayout can be configured in advance for text features, including font, textColor, line number, line spacing, inner spacing, etc. The advantage is that some logic can be processed in advance, such as dynamically configuring the font color according to the interface field. If you use Auto Layout, this logic will inevitably be written in cellForRowAtIndexPath:.

  • UITableViewCell processing:
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (!self) return nil;

YYLabel *title = [YYLabel new];
title.displaysAsynchronously = YES; // Enable asynchronous rendering
title.ignoreCommonProperties = YES; // Ignore attributes
title.layer.borderColor = [UIColor brownColor].CGColor;
title.layer.cornerRadius = 1.f;
title.layer.borderWidth = 1.f;
[self.contentView addSubview:_titleLabel = title];

return self;
}
Copy the code
  • Assignment:
- (void)setModel:(FrameYYModel *)model {
if(! model)return;
_model = model;

self.titleLabel.frame = model.titleFrame;
self.titleLabel.textLayout = model.titleLayout; // Take YYTextLayout directly
}
Copy the code

Auto Layout with asynchronous rendering

YYText is very friendly and also supports XIB. YYText is derived from UIView, and what it does is very simple:

  • Configure constraints in the XIB
  • Enabling asynchronous properties

Enable asynchronous attribute can be set in code, or directly set in xiB, as follows:

self.titleLabel.displaysAsynchronously = YES;
self.subTitleLabel.displaysAsynchronously = YES;
self.contentLabel.displaysAsynchronously = YES;
self.usernameLabel.displaysAsynchronously = YES;
self.timeLabel.displaysAsynchronously = YES;
Copy the code

Another thing to note is that you need to set the maximum line feed width for multiple lines of text:

CGFloat maxLayout = [UIScreen mainScreen].bounds.size.width - 20.f;
self.titleLabel.preferredMaxLayoutWidth = maxLayout;
self.subTitleLabel.preferredMaxLayoutWidth = maxLayout;
self.contentLabel.preferredMaxLayoutWidth = maxLayout;
Copy the code

The advantages and disadvantages

YYText asynchronous rendering can greatly improve the smoothness of the list, really achieve as smooth as silk, but in the open asynchrony, refresh the list will have flashing situation, think it is normal, after all, it is asynchronous, rendering also takes time, here the author gives some schemes, we can have a look.

other

About the rounded

If there are many system Settings in the list of rounded pages resulting in a lag:

label.layer.cornerRadius = 5.f;
label.clipsToBounds = YES;
Copy the code

According to observation, as long as the current screen as long as the number of controls set rounded corners is not too much (about a dozen counts as a critical point), will not cause stuck.

If you want the rounded control to have a white background and the parent control to have a white background and not highlight it, then you don’t need to clipsToBounds.

How to locate the cause of lag

We can use the Time Profiler in Instruments to help us locate the problem. Select Xcode, Command + Control + I to open:

We select the main thread, remove the system method, and then manipulate the list, and then intercept the call information. We can see that the method we implement doesn’t take much time, but the system method takes a lot of time, which is one of the reasons for the lag:

For others, instruments can’t see the method call stack (a pair of black methods on the right), just go to the Xcode Settings:

conclusion

YYText and uitableView-fdTemPlatelayOutCell can greatly improve the list smoothness:

  • If time is tight, you can directly use Auto Layout + UITableView-FDTemPlatelayOutCell + YYText

  • If the text of the list does not contain rich text, but displays only text, and you do not want to introduce either library, you can calculate the Frame in advance in a systematic way

  • If you want to maximize the fluency, you need to calculate the Frame + YYText in advance, and you can choose the appropriate scheme according to your own situation

Some discussion about smooth list scrolling schemes in iOS