UITableViewCell 自動高度
iOS8
由于各種天時地利的原因(OS X EI 和 Xcode 7.1.1)導致我在 google 了各種方式之后還是只能最低運行到 iOS8掌猛,所以就先從 iOS8 開始說起吧居暖。
首先在 iOS8 開始咳焚,系統(tǒng)將 Cell 的高度計算明確的分為了兩種方式:
- 固定高度
- 自動高度
固定高度
只要一行代碼就可以很簡單的實現(xiàn) Cell 的固定高度:
tableView.rowHeight = /* fixed height */;
不能更方便了。
自動高度
在 iOS8 中,Cell 的高度計算方式默認就是『自動高度』党觅,那么怎么實現(xiàn)呢炮障?其實也很簡單:
- 為你的 Cell 設置了『合理的約束』
- 設置 TableView 的
estimatedRowHeight
屬性
iOS8 中 tableView.rowHeight
的默認值就是 UITableViewAutomaticDimension
,所以不必設置了伊者。
contentSize
眾所周知 UITableView
繼承于 UIScrollView
英遭,那么 UITableView
就需要設置 contentSize
值双肤。那么 UITableView
如何知道 contentSize
的值呢妄田?
固定高度
如果 TableView 中的 Cell 采用的是固定高度婆硬,那么 contentSize
的高度很明顯就是 fixedHeight × cellCount
稠项。
自動高度
當采用了自動高度的話创淡,那么系統(tǒng)會分別調用 Cell 上的 systemLayoutSizeFittingSize
的方法粥谬,這個方法會根據(jù)你為 Cell 設置的約束計算出 Cell 的尺寸宙地,那么 contentSize
就會變成 dynamicallyCalculatedCellSize × cellCount
送漠。
estimatedRowHeight
估算高度的作用是很大的搂蜓,上面說到當你采用了『自動高度』的計算方式狼荞,那么系統(tǒng)為了知道 contentSize
,別無他法的在每個 Cell 上調用實例方法計算其尺寸帮碰,當你有若干的 Cell 時相味,就會引發(fā)性能問題。
為了上述的問題殉挽,系統(tǒng)在 TableView 上提供了 estimatedRowHeight
參數(shù)丰涉。那么我們可以看看這個『估算行高』是怎么起作用的。
首先 TableView 是可以知道自身的 bounds 的斯碌,那么就以 bounds 為基準一死,至少獲取能填滿 bounds 的 Cells 的尺寸,對于其余的 Cells 尺寸傻唾,系統(tǒng)采用的是『騎驢看唱本-走著瞧』的方式投慈。下面舉幾個例子大家體會下:
- bounds.size.height 為 570,而 estimatedRowHeight 為 20,總共的 cells 有 100 個伪煤,cells 的真實高度都八九不離十的是 22加袋。問,最初計算的 cell's height 有多少带族?
29個 = ceil( 570 / 20 )
- bounds.size.height 為 570锁荔,而 estimatedRowHeight 為 20,總共的 cells 有 100 個蝙砌,cells 的真實高度都八九不離十的是 100阳堕。問,最初計算的 cell's height 有多少择克?
仍然是 29個 = ceil( 570 / 20 )
- bounds.size.height 為 570恬总,而 estimatedRowHeight 為 90,總共的 cells 有 100 個肚邢,cells 的真實高度都八九不離十的是 100壹堰。問,最初計算的 cell's height 有多少骡湖?
7個 = ceil(570/90)
- bounds.size.height 為 570贱纠,而 estimatedRowHeight 為 1000,總共的 cells 有 100 個响蕴,cells 的真實高度都八九不離十的是 100谆焊。問,最初計算的 cell's height 有多少浦夷?
6 個辖试。因為 estimatedRowHeight 為 1000 那么系統(tǒng)通過計算
ceil(570 / 1000)
得出需要動態(tài)計算一個 Cell 的大小∨可是問題來了罐孝,計算了 Cell 的實際高度發(fā)現(xiàn)只有 100,于是為了填滿 bounds肥缔,必須繼續(xù)計算接下來的 Cell莲兢,于是計算到填滿了就不再計算。
所以對于 estimatedRowHeight
的值续膳,可以設置得和所有 Cell 的平均值一樣怒见,也可以設置得很大,比 TableView 的 bounds 還要大姑宽,當然前者是比較好確定的。
那么小結下 estimatedRowHeight
的作用闺阱,就是為了加速 TableView 獲取自身的 contentSize
的操作炮车,這樣盡快的將數(shù)據(jù)顯示出來,然后其余的 Cells 尺寸在滑動的時候在計算。
問題及優(yōu)化
在 iOS8 中使用了 Cell 自動高度之后瘦穆,你會發(fā)現(xiàn)纪隙,只要一個 Cell 需要被顯示到屏幕上,它的高度都會被計算一次扛或,即使這個 Cell 在之前的滑動中已經(jīng)被計算過高度了绵咱。之所以被設計成這樣的原因系統(tǒng)認為 Cell 的高度是隨時可能改變的,比如在設置中改變了字體大形跬谩:
如果在 iOS7 中使用了自動高度悲伶,你就會發(fā)現(xiàn)一旦 Cell 在之前被計算過高度,那么它下一次滑動出來時就不會被計算高度了住涉。這是因為從 iOS7 開始麸锉,iOS7 中引入了 Dynamic Type 的功能,這個功能使得用戶可以調整應用中字體的大小舆声,而 iOS7 中的所有系統(tǒng)應用都適配了這個功能需求花沉。但是從 iOS8 開始,Apple 希望所有的應用都可以適配這個功能需求媳握,于是就取消了 Cell 在自動算高時的高度緩存碱屁。
于是如你所見,在 iOS8 中由于沒有了自動的高度緩存蛾找,那么在使用自動高度時娩脾,Cell 的高度會被多次計算,這樣就會導致滑動不流暢腋粥。其實這不是大的問題晦雨,Apple 為了把 Cell 的高度計算變得更靈活,使得是否動態(tài)計算高度 or 使用緩存已計算的高度的工作放到了開發(fā)者這邊隘冲,還是很符合設計模式的闹瞧,只不過開發(fā)者使用有些麻煩了。
優(yōu)化的方式其實說起來也是很簡單的展辞,就是對于已經(jīng)計算了高度的 Cell奥邮,只要確信它的高度是不會再變化的,那么就將這個高度緩存起來罗珍,下回在系統(tǒng)向你所要 Cell 高度時(heightForRowAtIndexPath
)洽腺,返回那個之間計算過的高度緩存就行了。
iOS7
其實 iOS7 中使用 Cell 自動高度沒有什么好討論的了覆旱,系統(tǒng)會自動的為我們緩存已經(jīng)計算過的 Cell 高度蘸朋。唯一要注意的是在 iOS7 中需要顯式的設置:
tableView.rowHeight = UITableViewAutomaticDimension;
其他還是和在 iOS8 中一樣的:你為 Cell 設置了『合理的約束』,讓 TabaleView 使用自動 Cell 高度計算扣唱,剩下的系統(tǒng)就為了做了藕坯。
iOS6
完全沒接觸過不清楚
怎么緩存
上面已經(jīng)說了在 iOS8 中我們需要自己決定是否緩存那些已經(jīng)計算過的 Cell 高度团南。那么我們應該如何緩存呢?有兩點很重要:
- 緩存的 Key 如何決定
- 使用什么作為 Cache Storage
Key
因為需要通過 Key 去取回 Cell 已經(jīng)計算過的 Height炼彪,那么 Key 需要可以標識出各個 Cell吐根。我們可以選取既可以標識 Cells 又可以區(qū)別它們之間不同的屬性來作為 Key。對于一個 Objc 對象辐马,它們之間最顯著的不同肯定是它們的 memory address 了拷橘,而且需要獲取 Objc 對象的內存地址也很簡單:
NSString *temp = @"123";
uintptr_t ptrAddress = (uintptr_t) temp;
但是,請回憶我們在使用 TableView 和 Cell 時常用的方法:
- dequeueReusableCellWithIdentifier:forIndexPath:
- dequeueReusableCellWithIdentifier:
就是它倆使得 TableView 中 Cells 都是 Reused喜爷。所以通過 memory address 的方式是不行了冗疮。剩下的唯一可用的方式就是 indexPath
了 ??。
Cache Storage
選什么作為 Cache Storage 呢贞奋?可用的有:
NSMutableArray
NSCache
-
objc_setAssociatedObject
赌厅。
先看看它們之間讀寫性能的差別,主要代碼來自這兒轿塔,我加上了 NSMutableArray
部分:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
void logTimeSpentExecutingBlock(dispatch_block_t block, NSString* label)
{
NSTimeInterval then = CFAbsoluteTimeGetCurrent();
block();
NSTimeInterval now = CFAbsoluteTimeGetCurrent();
NSLog(@"Spent %.5f seconds on %@", now - then, label);
}
@interface Test : NSObject {
@public
NSString* ivar;
}
@property (nonatomic, strong) NSString* ordinary;
@end
@interface Test (Runtime)
@property (nonatomic, strong) NSString* runtime;
@end
@implementation Test
- (void)setOrdinary:(NSString*)ordinary
{
// the default implementation checks if the ivar is already equal
_ordinary = ordinary;
}
@end
@implementation Test (Runtime)
- (NSString*)runtime
{
return objc_getAssociatedObject(self, @selector(runtime));
}
- (void)setRuntime:(NSString*)string
{
objc_setAssociatedObject(self, @selector(runtime), string, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
int main(int argc, const char* argv[])
{
@autoreleasepool
{
Test* test = [Test new];
int iterations = 1000000;
NSCache* cache = [[NSCache alloc] init];
NSMutableArray* arr = [[NSMutableArray alloc] init];
logTimeSpentExecutingBlock(^{
for (int i = 0; i < iterations; i++) {
test->ivar = @"foo";
}
}, @"writing ivar");
logTimeSpentExecutingBlock(^{
for (int i = 0; i < iterations; i++) {
test->ivar;
}
}, @"reading ivar");
logTimeSpentExecutingBlock(^{
for (int i = 0; i < iterations; i++) {
test.ordinary = @"foo";
}
}, @"writing ordinary");
logTimeSpentExecutingBlock(^{
for (int i = 0; i < iterations; i++) {
[test ordinary];
}
}, @"reading ordinary");
logTimeSpentExecutingBlock(^{
for (int i = 0; i < iterations; i++) {
test.runtime = @"foo";
}
}, @"writing runtime");
logTimeSpentExecutingBlock(^{
for (int i = 0; i < iterations; i++) {
[test runtime];
}
}, @"reading runtime");
logTimeSpentExecutingBlock(^{
for (int i = 0; i < iterations; i++) {
[cache setObject:@"1" forKey:@(i)];
}
}, @"writing NSCache");
logTimeSpentExecutingBlock(^{
for (int i = 0; i < iterations; i++) {
[cache objectForKey:@(i)];
}
}, @"reading NSCache");
logTimeSpentExecutingBlock(^{
for (int i = 0; i < iterations; i++) {
[arr addObject:@"1"];
}
}, @"writing NSMutableArray");
logTimeSpentExecutingBlock(^{
for (int i = 0; i < iterations; i++) {
arr[i];
}
}, @"reading NSMutableArray");
}
return 0;
}
輸出的結果:
Spent 0.00408 seconds on writing ivar
Spent 0.00177 seconds on reading ivar
Spent 0.02587 seconds on writing ordinary
Spent 0.01329 seconds on reading ordinary
Spent 0.06314 seconds on writing runtime
Spent 0.04348 seconds on reading runtime
Spent 1.26897 seconds on writing NSCache
Spent 0.29358 seconds on reading NSCache
Spent 0.02913 seconds on writing NSMutableArray
Spent 0.01621 seconds on reading NSMutableArray
好了可以淘汰 NSCache
了特愿。看到 objc_set/get
性能和 NSMutableArray
是差不多的勾缭,那么選擇哪一個呢揍障?
其實這是要根據(jù)我們的業(yè)務需求的,對于存 Height 它倆都可以完成俩由,但是我們知道 Cells 是需要可以 Delete/Insert
的毒嫡,那么問題來了,如果有了 Delete/Insert
操作幻梯,而我們的 Key 是根據(jù)的 indexPath
兜畸,那么緩存中的 Key 就『不準』了,需要進行相應的調整碘梢。而使用 NSMutableArray
當你 Delete/Insert
時它會自動的為我們將操作索引的后續(xù)索引進行調整咬摇。
所以如果我們需要使用自己的緩存,需要這樣:
- 決定合適的 Cache Key
- 選取合適的 Cache Storage
- 在
Delete/Insert
發(fā)生時調整緩存數(shù)據(jù)
所有這些還是有點麻煩的煞躬,所以大概的原理知道了就可以開始使用別人的勞動成果了 UITableView-FDTemplateLayoutCell ??
對了開頭的『合理的約束』是什么可以在這里找到 About self-satisfied cell肛鹏。