UITableViewCell 自動高度
UITableViewCell 自動高度
iOS8
由於各種天時地利的原因(OS X EI 和 Xcode 7.1.1)導致我在 google 了各種方式之後還是隻能最低執行到 iOS8,所以就先從 iOS8 開始說起吧。
首先在 iOS8 開始,系統將 Cell 的高度計算明確的分為了兩種方式:
- 固定高度
- 自動高度
固定高度
只要一行程式碼就可以很簡單的實現 Cell 的固定高度:
tableView.rowHeight = /* fixed height */;
不能更方便了。
自動高度
在 iOS8 中,Cell 的高度計算方式預設就是『自動高度』,那麼怎麼實現呢?其實也很簡單:
- 為你的 Cell 設定了『合理的約束』
- 設定 TableView 的
estimatedRowHeight
屬性
iOS8 中 tableView.rowHeight
的預設值就是 UITableViewAutomaticDimension
,所以不必設定了。
contentSize
眾所周知 UITableView
繼承於 UIScrollView
,那麼 UITableView
就需要設定 contentSize
值。那麼 UITableView
如何知道 contentSize
的值呢?
固定高度
如果 TableView 中的 Cell 採用的是固定高度,那麼 contentSize
的高度很明顯就是 fixedHeight × cellCount
。
自動高度
當採用了自動高度的話,那麼系統會分別呼叫 Cell 上的 systemLayoutSizeFittingSize
的方法,這個方法會根據你為 Cell 設定的約束計算出 Cell 的尺寸,那麼 contentSize
就會變成 dynamicallyCalculatedCellSize × cellCount
。
estimatedRowHeight
估算高度的作用是很大的,上面說到當你採用了『自動高度』的計算方式,那麼系統為了知道 contentSize
,別無他法的在每個 Cell 上呼叫例項方法計算其尺寸,當你有若干的 Cell 時,就會引發效能問題。
為了上述的問題,系統在 TableView 上提供了 estimatedRowHeight
引數。那麼我們可以看看這個『估算行高』是怎麼起作用的。
首先 TableView 是可以知道自身的 bounds 的,那麼就以 bounds 為基準,至少獲取能填滿 bounds 的 Cells 的尺寸,對於其餘的 Cells 尺寸,系統採用的是『騎驢看唱本-走著瞧』的方式。下面舉幾個例子大家體會下:
- 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 那麼系統通過計算
ceil(570 / 1000)
得出需要動態計算一個 Cell 的大小。可是問題來了,計算了 Cell 的實際高度發現只有 100,於是為了填滿 bounds,必須繼續計算接下來的 Cell,於是計算到填滿了就不再計算。
所以對於 estimatedRowHeight
的值,可以設定得和所有 Cell 的平均值一樣,也可以設定得很大,比 TableView 的 bounds 還要大,當然前者是比較好確定的。
那麼小結下 estimatedRowHeight
的作用,就是為了加速 TableView 獲取自身的 contentSize
的操作,這樣儘快的將資料顯示出來,然後其餘的 Cells 尺寸在滑動的時候在計算。
問題及優化
在 iOS8 中使用了 Cell 自動高度之後,你會發現,只要一個 Cell 需要被顯示到螢幕上,它的高度都會被計算一次,即使這個 Cell 在之前的滑動中已經被計算過高度了。之所以被設計成這樣的原因系統認為 Cell 的高度是隨時可能改變的,比如在設定中改變了字型大小:
如果在 iOS7 中使用了自動高度,你就會發現一旦 Cell 在之前被計算過高度,那麼它下一次滑動出來時就不會被計算高度了。這是因為從 iOS7 開始,iOS7 中引入了 Dynamic Type 的功能,這個功能使得使用者可以調整應用中字型的大小,而 iOS7 中的所有系統應用都適配了這個功能需求。但是從 iOS8 開始,Apple 希望所有的應用都可以適配這個功能需求,於是就取消了 Cell 在自動算高時的高度快取。
於是如你所見,在 iOS8 中由於沒有了自動的高度快取,那麼在使用自動高度時,Cell 的高度會被多次計算,這樣就會導致滑動不流暢。其實這不是大的問題,Apple 為了把 Cell 的高度計算變得更靈活,使得是否動態計算高度 or 使用快取已計算的高度的工作放到了開發者這邊,還是很符合設計模式的,只不過開發者使用有些麻煩了。
優化的方式其實說起來也是很簡單的,就是對於已經計算了高度的 Cell,只要確信它的高度是不會再變化的,那麼就將這個高度快取起來,下回在系統向你所要 Cell 高度時(heightForRowAtIndexPath
),返回那個之間計算過的高度快取就行了。
iOS7
其實 iOS7 中使用 Cell 自動高度沒有什麼好討論的了,系統會自動的為我們快取已經計算過的 Cell 高度。唯一要注意的是在 iOS7 中需要顯式的設定:
tableView.rowHeight = UITableViewAutomaticDimension;
其他還是和在 iOS8 中一樣的:你為 Cell 設定了『合理的約束』,讓 TabaleView 使用自動 Cell 高度計算,剩下的系統就為了做了。
iOS6
完全沒接觸過不清楚
怎麼快取
上面已經說了在 iOS8 中我們需要自己決定是否快取那些已經計算過的 Cell 高度。那麼我們應該如何快取呢?有兩點很重要:
- 快取的 Key 如何決定
- 使用什麼作為 Cache Storage
Key
因為需要通過 Key 去取回 Cell 已經計算過的 Height,那麼 Key 需要可以標識出各個 Cell。我們可以選取既可以標識 Cells 又可以區別它們之間不同的屬性來作為 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
是差不多的,那麼選擇哪一個呢?
其實這是要根據我們的業務需求的,對於存 Height 它倆都可以完成,但是我們知道 Cells 是需要可以 Delete/Insert
的,那麼問題來了,如果有了 Delete/Insert
操作,而我們的 Key 是根據的 indexPath
,那麼快取中的 Key 就『不準』了,需要進行相應的調整。而使用 NSMutableArray
當你 Delete/Insert
時它會自動的為我們將操作索引的後續索引進行調整。
所以如果我們需要使用自己的快取,需要這樣:
- 決定合適的 Cache Key
- 選取合適的 Cache Storage
- 在
Delete/Insert
發生時調整快取資料
所有這些還是有點麻煩的,所以大概的原理知道了就可以開始使用別人的勞動成果了 UITableView-FDTemplateLayoutCell ?
對了開頭的『合理的約束』是什麼可以在這裡找到 About self-satisfied cell。
相關文章
- iOS AutoLayout進階(五)UITableViewCell自動高度iOSUIView
- UITableViewCell自適應圖片高度UIView
- 巢狀UITextView的UITableViewCell高度自適應巢狀UITextView
- UITableViewCell的高度快取UIView快取
- UITableViewCell含有WebView的自適應高度新解決方案UIWebView
- UITableViewCell使用自動佈局的“最佳實踐”UIView
- 自動載入的iframe高度自適應
- 如何實現高度自動化測試?
- UITableView 自動計算 cell 高度並快取,再也不用管高度啦UIView快取
- iOS11 UITableViewCell滑動事件改動iOSUIView事件
- 根據螢幕高度自適應元素高度
- 67,表格中單元格td自適應高度,最大高度後滾動條顯示
- li浮動時ul高度為0,解決ul自適應高度的幾種方法
- 直播帶貨原始碼,評論框自動控制高度原始碼
- iOS UITableViewCell允許滑動的處理iOSUIView
- html iframe高度自適應HTML
- TabelViewCell高度自適應View
- 小程式Swiper高度自適應
- textarea高度自適應詳解
- iframe 跨域高度自適應跨域
- jQuery textarea框高度自適應jQuery
- iOS初級開發學習筆記:一個頁面中自動計算cell的高度來自適應tableView的高度iOS筆記View
- iOS 精準獲取webView內容高度並自適應高度iOSWebView
- 自定義一個可以動態摺疊的UITAbleViewCellUIView
- UITableViewCell重用機制UIView
- 大陸集團推出全新電子制動方案,全力保障高度自動駕駛安全自動駕駛
- iframe自適應高度的外掛
- 微信小程式Swiper高度自適應微信小程式
- iframe高度自適應解決方案
- iOS cell中webview自適應高度iOSWebView
- jquery 實現iframe 自適應高度jQuery
- 微信小程式swiper高度自適應,swiper的子元素高度不固定微信小程式
- 監聽div滾動高度
- 三種方法解決浮動元素父容器高度自適應問題
- 父DIV的高度不能根據子DIV自動變化的解決
- iOS 【終極方案】精準獲取webView內容高度,自適應高度iOSWebView
- iOS【終極方案】精準獲取webView內容高度,自適應高度iOSWebView
- Widget小元件如何自適應高度元件