UITableViewCell 高度計(jì)算
UITableView 詢問 cell 高度有兩種方式:
1.rowHeight
屬性垛孔。所有Cell都為固定高度,這種情況下最好不要使用下面第2種方法先朦。
2.- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
代理方法,它會(huì)使rowHeight屬性的設(shè)置失效犬缨。
在自定義tableViewCell的時(shí)候,是否想過先在cellForRow...
方法里設(shè)置了數(shù)據(jù)模型棉浸,然后獲得cell準(zhǔn)確高度怀薛,再在heightForRow...
方法設(shè)置高度?但heightForRow...
是比cellForRow...
要先調(diào)用的迷郑,也就是調(diào)用heightForRow...
時(shí)還不知道行高枝恋。
實(shí)際加載tableView的過程中發(fā)現(xiàn)创倔,tableView幾個(gè)代理方法調(diào)用順序如下:
1.調(diào)用numberOfRow...
等詢問有多少個(gè)cell
2.調(diào)用heightForRow...
n次,n=cell的總個(gè)數(shù)
3.對(duì)當(dāng)前一屏顯示的x個(gè)cell焚碌,先調(diào)用cellForRow...
繪制畦攘,再調(diào)用heightForRow...
(依次交替調(diào)用x次)
4.當(dāng)屏幕滾動(dòng),有新的cell出現(xiàn)在屏幕上十电,同3知押,先調(diào)cellForRow...
再調(diào)heightForRow...
tableView繼承自scrollView ,它需要知道自己的contentSize鹃骂。因此它在一開始加載的時(shí)候台盯,對(duì)每個(gè)cell使用代理方法獲得它的高度 方便得到contentSize,進(jìn)而得到滾動(dòng)進(jìn)度條的位置畏线。但是静盅,如果cell太多,那么在首次加載的時(shí)候寝殴,會(huì)引發(fā)性能問題蒿叠,浪費(fèi)了多余的計(jì)算在屏幕外邊的 cell 上。
iOS7以后出現(xiàn)了預(yù)估高度estimatedRowHeight
對(duì)應(yīng)有:tableView: estimatedHeightForRowAtIndexPath:
如果設(shè)置了估算高度蚣常,避免了一開始調(diào)用n次heightForRow
導(dǎo)致的一些不必要的計(jì)算市咽,而是直接用預(yù)估高度*cell個(gè)數(shù)來計(jì)算contentSize。當(dāng)在繪制一個(gè)單元格時(shí)史隆,才去獲取它的準(zhǔn)確高度魂务。(步驟1、3泌射、4不變)
但是估算高度也有不足的地方:優(yōu)化UITableViewCell高度計(jì)算的那些事
1.設(shè)置估算高度后粘姜,contentSize.height 根據(jù)“cell估算值 x cell個(gè)數(shù)”計(jì)算,這就導(dǎo)致滾動(dòng)條的大小處于不穩(wěn)定的狀態(tài)熔酷,contentSize 會(huì)隨著滾動(dòng)從估算高度慢慢替換成真實(shí)高度孤紧,肉眼可見滾動(dòng)條突然變化甚至“跳躍”。
2.若是有設(shè)計(jì)不好的下拉刷新或上拉加載控件拒秘,或是 KVO 了 contentSize 或 contentOffset 屬性号显,有可能使表格滑動(dòng)時(shí)跳動(dòng)。
3.估算高度設(shè)計(jì)初衷是好的躺酒,讓加載速度更快押蚤,那憑啥要去侵害滑動(dòng)的流暢性呢,用戶可能對(duì)進(jìn)入頁面時(shí)多零點(diǎn)幾秒加載時(shí)間感覺不大羹应,但是滑動(dòng)時(shí)實(shí)時(shí)計(jì)算高度帶來的卡頓是明顯能體驗(yàn)到的揽碘,個(gè)人覺得還不如一開始都算好了呢(iOS8更過分,即使都算好了也會(huì)邊劃邊計(jì)算)
UITableView+FDTemplateLayoutCell
iOS8 之前雖然采用 autoLayout 相比 frame layout 得手動(dòng)計(jì)算已經(jīng)簡(jiǎn)化了不少:設(shè)置 estimatedRowHeight 屬性、對(duì)cell設(shè)置正確的約束雳刺、contentView 執(zhí)行 systemLayoutSizeFittingSize: 方法劫灶。但需要維護(hù)專門為計(jì)算高度而生的模板cell,以及UILabel 折行問題等掖桦。
iOS8后出現(xiàn)self-sizing cell本昏,設(shè)置好約束后,直接設(shè)置 estimatedRowHeight 就可以了枪汪。但是cell高度沒有緩存機(jī)制涌穆,不論何時(shí)都會(huì)重新計(jì)算 cell 高度。這樣就會(huì)導(dǎo)致滑動(dòng)不流暢料饥。
優(yōu)化的方式:對(duì)于已經(jīng)計(jì)算了高度的 Cell蒲犬,就將這個(gè)高度緩存起來,下次調(diào)用heightForRow...
方法時(shí)岸啡,返回高度緩存就行了原叮。UITableView+FDTemplateLayoutCell這個(gè)第三方開源主要做的就是這個(gè)事。
高度緩存
1.FDIndexPathHeightCache緩存策略
- 創(chuàng)建了一個(gè)類FDIndexPathHeightCache來進(jìn)行高度緩存的創(chuàng)建巡蘸、存取奋隶。
針對(duì)橫屏\豎屏分別聲明了 2 個(gè)以 indexPath 為索引的二維數(shù)組來存儲(chǔ)高度(section、row - 二維)悦荒。第一維定位到 Section唯欣,后一維定位到 Row,這樣就可以同時(shí)管到 Sections 和 Rows 的數(shù)據(jù)變動(dòng)搬味。
typedef NSMutableArray<NSMutableArray<NSNumber *> *> FDIndexPathHeightsBySection;
@interface FDIndexPathHeightCache ()
@property (nonatomic, strong) FDIndexPathHeightsBySection *heightsBySectionForPortrait;//豎屏?xí)r的基于indexPath高度緩存
@property (nonatomic, strong) FDIndexPathHeightsBySection *heightsBySectionForLandscape;//橫屏?xí)r的基于indexPath高度緩存
@end
使用indexPath 作為索引境氢,在發(fā)生刪除or插入單元格之后,緩存中的索引就需要進(jìn)行相應(yīng)的變動(dòng)碰纬,使用NSMutableArray能很方便適應(yīng)這種變動(dòng)萍聊。
如何創(chuàng)建高度緩存、分配空間悦析、初始化高度為-1寿桨,以及賦高度值到緩存數(shù)組中儲(chǔ)存和從緩存中取高度值,這些閱讀源碼都可以很好理解强戴,這里不多說亭螟。
- 分類 UITableView (FDIndexPathHeightCache)
@implementation UITableView (FDIndexPathHeightCache)
//懶加載?高度緩存
- (FDIndexPathHeightCache *)fd_indexPathHeightCache {
FDIndexPathHeightCache *cache = objc_getAssociatedObject(self, _cmd);
if (!cache) {
[self methodSignatureForSelector:nil];
cache = [FDIndexPathHeightCache new];//執(zhí)行init方法骑歹,初始化了兩個(gè)橫屏预烙、豎屏?xí)r的高度數(shù)組
objc_setAssociatedObject(self, _cmd, cache, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return cache;
}
@end
在UITableView+FDTemplateLayoutCell 框架中多處使用了runtime 的關(guān)聯(lián)對(duì)象Associated Object來進(jìn)行給類添加公有和私有變量。
_cmd表示當(dāng)前方法的Selector道媚。
OC 中可以通過 Category 給一個(gè)現(xiàn)有的類添加屬性扁掸,但是卻不能添加實(shí)例變量(即下劃線變量欢嘿,不過一般說法是不能添加屬性),這個(gè)“缺點(diǎn)”可以通過 Associated Objects 來彌補(bǔ)也糊。Associated Objects的使用樣例:1. 添加私有屬性用于更好地去實(shí)現(xiàn)細(xì)節(jié)。2.添加public屬性來增強(qiáng)category的功能羡宙。3.創(chuàng)建一個(gè)用于KVO的關(guān)聯(lián)觀察者狸剃。
關(guān)聯(lián)是可以保證被關(guān)聯(lián)的對(duì)象在關(guān)聯(lián)對(duì)象的整個(gè)生命周期都是可用的。
關(guān)聯(lián)對(duì)象 在這里的作用就相當(dāng)于是懶加載狗热,就是在用到相關(guān)的緩存策略時(shí)才會(huì)初始化(這里就初始化了橫豎屏?xí)r的兩個(gè)二維數(shù)組)钞馁。另外,它將內(nèi)存的釋放托管給了 UITableView 實(shí)例的生命周期匿刮,不用管釋放內(nèi)存的事情了僧凰。
Objective-C Associated Objects 的實(shí)現(xiàn)原理
ps:[self methodSignatureForSelector:nil];
這句runtime的沒太懂什么作用。
- 分類 UITableView (FDIndexPathHeightCacheInvalidation)
// We just forward primary call, in crash report, top most method in stack maybe FD's,
// but it's really not our bug, you should check whether your table view's data source and
// displaying cells are not matched when reloading.
static void __FD_TEMPLATE_LAYOUT_CELL_PRIMARY_CALL_IF_CRASH_NOT_OUR_BUG__(void (^callout)(void)) {
callout();
}
#define FDPrimaryCall(...) do {__FD_TEMPLATE_LAYOUT_CELL_PRIMARY_CALL_IF_CRASH_NOT_OUR_BUG__(^{__VA_ARGS__});} while(0)//宏定義.__VA_ARGS_ 就是直接將括號(hào)里的...轉(zhuǎn)化為實(shí)際的字符
調(diào)試時(shí)用的熟丸?看調(diào)用棧训措?沒看太懂 。注釋:“在崩潰報(bào)告中光羞,調(diào)用棧頂?shù)姆椒赡苁荈D的方法绩鸣,要檢查一下當(dāng)reload時(shí)dataSource和正在顯示的cell是否不對(duì)應(yīng)∩炊遥”
更新處理
+ (void)load {
// All methods that trigger height cache's invalidation 9個(gè)方法
SEL selectors[] = {
@selector(reloadData),
@selector(insertSections:withRowAnimation:),
@selector(deleteSections:withRowAnimation:),
@selector(reloadSections:withRowAnimation:),
@selector(moveSection:toSection:),
@selector(insertRowsAtIndexPaths:withRowAnimation:),
@selector(deleteRowsAtIndexPaths:withRowAnimation:),
@selector(reloadRowsAtIndexPaths:withRowAnimation:),
@selector(moveRowAtIndexPath:toIndexPath:)
};
for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); ++index) {
SEL originalSelector = selectors[index];
SEL swizzledSelector = NSSelectorFromString([@"fd_" stringByAppendingString:NSStringFromSelector(originalSelector)]);
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
- (void)fd_reloadData {//重寫的reload方法呀闻,替換tableView里的reload方法
if (self.fd_indexPathHeightCache.automaticallyInvalidateEnabled) {
[self.fd_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) {
[heightsBySection removeAllObjects];
}];
}
FDPrimaryCall([self fd_reloadData];);//不是遞歸調(diào)用。是調(diào)用原來的方法潜慎?
}
IndexPathHeightCache 在實(shí)現(xiàn)上需要在插入捡多、刪除cell變動(dòng)時(shí)更新高度緩存。
有種做法是:子類化uitableview,重寫相關(guān)方法铐炫,然后使用這些子類垒手。FDIndexPathHeightCache重寫了UITableView的9個(gè)觸發(fā)刷新的相關(guān)方法,并利用 runtime 的method_exchangeImplementations
函數(shù)對(duì)這9個(gè)方法做了替換驳遵,對(duì)高度緩存進(jìn)行更新淫奔。這種做法更加簡(jiǎn)單靈活。
這里在+load方法里堤结,利用 Runtime 特性把一個(gè)方法的實(shí)現(xiàn)與另一個(gè)方法的實(shí)現(xiàn)進(jìn)行替換唆迁,實(shí)現(xiàn)Method Swizzling 。
Objective C類方法load和initialize的區(qū)別
Method Swizzling 和 AOP 實(shí)踐
Objective-C Method Swizzling 的最佳實(shí)踐
//用于需要刷新數(shù)據(jù)但不想移除原有緩存數(shù)據(jù)(框架內(nèi)對(duì) reloadData 方法的處理是清空緩存)時(shí)調(diào)用竞穷,比如常見的“下拉加載更多數(shù)據(jù)”操作唐责。
- (void)fd_reloadDataWithoutInvalidateIndexPathHeightCache {
FDPrimaryCall([self fd_reloadData];);
}
用于需要刷新數(shù)據(jù)但不想移除原有緩存數(shù)據(jù)(框架內(nèi)對(duì) reloadData 方法的處理是清空緩存)時(shí)調(diào)用,比如常見的“下拉加載更多數(shù)據(jù)”操作瘾带。
2.FDKeyedHeightCache緩存策略
除了提供了indexPath作為索引的方式鼠哥,還提供了另外一個(gè) API:把數(shù)據(jù)模型的唯一標(biāo)識(shí)key用作索引
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByKey:(id<NSCopying>)key configuration:(void (^)(id cell))configuration;
FDKeyedHeightCache采用字典做緩存,沒有復(fù)雜的數(shù)組構(gòu)建、存取操作朴恳,源碼實(shí)現(xiàn)上相比于FDIndexPathHeightCache要簡(jiǎn)單得多抄罕。當(dāng)然,在刪除于颖、插入呆贿、刷新 相關(guān)的緩存操作并沒有實(shí)現(xiàn),因此需要開發(fā)者來自己完成森渐。
一般來說 cacheByIndexPath: 方法最為“傻瓜”做入,可以直接搞定所用問題。cacheByKey: 方法稍顯復(fù)雜(需要關(guān)注數(shù)據(jù)刷新)同衣,但在緩存機(jī)制上相比 cacheByIndexPath: 方法更為高效竟块。因此,像類似微博耐齐、新聞這種會(huì)擁有唯一標(biāo)識(shí)的 cell 數(shù)據(jù)模型浪秘,更建議使用cacheByKey: 方法。
如果cell高度發(fā)生變化(數(shù)據(jù)源改變)蚪缀,那么需要手動(dòng)對(duì)高度緩存進(jìn)行處理:
- (void)invalidateHeightForKey:(id<NSCopying>)key {
[self.mutableHeightsByKeyForPortrait removeObjectForKey:key];
[self.mutableHeightsByKeyForLandscape removeObjectForKey:key];
}
- (void)invalidateAllHeightCache {
[self.mutableHeightsByKeyForPortrait removeAllObjects];
[self.mutableHeightsByKeyForLandscape removeAllObjects];
}
高度獲取
- 獲取高度的過程:以indexPath為例秫逝,key的實(shí)現(xiàn)大致相同。
//FDSimulatedCacheModeCacheByIndexPath模式询枚。建立基于indexpath的高度緩存數(shù)組(空間)违帆,返回高度
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(id cell))configuration {
if (!identifier || !indexPath) {
return 0;
}
// Hit cache 已經(jīng)建立了高度緩存,命中緩存
if ([self.fd_indexPathHeightCache existsHeightAtIndexPath:indexPath]) {
//debug打印
[self fd_debugLog:[NSString stringWithFormat:@"hit cache by index path[%@:%@] - %@", @(indexPath.section), @(indexPath.row), @([self.fd_indexPathHeightCache heightForIndexPath:indexPath])]];
//返回緩存中的高度
return [self.fd_indexPathHeightCache heightForIndexPath:indexPath];
}
//還沒建立高度緩存金蜀。調(diào)用fd_heightForCellWithIdentifier: configuration: 方法計(jì)算獲得 cell 高度
CGFloat height = [self fd_heightForCellWithIdentifier:identifier configuration:configuration];//創(chuàng)建templateCell刷后,計(jì)算高度
[self.fd_indexPathHeightCache cacheHeight:height byIndexPath:indexPath];//插入緩存
[self fd_debugLog:[NSString stringWithFormat: @"cached by index path[%@:%@] - %@", @(indexPath.section), @(indexPath.row), @(height)]];
return height;
}
這里- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier configuration:(void (^)(id cell))configuration
方法對(duì)應(yīng)FDSimulatedCacheModeNone模式(沒有建立緩存)。用于創(chuàng)建渊抄、配置一個(gè)和tableview cell 布局相同的TemplateCell(模板cell)尝胆,并計(jì)算它的高度。
- 創(chuàng)建模板cell
//返回一個(gè)template Cell
- (__kindof UITableViewCell *)fd_templateCellForReuseIdentifier:(NSString *)identifier {
NSAssert(identifier.length > 0, @"Expect a valid identifier - %@", identifier);
//儲(chǔ)存單元格的字典护桦。一種identifier對(duì)應(yīng)一個(gè)templateCell
NSMutableDictionary<NSString *, UITableViewCell *> *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd);
if (!templateCellsByIdentifiers) {
templateCellsByIdentifiers = @{}.mutableCopy;
objc_setAssociatedObject(self, _cmd, templateCellsByIdentifiers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}//懶加載
UITableViewCell *templateCell = templateCellsByIdentifiers[identifier];
if (!templateCell) {
templateCell = [self dequeueReusableCellWithIdentifier:identifier];
NSAssert(templateCell != nil, @"Cell must be registered to table view for identifier - %@", identifier);
templateCell.fd_isTemplateLayoutCell = YES;//runtime關(guān)聯(lián)含衔。不過這個(gè)屬性的get方法似乎沒有被調(diào)用。使用 UITableViewCell 模板Cell計(jì)算高度二庵,通過 fd_isTemplateLayoutCell 可在Cell內(nèi)部判斷當(dāng)前是否是模板Cell贪染。可以省去一些與高度無關(guān)的操作催享。
templateCell.contentView.translatesAutoresizingMaskIntoConstraints = NO;
templateCellsByIdentifiers[identifier] = templateCell;
[self fd_debugLog:[NSString stringWithFormat:@"layout cell created - %@", identifier]];
}
return templateCell;
}
fd_isTemplateLayoutCell屬性:模板cell僅用來計(jì)算高度杭隙,通過 fd_isTemplateLayoutCell 可在Cell內(nèi)部判斷當(dāng)前是否是模板Cell。若是模板cell可以省去一些與高度計(jì)算無關(guān)的操作因妙。
- templateCell高度計(jì)算
- (CGFloat)fd_systemFittingHeightForConfiguratedCell:(UITableViewCell *)cell
中有段注釋說明算高的流程:
// If not using auto layout, you have to override "-sizeThatFits:" to provide a fitting size by yourself.
// This is the same height calculation passes used in iOS8 self-sizing cell's implementation.
//
// 1. Try "- systemLayoutSizeFittingSize:" first. (skip this step if 'fd_enforceFrameLayout' set to YES.)
// 2. Warning once if step 1 still returns 0 when using AutoLayout
// 3. Try "- sizeThatFits:" if step 1 returns 0
// 4. Use a valid height or default row height (44) if not exist one
默認(rèn)情況下是使用autoLayout的(fd_enforceFrameLayout屬性默認(rèn)為NO)痰憎,如果使用的是frameLayout則設(shè)置fd_enforceFrameLayout為YES票髓,代碼會(huì)根據(jù)你使用的layout模式來計(jì)算template Cell的高度。使用autoLayout的用systemLayoutSizeFittingSize:方法铣耘。使用frameLayout需要在自定義Cell里重寫sizeThatFit:方法洽沟。如果兩種模式都沒有使用,單元格高度設(shè)為默認(rèn)的44蜗细。
fd_enforceFrameLayout屬性不需要手動(dòng)設(shè)置:it will automatically choose a proper mode by whether you have set auto layout constrants on cell's content view.
關(guān)于UILable的問題:
當(dāng) UILabel 行數(shù)大于0時(shí)玲躯,需要指定 preferredMaxLayoutWidth 后它才知道自己什么時(shí)候該折行。這是個(gè)“雞生蛋蛋生雞”的問題鳄乏,因?yàn)?UILabel 需要知道 superview 的寬度才能折行,而 superview 的寬度還依仗著子 view 寬度的累加才能確定棘利。
框架中的做法是:先計(jì)算contentView的寬度橱野,然后對(duì)contentView添加寬度約束,然后使用systemLayoutSizeFittingSize:計(jì)算獲得高度善玫,計(jì)算完成以后移除contentView的寬度約束水援。
CGFloat contentViewWidth = CGRectGetWidth(self.frame);//先設(shè)置contentView的寬度等于tableView的寬度
// If a cell has accessory view or system accessory type, its content view's width is smaller
// than cell's by some fixed values.
//如果單元格有accessory類型或者accessory子視圖的,contentView的寬度要減去這一部分
if (cell.accessoryView) { contentViewWidth -= 16 + CGRectGetWidth(cell.accessoryView.frame);
} else {
static const CGFloat systemAccessoryWidths[] = {
[UITableViewCellAccessoryNone] = 0,
[UITableViewCellAccessoryDisclosureIndicator] = 34,
[UITableViewCellAccessoryDetailDisclosureButton] = 68,
[UITableViewCellAccessoryCheckmark] = 40,
[UITableViewCellAccessoryDetailButton] = 48
};
contentViewWidth -= systemAccessoryWidths[cell.accessoryType];
}
CGFloat fittingHeight = 0;
if (!cell.fd_enforceFrameLayout && contentViewWidth > 0) {//不使用frameLayout
// Add a hard width constraint to make dynamic content views (like labels) expand vertically instead
// of growing horizontally, in a flow-layout manner.
NSLayoutConstraint *widthFenceConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:contentViewWidth];//寬度約束
[cell.contentView addConstraint:widthFenceConstraint];
// Auto layout engine does its math
fittingHeight = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;//算高
[cell.contentView removeConstraint:widthFenceConstraint];//移除寬度約束
[self fd_debugLog:[NSString stringWithFormat:@"calculate using system fitting size (AutoLayout) - %@", @(fittingHeight)]];
}
如果使用的是frameLayout茅郎,重寫sizeThatFits:并用數(shù)據(jù)內(nèi)容來反算高度蜗元。
fittingHeight = [cell sizeThatFits:CGSizeMake(contentViewWidth, 0)].height;
- (CGSize)sizeThatFits:(CGSize)size {
CGFloat totalHeight = 0;
totalHeight += [self.titleLabel sizeThatFits:size].height;
totalHeight += [self.contentLabel sizeThatFits:size].height;
totalHeight += [self.contentImageView sizeThatFits:size].height;
totalHeight += [self.usernameLabel sizeThatFits:size].height;
totalHeight += 40; // margins
return CGSizeMake(size.width, totalHeight);
}
最后視情況而定是否需要加上分割線高度:
if (self.separatorStyle != UITableViewCellSeparatorStyleNone) {
fittingHeight += 1.0 / [UIScreen mainScreen].scale;
}
其他
__kindof :一般用在方法的返回值,返回類或者其子類都是合法的系冗。http://www.reibang.com/p/3f73e696dd4d
使用
注意的地方:
1.使用storyboard創(chuàng)建cell奕扣,要保證 contentView 內(nèi)部上下左右所有方向都有約束支撐。
2.使用代碼或 XIB 創(chuàng)建的 cell掌敬,使用以下注冊(cè)方法:
- (void)registerClass:(nullableClass)cellClassforCellReuseIdentifier:(NSString *)identifier;
- (void)registerNib:(nullableUINib *)nibforCellReuseIdentifier:(NSString *)identifier;
3.cell通過-dequeueCellForReuseIdentifier:來創(chuàng)建惯豆。
4.在-tableView:heightForRowAtIndexPath:
方法中調(diào)用cacheByIndexPath或者cacheByKey的方法完成高度緩存的創(chuàng)建和獲取。
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return [tableView fd_heightForCellWithIdentifier:@"identifer" cacheByIndexPath:indexPath configuration:^(id cell) {
// configurations
}];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
Entity *entity = self.entities[indexPath.row];
return [tableView fd_heightForCellWithIdentifier:@"identifer" cacheByKey:entity.uid configuration:^(id cell) {
// configurations
}];
}
5.不需要再設(shè)置estimatedRowHeight屬性
這里以使用autolayout的情況為例奔害,使用frameLayout的情況不作說明楷兽,以下demo數(shù)據(jù)來自原作demo。
-
使用storyboard
UITableView+FDTemplateLayoutCell框架中的demo就是使用storyboard實(shí)現(xiàn)的华临,非常簡(jiǎn)單易懂不作過多說明芯杀。下面說一些要注意的地方:
在子線程解析json數(shù)據(jù)然后再回到主線程刷新tableView。以前自己一般的做法是設(shè)置一個(gè)NSMutableArray類型屬性用來儲(chǔ)存模型數(shù)據(jù)雅潭,然后在懶加載中解析數(shù)據(jù)揭厚。
imageview 的mode設(shè)置為aspect fit,在保持長(zhǎng)寬比的前提下寻馏,縮放圖片棋弥,使得圖片在容器內(nèi)完整顯示出來。
然后imageView的右約束是一個(gè)不等于約束诚欠,intrinsic size 設(shè)為placeholder顽染。這是因?yàn)椋喝绻麅?nèi)容是運(yùn)行時(shí)決定的如UIImageView漾岳,若圖片是從服務(wù)器下載的,那么我們就需要放一個(gè)空的UIImageView粉寞,不包含所顯示的圖片尼荆,不過這樣會(huì)因未設(shè)置圖片導(dǎo)致imageView尺寸無法確定,storyboard拋出錯(cuò)誤唧垦,解決方案便是放一個(gè)臨時(shí)的占位尺寸來告訴sotryboard捅儒。
-
使用純代碼,autolayout
參照storyboard約束設(shè)置用純代碼寫約束條件振亮。
自定義cell里面的實(shí)現(xiàn):初始化的方法內(nèi)部創(chuàng)建子控件并且使用Masonry布局
initSubview方法的實(shí)現(xiàn)巧还,保證 contentView 內(nèi)部上下左右所有方向都有約束支撐:
ps:這樣子設(shè)置約束還是會(huì)有點(diǎn)問題(包括原作的例子),想想如果標(biāo)題坊秸、正文內(nèi)容麸祷、圖片或者名字其中一個(gè)子控件賦值為空,但是約束仍然存在褒搔,這種情況下應(yīng)該怎樣處理阶牍。
更新:解決辦法。
#import "HXTableViewCell.h"
#import "Masonry.h"
@interface HXTableViewCell()
@property (weak, nonatomic) UILabel *title;
@property (weak, nonatomic) UILabel *content;
@property (weak, nonatomic) UILabel *name;
@property (weak, nonatomic) UILabel *time;
@property (weak, nonatomic) UIImageView *image;
@property (nonatomic,strong) MASConstraint *contentConstraint;
@property (nonatomic,strong) MASConstraint *imgConstraint;
@property (nonatomic,strong) MASConstraint *titleConstraint;
@end
@implementation HXTableViewCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
[self initSubView];
}
return self;
}
- (void)setDatamodel:(DataModel *)datamodel{
_datamodel = datamodel;
self.title.text = datamodel.title;
self.content.text = datamodel.content;
self.name.text = datamodel.username;
self.time.text = datamodel.time;
self.image.image = datamodel.imageName.length > 0 ? [UIImage imageNamed:datamodel.imageName] : nil;
self.title.text.length == 0 ? [self.titleConstraint deactivate]:[self.titleConstraint activate];
self.content.text.length == 0 ? [self.contentConstraint deactivate]:[self.contentConstraint activate];
self.image.image == nil ? [self.imgConstraint deactivate]:[self.imgConstraint activate];
}
- (void)initSubView{
UILabel *title = [[UILabel alloc]init];
_title = title;
_title.numberOfLines = 0;//多行文字
[self.contentView addSubview:_title];
UILabel *content = [[UILabel alloc]init];
_content = content;
_content.numberOfLines = 0;//多行文字
[self.contentView addSubview:_content];
UILabel *name = [[UILabel alloc]init];
_name = name;
_name.font = [UIFont systemFontOfSize:14.0];
[self.contentView addSubview:_name];
UILabel *time = [[UILabel alloc]init];
_time = time;
_time.font = [UIFont systemFontOfSize:14.0];
[self.contentView addSubview:_time];
UIImageView *image = [[UIImageView alloc]init];
_image = image;
_image.contentMode = UIViewContentModeScaleAspectFill;
[self.contentView addSubview:_image];
int padding = 20;
__weak typeof(self) weakself = self;
[_title mas_makeConstraints:^(MASConstraintMaker *make) {
//以下設(shè)置距離contentView的邊距,設(shè)置兩條優(yōu)先度不同的約束星瘾,內(nèi)容為空時(shí)將優(yōu)先度高的約束禁用
make.top.equalTo(weakself.contentView).priorityLow();
weakself.titleConstraint = make.top.mas_equalTo(weakself.contentView).offset(20).priorityHigh();
make.left.mas_equalTo(weakself.contentView).offset(padding);
make.right.mas_equalTo(weakself.contentView.mas_right).offset(-padding);
}];
[_content mas_makeConstraints:^(MASConstraintMaker *make) {
//以下設(shè)置距離title的邊距,設(shè)置兩條優(yōu)先度不同的約束走孽,內(nèi)容為空時(shí)將優(yōu)先度高的約束禁用
make.top.equalTo(_title.mas_bottom).priorityLow();
weakself.contentConstraint = make.top.mas_equalTo(_title.mas_bottom).offset(20).priorityHigh();
make.leading.mas_equalTo(_title.mas_leading);
make.right.mas_equalTo(weakself.contentView.mas_right).offset(-padding);
}];
[_image mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(_content.mas_bottom).priorityLow();
weakself.imgConstraint = make.top.mas_equalTo(weakself.content.mas_bottom).offset(20).priorityHigh();
make.leading.mas_equalTo(_title.mas_leading);
}];
[_name mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(_title.mas_leading);
make.top.mas_equalTo(_image.mas_bottom).offset(20);
make.bottom.mas_equalTo(weakself.contentView.mas_bottom).offset(-10);
}];
[_time mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.mas_equalTo(weakself.contentView.mas_right).offset(-padding);
make.baseline.mas_equalTo(_name.mas_baseline);
}];
}
@end
控制器中的實(shí)現(xiàn):基本和原作demo中的差不多,一定要使用- registerClass:forCellReuseIdentifier:
方法注冊(cè)琳状。而且應(yīng)該像原作demo中在子線程解析json數(shù)據(jù)然后再回到主線程刷新tableView
有個(gè)奇怪的現(xiàn)象:如果vc中的數(shù)據(jù)模型是二維數(shù)組(section \row)的話只會(huì)計(jì)算磕瓷、緩存一次高度。如果是一維數(shù)組念逞,就會(huì)計(jì)算生宛、緩存兩次高度(重復(fù)兩次)。不知道為什么肮柜。代碼如下:
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.fd_debugLogEnabled = YES;
[self buildTestDataThen:^{
[self.tableView reloadData];
}];
}
- (void)buildTestDataThen:(void (^)(void))then{
// Simulate an async request
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Data from `data.json`
NSString *dataFilePath = [[NSBundle mainBundle] pathForResource:@"data" ofType:@"json"];
NSData *data = [NSData dataWithContentsOfFile:dataFilePath];
NSDictionary *rootDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
NSArray *feedDicts = rootDict[@"feed"];
// Convert to `FDFeedEntity`
NSMutableArray *entities = @[].mutableCopy;
[feedDicts enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
[entities addObject:[[DataModel alloc] initWithDictionary:obj]];
}];
self.cellData = entities;
// Callback
dispatch_async(dispatch_get_main_queue(), ^{
!then ?: then();
});
});
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.cellData count];
}
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
HXTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"FDDemo"];
[self configureCell:cell atIndexPath:indexPath];
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
return [tableView fd_heightForCellWithIdentifier:@"FDDemo" cacheByIndexPath:indexPath configuration:^(HXTableViewCell *cell) {
[self configureCell:cell atIndexPath:indexPath];
}];
}
- (void)configureCell:(HXTableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
cell.fd_enforceFrameLayout = NO;
cell.datamodel = self.cellData[indexPath.row];
}
參考文章:
UITableViewCell 自動(dòng)高度
優(yōu)化UITableViewCell高度計(jì)算的那些事
UITableView+FDTemplateLayoutCell 框架學(xué)習(xí)
UITableView-FDTemplateLayoutCell源碼分析
有了Auto Layout,為什么你還是害怕寫UITabelView的自適應(yīng)布局?
使用Autolayout實(shí)現(xiàn)UITableView的Cell動(dòng)態(tài)布局和高度動(dòng)態(tài)改變
更新:
關(guān)于UITableView+FDTemplateLayoutCell的1.2版本中利用RunLoop空閑時(shí)間執(zhí)行預(yù)緩存任務(wù)(雖然預(yù)緩存功能因?yàn)橄吕⑿碌臎_突和不明顯的收益已經(jīng)廢棄)
sunny博客原文在這一部分已經(jīng)講述得比較清楚了陷舅,這里總結(jié)一下
先來看看runloop內(nèi)部邏輯:
預(yù)緩存高度 要求頁面處于空閑狀態(tài)時(shí)才執(zhí)行高度計(jì)算,當(dāng)用戶正在滑動(dòng)列表時(shí)不應(yīng)該執(zhí)行計(jì)算任務(wù)影響滑動(dòng)體驗(yàn)审洞,需要在最無感知的時(shí)刻進(jìn)行莱睁,所以應(yīng)該同時(shí)滿足:
1.RunLoop 處于“空閑”狀態(tài)(defaultMode)
2。當(dāng)這一次 RunLoop 迭代處理完成了所有事件芒澜,馬上要休眠時(shí)
注冊(cè) RunLoopObserver 可以觀測(cè)當(dāng)前 RunLoop 的運(yùn)行狀態(tài)仰剿,每個(gè) Observer 都包含了一個(gè)回調(diào)(函數(shù)指針),當(dāng) RunLoop 的狀態(tài)發(fā)生變化時(shí)痴晦,觀察者就能通過回調(diào)接受到這個(gè)變化南吮。可以觀測(cè)的時(shí)間點(diǎn)有以下幾個(gè):
在源代碼中對(duì)應(yīng)的就是:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進(jìn)入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進(jìn)入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出Loop
};
FD框架里面做的主要兩個(gè)事情:
1.創(chuàng)建observer觀測(cè)runloop即將進(jìn)入休眠(kCFRunLoopBeforeWaiting)誊酌,
2.在observer的回調(diào)里收集部凑、分發(fā)任務(wù)(分發(fā)到多個(gè)runloop中執(zhí)行避免卡主線程)露乏。
利用performSelector這個(gè)api創(chuàng)建一個(gè) Source 0 任務(wù),分發(fā)到指定線程的 RunLoop 中涂邀,在給定的 Mode 下執(zhí)行瘟仿,若指定的 RunLoop 處于休眠狀態(tài),則喚醒它處理事件(上面圖中第七步比勉,source0任務(wù)可以喚醒runloop)
- (void)performSelector:(SEL)aSelector
onThread:(NSThread *)thr
withObject:(id)arg
waitUntilDone:(BOOL)wait
modes:(NSArray *)array;
參考:
深入理解RunLoop
Cocoa深入學(xué)習(xí):NSOperationQueue劳较、NSRunLoop和線程安全