随着iOS11的到来,开发app支持的最低版本也就是iOS8了。iOS8中添加的Self Sizing Cells功能有没有使用过呢?比自己计算cell的高度真的快很多吗?今天写个demo来看一看。demo中会创建两个Controller,分别使用手动计算cell高度的方式和cell自己计算高度的方式加载同一类型同样数据的cell。
Demo地址:https://github.com/huibaoer/Demo_TableViewOptimize.git
1. 编写自定义cell
TableViewCell是一个能展示图片和文字,或者只展示文字的cell,为了快速编写,使用xib方式创建。cell上方是一个640:530的imageView,下方是一个展示文字的label。当传进来的数据有图片和文字的时候就同时显示;当传进来的数据只有文字的时候就隐藏imageView只显示文字。约束一般情况下动态修改都是修改constant值,但是imageView没有高度约束,它的高度是由宽高比计算来的,想使用通过改变imageView的高度达到只显示文字的效果不是很好实现。所以为label分别添加了两个top约束,通过active属性控制哪个约束生效来达到展示图文或者只展示文字的效果。
创建问基本UI后,为cell添加手动计算高度的方法。
+ (CGFloat)cellHeightWithDictionary:(NSDictionary *)dic {
CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width;
NSString *image = dic[@"image"];
CGFloat imageHeight = -10;
if (image) {
imageHeight = (screenWidth-32)*530.0/640;
}
NSString *text = dic[@"text"];
NSDictionary *attributes = @{NSFontAttributeName : [UIFont systemFontOfSize:17.f ]};
CGRect rect = [text boundingRectWithSize:CGSizeMake(screenWidth-32, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading attributes:attributes context:nil];
return 16 + imageHeight + 10 + ceil(rect.size.height) + 16 + 0.5;//系统分割线占了0.5
}
添加数据的方法,这里cell根据传入的数据不同,显示不同的内容和样式。
- (void)setDictionary:(NSDictionary *)dic {
_descLabel.text = dic[@"text"];
NSString *image = dic[@"image"];
if (image) {
_labelTop2Image.active = YES;
_labelTop2ContentView.active = NO;
_mediaImgView.hidden = NO;
_mediaImgView.image = [UIImage imageNamed:image];
} else {
_labelTop2Image.active = NO;
_labelTop2ContentView.active = YES;
_mediaImgView.hidden = YES;
}
[self setNeedsLayout];
}
2. 创建一点假数据
DataAccess类负责数据的提供,demo中就直接本地随机创建的假数据。为了保证两个Controller获取的数据一致。DataAccess就只有在单例初始化的时候创建了一次数据。
static DataAccess *instance = nil;
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[[self class] alloc] init];
});
return instance;
}
- (instancetype)init {
self = [super init];
if (self) {
_array = [[self class] getData];
}
return self;
}
+ (NSArray *)getData {
NSMutableArray *arr = [NSMutableArray array];
for (int i = 0; i < 99; i++) {
NSDictionary *dic = [self generateCellData];
[arr addObject:dic];
}
return arr;
}
+ (NSDictionary *)generateCellData {
NSMutableString *str = [NSMutableString string];
int random = arc4random()%30 + 1;
for (int i = 0; i < random; i++) {
[str appendString:@"随机生成的内容"];
}
NSString *imageName = @"eva_image";
if (random % 3 == 0) {
imageName = nil;
}
NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithObject:str forKey:@"text"];
if (imageName) {
[dic setObject:imageName forKey:@"image"];
}
return dic;
}
3. 手动计算cell高度的tableView
在FirstViewController中我们让tableView用手动方式计算cell, heightForRowAtIndexPath方法将会被调用多次来计算每个要展示的cell的高度
@interface FirstViewController () <UITableViewDataSource, UITableViewDelegate>
@property (nonatomic, strong) NSArray *dataArray;
@property (nonatomic, strong) UITableView *tableView;
@end
@implementation FirstViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
_dataArray = [DataAccess sharedInstance].array;
_tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
_tableView.dataSource = self;
_tableView.delegate = self;
[_tableView registerNib:[UINib nibWithNibName:@"TableViewCell" bundle:[NSBundle mainBundle]] forCellReuseIdentifier:@"cell"];
[self.view addSubview:_tableView];
}
#pragma mark - tableView
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return _dataArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSDictionary *dic = _dataArray[indexPath.row];
TableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
[cell setDictionary:dic];
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSDictionary *dic = _dataArray[indexPath.row];
CGFloat cellHeight = [TableViewCell cellHeightWithDictionary:dic];
return cellHeight;
}
@end
4. cell 自动计算高度
SecondViewController中使用自动计算高度的方式,对比下代码发现去掉了heightForRowAtIndexPath方法,多了estimatedRowHeight和rowHeight两个属性的设置。因为cell内部UI元素的纵向约束都已经加好了,所以cell就可以根据具体的内容自动计算高度撑起来。iOS11中estimatedRowHeight属性默认是开启的,看来苹果已经很推荐我们使用这种方式了,不过一些老代码可能需要将其关闭。
@interface SecondViewController () <UITableViewDataSource, UITableViewDelegate>
@property (nonatomic, strong) NSArray *dataArray;
@property (nonatomic, strong) UITableView *tableView;
@end
@implementation SecondViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
_dataArray = [DataAccess sharedInstance].array;
_tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
_tableView.dataSource = self;
_tableView.delegate = self;
_tableView.estimatedRowHeight = 300;
_tableView.rowHeight = UITableViewAutomaticDimension;
[_tableView registerNib:[UINib nibWithNibName:@"TableViewCell" bundle:[NSBundle mainBundle]] forCellReuseIdentifier:@"cell"];
[self.view addSubview:_tableView];
}
#pragma mark - tableView
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return _dataArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
TableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
NSDictionary *dic = [_dataArray objectAtIndex:indexPath.row];
[cell setDictionary:dic];
return cell;
}
5. 看看结果
demo写完了,我们使用 Instruments 下的 Time Profiler 看看两个页面的tableView的耗时情况,连接真机,开始启动后,我分别将两个tableView的内容滚动到了最低端,没有做其他的操作。可以看出耗时最高的就是手动计算cell高度的方法。看来使用Self Sizing cells还是能提升tableView的不少性能的。
6. TableView的一些其他优化
- cell复用,cell的复用是tableView展示内容的一个良好机制,可以有效的减少cell对象的个数减少内存占用,需要注意的是根据业务需要尽量减少cell的类型,理论上可以减少cell复用池中的总个数。另外自定义cell的一些UI不要在cell的调用过程中频繁创建,尽量在cell创建时只创建一次,通过显示隐藏等方式使cell展示不同的样式。nib文件可以方便我们创建UI添加约束,但是相较于纯代码编写,纯代码的效率更高,可根据实际情况取舍。
- cell高度的计算,传统的手动计算cell高度的方式,尽量避免在heightForRow方法中频繁计算,可考虑在数据获取后,统一计算一次cell高度存起来,在heightForRow方法中直接返回。现在Self Sizing Cells是另一种更好的优化方式。
- tableView在快速滚动时,会频繁调用cellForRow方法获取cell,尽量快速的返回cell可以避免由于等待cell的返回造成的卡顿。建议在cellForRow方法中不要对cell做过多的设置和数据绑定,可以将这些操作放在willDisplayCell回调中。
- 在实际项目中,往往性能瓶颈在网络请求,适当的做一些缓存可以提升用户体验。