forkingdog关于UITableView优化的框架其实已经能够应用在一般的场景,且有蛮多的知识点供我们借鉴,借此站在巨人的肩膀上来分析一把。
至于UITableView的瓶颈在哪里,我相信网上随便一搜就能了解的大概,我这里顺便提供下信息点:
1 //罪魁祸首 2 tableView:cellForRowAtIndexPath: 3 tableView:heightForRowAtIndexPath:
框架同样根据这两个痛点给出了解决方案:
高度计算
fd_heightForCellWithIdentifier:configuration方法
1 - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier configuration:(void (^)(id cell))configuration { 2 if (!identifier) { 3 return 0; 4 } 5 6 UITableViewCell *templateLayoutCell = [self fd_templateCellForReuseIdentifier:identifier]; 7 8 // Manually calls to ensure consistent behavior with actual cells. (that are displayed on screen) 9 [templateLayoutCell prepareForReuse]; 10 11 // Customize and provide content for our template cell. 12 if (configuration) { 13 configuration(templateLayoutCell); 14 } 15 16 return [self fd_systemFittingHeightForConfiguratedCell:templateLayoutCell]; 17 }
这里先是通过调用fd_templateCellForReuseIdentifier:从dequeueReusableCellWithIdentifier取出之后,如果需要做一些额外的计算,比如说计算cell高度, 可以手动调用 prepareForReuse方法,以确保与实际cell(显示在屏幕上)行为一致。接着执行configuration参数对Cell内容进行配置。最后通过调用fd_systemFittingHeightForConfiguratedCell:方法计算实际的高度并返回。
fd_systemFittingHeightForConfiguratedCell方法
1 - (CGFloat)fd_systemFittingHeightForConfiguratedCell:(UITableViewCell *)cell { 2 CGFloat contentViewWidth = CGRectGetWidth(self.frame); 3 4 // If a cell has accessory view or system accessory type, its content view‘s width is smaller 5 // than cell‘s by some fixed values. 6 if (cell.accessoryView) { 7 contentViewWidth -= 16 + CGRectGetWidth(cell.accessoryView.frame); 8 } else { 9 static const CGFloat systemAccessoryWidths[] = { 10 [UITableViewCellAccessoryNone] = 0, 11 [UITableViewCellAccessoryDisclosureIndicator] = 34, 12 [UITableViewCellAccessoryDetailDisclosureButton] = 68, 13 [UITableViewCellAccessoryCheckmark] = 40, 14 [UITableViewCellAccessoryDetailButton] = 48 15 }; 16 contentViewWidth -= systemAccessoryWidths[cell.accessoryType]; 17 } 18 19 // If not using auto layout, you have to override "-sizeThatFits:" to provide a fitting size by yourself. 20 // This is the same height calculation passes used in iOS8 self-sizing cell‘s implementation. 21 // 22 // 1. Try "- systemLayoutSizeFittingSize:" first. (skip this step if ‘fd_enforceFrameLayout‘ set to YES.) 23 // 2. Warning once if step 1 still returns 0 when using AutoLayout 24 // 3. Try "- sizeThatFits:" if step 1 returns 0 25 // 4. Use a valid height or default row height (44) if not exist one 26 27 CGFloat fittingHeight = 0; 28 29 if (!cell.fd_enforceFrameLayout & contentViewWidth > 0) { 30 // Add a hard width constraint to make dynamic content views (like labels) expand vertically instead 31 // of growing horizontally, in a flow-layout manner. 32 NSLayoutConstraint *widthFenceConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:contentViewWidth]; 33 [cell.contentView addConstraint:widthFenceConstraint]; 34 35 // Auto layout engine does its math 36 fittingHeight = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height; 37 [cell.contentView removeConstraint:widthFenceConstraint]; 38 39 [self fd_debugLog:[NSString stringWithFormat:@"calculate using system fitting size (AutoLayout) - %@", @(fittingHeight)]]; 40 } 41 42 if (fittingHeight == 0) { 43 #if DEBUG 44 // Warn if using AutoLayout but get zero height. 45 if (cell.contentView.constraints.count > 0) { 46 if (!objc_getAssociatedObject(self, _cmd)) { 47 NSLog(@"[FDTemplateLayoutCell] Warning once only: Cannot get a proper cell height (now 0) from ‘- systemFittingSize:‘(AutoLayout). You should check how constraints are built in cell, making it into ‘self-sizing‘ cell."); 48 objc_setAssociatedObject(self, _cmd, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 49 } 50 } 51 #endif 52 // Try ‘- sizeThatFits:‘ for frame layout. 53 // Note: fitting height should not include separator view. 54 fittingHeight = [cell sizeThatFits:CGSizeMake(contentViewWidth, 0)].height; 55 56 [self fd_debugLog:[NSString stringWithFormat:@"calculate using sizeThatFits - %@", @(fittingHeight)]]; 57 } 58 59 // Still zero height after all above. 60 if (fittingHeight == 0) { 61 // Use default row height. 62 fittingHeight = 44; 63 } 64 65 // Add 1px extra space for separator line if needed, simulating default UITableViewCell. 66 if (self.separatorStyle != UITableViewCellSeparatorStyleNone) { 67 fittingHeight += 1.0 / [UIScreen mainScreen].scale; 68 } 69 70 return fittingHeight; 71 }
这里作者考虑到了如果Cell使用了accessory view或者使用了系统的accessory type,需要减掉相应的宽度。接着判断如果使用了AutoLayout,则使用iOS 6提供的systemLayoutSizeFittingSize方法获取高度。如果高度为0,则尝试使用Frame Layout的方式,调用重写的sizeThatFits方法进行获取。如果还是为0,则给出默认高度并返回。
Cell重用
fd_templateCellForReuseIdentifier方法
1 - (__kindof UITableViewCell *)fd_templateCellForReuseIdentifier:(NSString *)identifier { 2 NSAssert(identifier.length > 0, @"Expect a valid identifier - %@", identifier); 3 4 NSMutableDictionary *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd); 5 if (!templateCellsByIdentifiers) { 6 templateCellsByIdentifiers = @{}.mutableCopy; 7 objc_setAssociatedObject(self, _cmd, templateCellsByIdentifiers, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 8 } 9 10 UITableViewCell *templateCell = templateCellsByIdentifiers[identifier]; 11 12 if (!templateCell) { 13 templateCell = [self dequeueReusableCellWithIdentifier:identifier]; 14 NSAssert(templateCell != nil, @"Cell must be registered to table view for identifier - %@", identifier); 15 templateCell.fd_isTemplateLayoutCell = YES; 16 templateCell.contentView.translatesAutoresizingMaskIntoConstraints = NO; 17 templateCellsByIdentifiers[identifier] = templateCell; 18 [self fd_debugLog:[NSString stringWithFormat:@"layout cell created - %@", identifier]]; 19 } 20 21 return templateCell; 22 }
这里通过dequeueReusableCellWithIdentifier方法从队列中获取templateCell,并通过fd_isTemplateLayoutCell属性标识其只用来充当模板计算,并不真正进行呈现,最后通过关联对象的方式进行存取。
注意:这里通过dequeueReusableCellWithIdentifier进行获取,也就意味着你必须对指定的Identifier先进行注册,注册可以通过以下三中方法:
1.使用storyboard中的Cell原型 2.使用registerNib:forCellReuseIdentifier: 3.使用registerClass:forCellReuseIdentifier:
到这里最重要的几个方法已经讲完了,除此之外框架还针对获取的高度进行了缓存。缓存的方式分为两种 :
1 1.根据IndexPath进行缓存(fd_heightForCellWithIdentifier:cacheByIndexPath:configuration) 2 2.根据实体的唯一标识符进行缓存(fd_heightForCellWithIdentifier:cacheByKey:configuration)
总结:
UITableView优化方案其实还有很多,不同的场景选用不同的方案,实现效果达到预期,这才是我么最终的目标。我这里简单介绍下其他的优化的细节:
1.复杂界面的时候,我们可以尝试异步手动进行绘制。 2.针对超出屏幕的Cell进行预缓存 3.存在大量图片的时候,只针对目标范围内的图片进行异步加载并缓存结果。 4.设置Views/Layers为不透明。