UITableViewCell 自動高度

UITableViewCell 自動高度

iOS8

由于各種天時地利的原因(OS X EI 和 Xcode 7.1.1)導致我在 google 了各種方式之后還是只能最低運行到 iOS8掌猛,所以就先從 iOS8 開始說起吧居暖。

首先在 iOS8 開始咳焚,系統(tǒng)將 Cell 的高度計算明確的分為了兩種方式:

  1. 固定高度
  2. 自動高度

固定高度

只要一行代碼就可以很簡單的實現(xiàn) Cell 的固定高度:

tableView.rowHeight = /* fixed height */;

不能更方便了。

自動高度

在 iOS8 中,Cell 的高度計算方式默認就是『自動高度』党觅,那么怎么實現(xiàn)呢炮障?其實也很簡單:

  1. 為你的 Cell 設置了『合理的約束』
  2. 設置 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)采用的是『騎驢看唱本-走著瞧』的方式投慈。下面舉幾個例子大家體會下:

  1. bounds.size.height 為 570,而 estimatedRowHeight 為 20,總共的 cells 有 100 個伪煤,cells 的真實高度都八九不離十的是 22加袋。問,最初計算的 cell's height 有多少带族?

29個 = ceil( 570 / 20 )

  1. bounds.size.height 為 570锁荔,而 estimatedRowHeight 為 20,總共的 cells 有 100 個蝙砌,cells 的真實高度都八九不離十的是 100阳堕。問,最初計算的 cell's height 有多少择克?

仍然是 29個 = ceil( 570 / 20 )

  1. bounds.size.height 為 570恬总,而 estimatedRowHeight 為 90,總共的 cells 有 100 個肚邢,cells 的真實高度都八九不離十的是 100壹堰。問,最初計算的 cell's height 有多少骡湖?

7個 = ceil(570/90)

  1. 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 高度团南。那么我們應該如何緩存呢?有兩點很重要:

  1. 緩存的 Key 如何決定
  2. 使用什么作為 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ù)索引進行調整咬摇。

所以如果我們需要使用自己的緩存,需要這樣:

  1. 決定合適的 Cache Key
  2. 選取合適的 Cache Storage
  3. Delete/Insert 發(fā)生時調整緩存數(shù)據(jù)

所有這些還是有點麻煩的煞躬,所以大概的原理知道了就可以開始使用別人的勞動成果了 UITableView-FDTemplateLayoutCell ??

對了開頭的『合理的約束』是什么可以在這里找到 About self-satisfied cell肛鹏。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市恩沛,隨后出現(xiàn)的幾起案子在扰,更是在濱河造成了極大的恐慌,老刑警劉巖雷客,帶你破解...
    沈念sama閱讀 218,607評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件芒珠,死亡現(xiàn)場離奇詭異,居然都是意外死亡搅裙,警方通過查閱死者的電腦和手機妓局,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,239評論 3 395
  • 文/潘曉璐 我一進店門总放,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人好爬,你說我怎么就攤上這事∩模” “怎么了存炮?”我有些...
    開封第一講書人閱讀 164,960評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蜈漓。 經(jīng)常有香客問我穆桂,道長,這世上最難降的妖魔是什么融虽? 我笑而不...
    開封第一講書人閱讀 58,750評論 1 294
  • 正文 為了忘掉前任享完,我火速辦了婚禮,結果婚禮上有额,老公的妹妹穿的比我還像新娘般又。我一直安慰自己,他們只是感情好巍佑,可當我...
    茶點故事閱讀 67,764評論 6 392
  • 文/花漫 我一把揭開白布茴迁。 她就那樣靜靜地躺著,像睡著了一般萤衰。 火紅的嫁衣襯著肌膚如雪堕义。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,604評論 1 305
  • 那天脆栋,我揣著相機與錄音倦卖,去河邊找鬼。 笑死椿争,一個胖子當著我的面吹牛怕膛,可吹牛的內容都是我干的。 我是一名探鬼主播丘薛,決...
    沈念sama閱讀 40,347評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼嘉竟,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了洋侨?” 一聲冷哼從身側響起舍扰,我...
    開封第一講書人閱讀 39,253評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎希坚,沒想到半個月后边苹,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,702評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡裁僧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,893評論 3 336
  • 正文 我和宋清朗相戀三年个束,在試婚紗的時候發(fā)現(xiàn)自己被綠了慕购。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,015評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡茬底,死狀恐怖沪悲,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情阱表,我是刑警寧澤殿如,帶...
    沈念sama閱讀 35,734評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站最爬,受9級特大地震影響涉馁,放射性物質發(fā)生泄漏。R本人自食惡果不足惜爱致,卻給世界環(huán)境...
    茶點故事閱讀 41,352評論 3 330
  • 文/蒙蒙 一烤送、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧糠悯,春花似錦帮坚、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,934評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至忘朝,卻和暖如春灰署,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背局嘁。 一陣腳步聲響...
    開封第一講書人閱讀 33,052評論 1 270
  • 我被黑心中介騙來泰國打工溉箕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人悦昵。 一個月前我還...
    沈念sama閱讀 48,216評論 3 371
  • 正文 我出身青樓肴茄,卻偏偏與公主長得像,于是被迫代替她去往敵國和親但指。 傳聞我的和親對象是個殘疾皇子寡痰,可洞房花燭夜當晚...
    茶點故事閱讀 44,969評論 2 355

推薦閱讀更多精彩內容