- 在我們日常的業務中,常常伴隨大量的UITableView,然而動態地計算Cell的高度常常困擾著我。自從使用了這個元件之後,一切都變得沒那麼複雜。所以深入學習下這個框架的元件的實現原理。
- 框架地址:https://github.com/forkingdog/UITableView-FDTemplateLayoutCell
程式碼檔案目錄
1 2 3 4 5 6 7 8 |
- UITableView+FDIndexPathHeightCache.h - UITableView+FDIndexPathHeightCache.m - UITableView+FDKeyedHeightCache.h - UITableView+FDKeyedHeightCache.m - UITableView+FDTemplateLayoutCell.h - UITableView+FDTemplateLayoutCell.m - UITableView+FDTemplateLayoutCellDebug.h - UITableView+FDTemplateLayoutCellDebug.m |
首先,介紹一下這幾個類的基本功能,再層層推進,逐一分析。
1 2 3 4 |
- UITableView+FDIndexPathHeightCache,主要負責cell通過NSIndexPath進行快取高度的功能 - UITableView+FDKeyedHeightCache,主要負責cell通過key值進行快取高度的功能 - UITableView+FDTemplateLayoutCell,提供介面方法方便使用者定義cell的資料來源,以及幫助我們計算cell的高度 - UITableView+FDTemplateLayoutCellDebug,提供一些Debug列印資訊 |
關於這個框架,坦白說,從程式碼中看,作者無疑秀了一波runtime底層的功底,讓我這種小白起初一臉懵逼。自然我得換種思路來解讀這個框架,那就是從字數最少的類入手吧。
UITableView+FDTemplateLayoutCellDebug
1 2 3 4 5 6 7 8 9 |
@interface UITableView (FDTemplateLayoutCellDebug) //設定Debug模式是否開啟 @property (nonatomic, assign) BOOL fd_debugLogEnabled; //通過該方法,傳遞NSLog列印對應的Debug資訊 - (void)fd_debugLog:(NSString *)message; @end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@implementation UITableView (FDTemplateLayoutCellDebug) - (BOOL)fd_debugLogEnabled { return [objc_getAssociatedObject(self, _cmd) boolValue]; } - (void)setFd_debugLogEnabled:(BOOL)debugLogEnabled { objc_setAssociatedObject(self, @selector(fd_debugLogEnabled), @(debugLogEnabled), OBJC_ASSOCIATION_RETAIN); } - (void)fd_debugLog:(NSString *)message { if (self.fd_debugLogEnabled) { NSLog(@"** FDTemplateLayoutCell ** %@", message); } } @end |
- 在分類中,如果要宣告屬性,可以通過使用關聯度物件( AssociatedObject ), 通過objc_setAssociatedObject() 新增屬性,objc_getAssociatedObject() 獲取屬性。實際上,相當於在執行時系統中動態地在記憶體中開闢一塊空間,儲存debugLogEnabled這個BOOL變數,類似懶載入的方式,通過runtime實現setter & getter方法。
- 關於runtime的知識點,推薦這篇部落格:http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/
UITableView+FDKeyedHeightCache
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#import @interface FDKeyedHeightCache : NSObject //判斷快取中是否存在key為值的快取高度 - (BOOL)existsHeightForKey:(id)key; //對指定key的cell設定高度為height - (void)cacheHeight:(CGFloat)height byKey:(id)key; //從快取中獲取對應key的cell的高度height值 - (CGFloat)heightForKey:(id)key; //從快取中刪除指定key的cell的值 - (void)invalidateHeightForKey:(id)key; //移除快取中所有的cell的高度快取值 - (void)invalidateAllHeightCache; @end @interface UITableView (FDKeyedHeightCache) @property (nonatomic, strong, readonly) FDKeyedHeightCache *fd_keyedHeightCache; @end |
- 先來看看FDKeyedHeightCache類中宣告的屬性
1 2 3 |
@property (nonatomic, strong) NSMutableDictionary, NSNumber *> *mutableHeightsByKeyForPortrait; @property (nonatomic, strong) NSMutableDictionary, NSNumber *> *mutableHeightsByKeyForLandscape; |
不難看出,這是兩個指定泛型的可變字典。
- mutableHeightsByKeyForPortrait : 用於快取裝置豎直放置時,對應key的cell的高度值。
- mutableHeightsByKeyForLandscape : 用於快取裝置橫向放置時,對應key的cell的高度值。
- FDKeyedHeightCache中的介面方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
- (BOOL)existsHeightForKey:(id)key { NSNumber *number = self.mutableHeightsByKeyForCurrentOrientation[key]; return number && ![number isEqualToNumber:@-1]; } - (void)cacheHeight:(CGFloat)height byKey:(id)key { self.mutableHeightsByKeyForCurrentOrientation[key] = @(height); } - (CGFloat)heightForKey:(id)key { #if CGFLOAT_IS_DOUBLE return [self.mutableHeightsByKeyForCurrentOrientation[key] doubleValue]; #else return [self.mutableHeightsByKeyForCurrentOrientation[key] floatValue]; #endif } - (void)invalidateHeightForKey:(id)key { [self.mutableHeightsByKeyForPortrait removeObjectForKey:key]; [self.mutableHeightsByKeyForLandscape removeObjectForKey:key]; } - (void)invalidateAllHeightCache { [self.mutableHeightsByKeyForPortrait removeAllObjects]; [self.mutableHeightsByKeyForLandscape removeAllObjects]; } |
- 這些方法並不晦澀,看到這裡,大家不禁會問,self.mutableHeightsByKeyForCurrentOrientation從何而來,這也是我覺得這個類中,細節處理比較好的地方,由於此處考慮到快取的高度區別了裝置方向,所以框架作者,通過一個getter方法來獲取對應的存放高度的字典。
1 2 3 |
- (NSMutableDictionary, NSNumber *> *)mutableHeightsByKeyForCurrentOrientation { return UIDeviceOrientationIsPortrait([UIDevice currentDevice].orientation) ? self.mutableHeightsByKeyForPortrait: self.mutableHeightsByKeyForLandscape; } |
- 根據UIDeviceOrientationIsPortrait()函式,傳入當前裝置的放置方向([UIDevice currentDevice].orientation
)進行判斷。從而便可以通過屬性簡潔判斷需要從那個字典中取值了。
UITableView+FDIndexPathHeightCache
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
@interface FDIndexPathHeightCache : NSObject // 如果您使用索引路徑獲取高度快取,則自動啟用 @property (nonatomic, assign) BOOL automaticallyInvalidateEnabled; // Height cache - (BOOL)existsHeightAtIndexPath:(NSIndexPath *)indexPath; - (void)cacheHeight:(CGFloat)height byIndexPath:(NSIndexPath *)indexPath; - (CGFloat)heightForIndexPath:(NSIndexPath *)indexPath; - (void)invalidateHeightAtIndexPath:(NSIndexPath *)indexPath; - (void)invalidateAllHeightCache; @end @interface UITableView (FDIndexPathHeightCache) @property (nonatomic, strong, readonly) FDIndexPathHeightCache *fd_indexPathHeightCache; @end @interface UITableView (FDIndexPathHeightCacheInvalidation) /// 當你不想通過刪除快取中的高度來重新整理資料來源重新計算時,可以呼叫這個方法。 /// 該方法中用過runtime重寫了tableView中修改cell的一些方法,例如插入cell,刪除cell,移動cell,以及reloadData方法。 - (void)fd_reloadDataWithoutInvalidateIndexPathHeightCache; @end |
- 首先看看FDIndexPathHeightCache中設定的屬性
1 2 3 4 5 6 |
typedef NSMutableArray *> FDIndexPathHeightsBySection; @interface FDIndexPathHeightCache () @property (nonatomic, strong) FDIndexPathHeightsBySection *heightsBySectionForPortrait; @property (nonatomic, strong) FDIndexPathHeightsBySection *heightsBySectionForLandscape; @end |
通過前面key的高度快取分析,不難猜出這幾個屬性是幹什麼的。
- 由於通過NSIndexPath獲取高度快取,NSIndexPath對應section, 以及indexPath。FDIndexPathHeightsBySection這個陣列,通過陣列巢狀字典的資料結構來儲存,不同的section組中對應的cell的高度快取。
- FDIndexPathHeightCache中的方法
由於標頭檔案宣告的幾個介面方法,與FDKeyedHeightCache中的思路類似,就不再費口舌了,大家翻看原始碼便一目瞭然。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
- (void)enumerateAllOrientationsUsingBlock:(void (^)(FDIndexPathHeightsBySection *heightsBySection))block { block(self.heightsBySectionForPortrait); block(self.heightsBySectionForLandscape); } - (void)invalidateHeightAtIndexPath:(NSIndexPath *)indexPath { [self buildCachesAtIndexPathsIfNeeded:@[indexPath]]; [self enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection { heightsBySection[indexPath.section][indexPath.row] = @-1; }]; } - (void)invalidateAllHeightCache { [self enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) { [heightsBySection removeAllObjects]; }]; } - (void)buildCachesAtIndexPathsIfNeeded:(NSArray *)indexPaths { // Build every section array or row array which is smaller than given index path. [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { [self buildSectionsIfNeeded:indexPath.section]; [self buildRowsIfNeeded:indexPath.row inExistSection:indexPath.section]; }]; } - (void)buildSectionsIfNeeded:(NSInteger)targetSection { [self enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) { for (NSInteger section = 0; section = heightsBySection.count) { heightsBySection[section] = [NSMutableArray array]; } } }]; } - (void)buildRowsIfNeeded:(NSInteger)targetRow inExistSection:(NSInteger)section { [self enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) { NSMutableArray *heightsByRow = heightsBySection[section]; for (NSInteger row = 0; row = heightsByRow.count) { heightsByRow[row] = @-1; } } }]; } |
- 這幾個封裝的方法,主要一點就是通過block來回撥,判斷刪除NSIndexPath對應的cell高度快取。
- 在這個類中,最核心的莫過於UITableView (FDIndexPathHeightCacheInvalidation) 這個分類的實現細節,廢話少說,繼續看程式碼。
1 2 3 4 5 6 7 |
//我們只是轉發主呼叫,在崩潰報告中,最頂層的方法在堆疊可能存在FD, //但它真的不是我們的錯誤,你應該檢查你的表檢視的資料來源和 //重新載入時顯示單元格不匹配。 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) |
- 呼叫的介面方法
1 2 3 |
- (void)fd_reloadDataWithoutInvalidateIndexPathHeightCache { FDPrimaryCall([self fd_reloadData];); } |
- 這個方法,主要呼叫的是[self fd_reloadData],看到這裡的時候,我們的第一反應應該是此處通過runtime 交換了系統方法的實現。這是一種動態的攔截技巧,也算是基礎的runtime知識了,懵逼的小夥伴可以認真閱讀下前面提到的關於runtime的大牛博文。
- 既然如此,先來看看作者重寫了哪些系統的方法吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
+ (void)load { // All methods that trigger height cache's invalidation 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); } } |
- 通過method_exchangeImplementations() C函式, 將重寫的方法,一一交換成重寫的方法。
- 在這些fd_方法中的實現細節中,需要注意的一點就是,如果對應的fd_indexPathHeightCache設定了automaticallyInvalidateEnabled屬性為YES時,對應的方法對高度快取做相應的處理,重新更新fd_indexPathHeightCache中儲存的高度快取。
- 當第一次reloadData,或者cell的行數發生變化(增減行,section) ,會先在tableview不處於滾動狀態的時候非同步計算那些沒有被計算過的cell的高度,做預快取,這個想法非常贊。
- 使用者需要小心,這些呼叫是非同步的, tableview delegate有可能會在預快取計算的時候不存在了,導致程式崩潰,所以使用者在tableview需要析構的時候,在對應的tableview controller的dealloc中講self.tableview.delegate = nil;,確保delegate後續不會是一個野指標。
UITableView+FDTemplateLayoutCell
至此,我們已經分析了幾個子類的實現邏輯,唯一剩下一個分類,也是我們使用這個框架的入口 FDTemplateLayoutCell分類。全面瞭解這個元件近在咫尺。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
@interface UITableView (FDTemplateLayoutCell) /* 為給定的重用識別符號訪問內部模板佈局單元格。 * 一般來說,你不需要知道這些模板佈局單元格。 * @param identifier重用必須註冊的單元格的識別符號。 */ - (__kindof UITableViewCell *)fd_templateCellForReuseIdentifier:(NSString *)identifier; /* 返回由重用識別符號指定並配置的型別的單元格的高度, 並通過block來配置。 * 單元格將被放置在固定寬度,垂直擴充套件的基礎上,相對於其動態內容,使用自動佈局。 * 因此,這些必要的單元被設定為自適應,即其內容總是確定它的寬度給定的寬度等於tableview的寬度。 * @param identifier用於檢索和維護模板的字串識別符號cell通過系統方法 * '- dequeueReusableCellWithIdentifier:' * @param configuration用於配置和提供內容的可選塊到模板單元格。 * 配置應該是最小的滾動效能足以計算單元格的高度。 */ - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier configuration:(void (^)(id cell))configuration; /* 計算的高度將通過其索引路徑進行快取記憶體,當需要時返回快取記憶體的高度,因此,可以節省大量額外的高度計算。 * 無需擔心資料來源更改時使快取高度無效,它將在呼叫“-reloadData”或任何觸發方法時自動完成UITableView的重新載入。 * @param indexPath此單元格的高度快取所屬的位置。 */ - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(id cell))configuration; /* 此方法通過模型實體的識別符號快取高度。 * 如果你的模型改變,呼叫“-invalidateHeightForKey:(id )key”到無效快取並重新計算,它比“cacheByIndexPath”方便得多。 * @param key model entity的識別符號,其資料配置一個單元格。 */ - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByKey:(id)key configuration:(void (^)(id cell))configuration; @end @interface UITableView (FDTemplateLayoutHeaderFooterView) /* 返回在具有重用識別符號的表檢視中註冊的Header或Footer檢視的高度。 * 在呼叫“ - [UITableView registerNib / Class:forHeaderFooterViewReuseIdentifier]”之後使用它, * 與“-fd_heightForCellWithIdentifier:configuration:”相同。 * 它將呼叫“-sizeThatFits:” * UITableViewHeaderFooterView的子類不使用自動佈局。 */ - (CGFloat)fd_heightForHeaderFooterViewWithIdentifier:(NSString *)identifier configuration:(void (^)(id headerFooterView))configuration; @end @interface UITableViewCell (FDTemplateLayoutCell) /* 指示這是僅用於計算的模板佈局單元格。 * 當配置單元格時,如果有非UI的副作用,你可能需要這個。 * 類似: * - (void)configureCell:(FooCell *)cell atIndexPath:(NSIndexPath *)indexPath { * cell.entity = [self entityAtIndexPath:indexPath]; * if (!cell.fd_isTemplateLayoutCell) { * [self notifySomething]; // non-UI side effects * } * } */ @property (nonatomic, assign) BOOL fd_isTemplateLayoutCell; /* 啟用以強制此模板佈局單元格使用“框架佈局”而不是“自動佈局”, * 並且通過呼叫“-sizeThatFits:”來詢問單元格的高度,所以你必須重寫這個方法。 * 僅當要手動控制此模板佈局單元格的高度時才使用此屬性 * 計算模式,預設為NO。 */ @property (nonatomic, assign) BOOL fd_enforceFrameLayout; @end |
- 先來看看我們平時開發中最頻繁呼叫的兩個方法
- (CGFloat)fd_heightForCellWithIdentifier:(NSString )identifier cacheByIndexPath:(NSIndexPath )indexPath configuration:(void (^)(id cell))configuration;
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByKey:(id)key configuration:(void (^)(id cell))configuration;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(id cell))configuration { if (!identifier || !indexPath) { return 0; } // Hit cache if ([self.fd_indexPathHeightCache existsHeightAtIndexPath:indexPath]) { return [self.fd_indexPathHeightCache heightForIndexPath:indexPath]; } CGFloat height = [self fd_heightForCellWithIdentifier:identifier configuration:configuration]; [self.fd_indexPathHeightCache cacheHeight:height byIndexPath:indexPath]; return height; } - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByKey:(id)key configuration:(void (^)(id cell))configuration { if (!identifier || !key) { return 0; } // Hit cache if ([self.fd_keyedHeightCache existsHeightForKey:key]) { CGFloat cachedHeight = [self.fd_keyedHeightCache heightForKey:key]; return cachedHeight; } CGFloat height = [self fd_heightForCellWithIdentifier:identifier configuration:configuration]; [self.fd_keyedHeightCache cacheHeight:height byKey:key]; return height; } |
- 這兩個方法,分別是對cell通過NSIndexPath 或者 key值 進行高度快取,讀取高度的時候,先從快取cache中讀取,如果快取中沒有,在通過[self fd_heightForCellWithIdentifier:identifier configuration:configuration]方法進行計算高度並加入快取中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier configuration:(void (^)(id cell))configuration { if (!identifier) { return 0; } UITableViewCell *templateLayoutCell = [self fd_templateCellForReuseIdentifier:identifier]; //手動呼叫以確保與實際單元格的一致行為。 (顯示在螢幕上) [templateLayoutCell prepareForReuse]; if (configuration) { configuration(templateLayoutCell); } return [self fd_systemFittingHeightForConfiguratedCell:templateLayoutCell]; } |
- 通過blocks進行配置並計算cell的高度,主要通過[self fd_templateCellForReuseIdentifier:identifier]方法建立一個UITableViewCell的例項templateLayoutCell,最後再把templateLayoutCell放入[self fd_systemFittingHeightForConfiguratedCell:templateLayoutCell]中進行計算返回高度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
- (__kindof UITableViewCell *)fd_templateCellForReuseIdentifier:(NSString *)identifier { NSAssert(identifier.length > 0, @"Expect a valid identifier - %@", identifier); NSMutableDictionary *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; templateCell.contentView.translatesAutoresizingMaskIntoConstraints = NO; templateCellsByIdentifiers[identifier] = templateCell; [self fd_debugLog:[NSString stringWithFormat:@"layout cell created - %@", identifier]]; } return templateCell; } |
- 將所有建立的templateCell放置在一個字典templateCellsByIdentifiers中,並通過runtime將其加入記憶體中作為屬性,實際上,templateCell 也是通過identifier在複用佇列中獲取複用的。所以,cell在使用前,應先註冊為cell的複用物件。
- 最後呼叫的[self fd_systemFittingHeightForConfiguratedCell:templateLayoutCell]進行高度計算。當然也是最關鍵的一個操作,既然這是一個高度計算的框架,那麼計算的步驟當然是重中之重。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
- (CGFloat)fd_systemFittingHeightForConfiguratedCell:(UITableViewCell *)cell { CGFloat contentViewWidth = CGRectGetWidth(self.frame); if (cell.accessoryView) { //如果有定製accessoryView,則去除這個寬度 contentViewWidth -= 16 + CGRectGetWidth(cell.accessoryView.frame); } else { //如果有系統accessoryView展示,則去除對應的寬度。 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) { //如果是自動佈局,則將contentView的寬度約束新增進去。 //這樣做的目的是讓UILabel之類的內容可能多行的控制元件適應這個寬度折行(當然前提是我們已經設定好了這些控制元件的佈局約束)。然後呼叫systemLayoutSizeFittingSize來計算高度。 //最後移除剛才臨時新增的contentView寬度約束。 NSLayoutConstraint *widthFenceConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:contentViewWidth]; [cell.contentView addConstraint:widthFenceConstraint]; fittingHeight = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height; [cell.contentView removeConstraint:widthFenceConstraint]; } if (fittingHeight == 0) { // 嘗試呼叫 '- sizeThatFits:'進行高度計算. // 注意:配件高度不應包括分隔線檢視高度。 fittingHeight = [cell sizeThatFits:CGSizeMake(contentViewWidth, 0)].height; } // 進行完前面的邏輯後高度如果仍然為0,則使用預設行高44 if (fittingHeight == 0) { fittingHeight = 44; } // 新增一畫素作為tableView分割線高度。 if (self.separatorStyle != UITableViewCellSeparatorStyleNone) { fittingHeight += 1.0 / [UIScreen mainScreen].scale; } return fittingHeight; } |
至此,就大致將這個框架分析的差不多了,原始碼中,對類的例項化均為採用runtime新增AssociatedObject的方式。就不做解釋了。
最後
- 賞花不忘種花人,把作者關於這個框架優秀的效能分析複習下。
- 地址:http://blog.sunnyxx.com/2015/05/17/cell-height-calculation/