你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 回頭看之UITableView

回頭看之UITableView

編輯:IOS開發基礎

47.png

本文是投稿文章,原文

UITableVIew是iOS開發中最常見的視圖中最經典的視圖了,沒有之一,相信對這個視圖敢稱精通的人開發個好應用應該是問題不大的。

閒話少敘,進入正題。

怎麼使用

掌握兩個代理

1.UITableViewDelegate

@optional  
//下文再提到該方法用heightForRow代替
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;

2.UITableViewDataSource

@required
//下文再提到該方法用numberOfRowsInSection代替
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
//下文再提到該方法用cellForRow代替
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;

@optional
//下文再提到該方法用numberOfSection代替
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView

要想比較完整的展示你的數據,這四個方法是最經常被實現的。

調用過程大體是這樣的:tableView會先詢問代理(在一般MVC裡大部分是當前視圖控制器ViewController)要展示多少個section,就是調用numberOfSections,如果代理沒有實現該方法默認就是1個section。然後tableView調用numberOfRowsInSection先詢問第一個section有多少個cell,然後挨個執行heightForRow獲取每個cell的高度。tableView對每個section都執行一遍這樣的操作後,那麼結果來了:tableView通過對這些cell高度的累加就知道了需要多大的空間才能安放得了所有的內容,於是它調整好了contentSize的值。這樣走下來就為我們後續在滑動時能通過scrollIndicator觀察到我們大體滑到了哪個位置做好了准備。

准備好空間之後接下來的任務就是准備內容了。當然大家都知道真正的內容是依附在UITableViewCell上的,tableView先調用cellForRow去獲取代理返回給它的第一個cell,對於所有的cell來說width都是固定的,即tableView本身的寬度,對於第一個cell來說它的origin也是確定的,即(0,0),也就是說要想確定這個cell的位置就只需要知道它的height了。於是tableView再去調用heightForRow去獲取它的高度,這樣一個視圖能確定顯示在屏幕什麼位置的充要條件就具備了。剩下的cell同理,挨個放在上一個cell的下邊就行了。

總結一下:

  • 調用numberOfSection獲得 A個 section

  • 先調用numberOfRowsInSection獲得B個cell,再調用heightForRowB次。如此循環A次

  • 循環調用cellForRow和heightForRow,直到cell的個數充滿當前屏幕。

這就是一個普通的tableView一開始加載數據的過程,有幾點需要說明:

  • 如果你展示在每個cell上的內容是相對固定的,准確點說是每個cell的高度是固定的,那麼heightForRow是不建議讓代理去實現的,而是通過tableView的rowHeight屬性來代替,當數據量比較大,比如說有10000個(其實只要 >= 2)cell時,tableView只需要10000*rowHeight就知道應該准備的空間大小了,而不是調用一個方法10000次通過累加獲知需要的大小。而且你懂的,要想獲取一個cell的高度並不是那麼容易的事,尤其是在自動布局出現之前,你需要計算各種字符串的所占空間的大小,這對性能是相當大的損耗。

  • 如果每個cell高度確實不一樣,數據量又很大時該怎麼解決這個性能問題呢,iOS7之後系統提供了估算高度的辦法,estimatedRowHeight和- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath//下文再提到該方法用estimateHeightForRow代替,這樣每次在加載數據之前,tableView不再通過heightForRow消耗大量的性能獲取空間大小了,而是通過在estimateRowHeight或者estimatedHeightForRow不需要費勁計算就能獲取的一個估算值來獲取一個大體的空間大小,等到真正的加載數據時才根據獲取真實數據,並做出相應的調整,比如contentSize或者scrollIndicator的位置。關於動態計算高度,推薦羊教授的一篇文章優化UITableViewCell高度計算的那些事

  • 這些方法的調用在保證大順序不變的情況下,每個方法的調用次數是不一定的,每個iOS版本又不一樣,你如果想知道可以動手去試驗一下。尤其是在iOS8,它認為cell會隨時變化,所以一滑動就重新計算cell的高度。

  • 這些方法的調用其實也是有插曲的,比如調用了reloadData之後,tableView只會調用能讓它知道所需空間大小的代理方法,然後立馬執行reloadData之後的語句,也就說cellForRow並不會在reloadData之後緊接著執行。所以reloadData之後盡量避免對數據源數組的操作。

復用機制

了解UITableView的人肯定對這一著名特性多少有點了解。咱們先假設UITableView沒有復用機制,那麼我們要展示10000條數據的話,那就得生成10000個UITableViewCell,占用了大量內存不說,性能也可想而知了,必然是一滑一卡頓,一頓一暴怒啊,控制力弱的估計要摔手機了。

復用機制大體是這樣:UITableView首先加載一屏幕(假設UITableView的大小是整個屏幕的大小)所需要的UITableViewCell,具體個數要根據每個cell的高度而定,總之肯定要鋪滿整個屏幕,更准確說當前加載的cell的高度要大於屏幕高度。然後你往上滑動,想要查看更多的內容,那麼肯定需要一個新的cell放在已經存在內容的下邊。這時候先不去生成,而是先去UITableView自己的一個資源池裡去獲取。這個資源池裡放了已經生成的而且能用的cell。如果資源池是空的話才會主動生成一個新的cell。那麼這個資源池裡的cell又來自哪裡呢?當你滑動時視圖是,位於最頂部的cell會相應的往上滑動,直到它徹底消失在屏幕上,消失的cell去了哪裡呢?你肯定想到了,是的,它被UITableView放到資源池裡了。其他cell也是這樣,只要一滑出屏幕就放入資源池。這樣,有進有出,總共需要大約一屏幕多一點的cell就夠了。相對於1000來說節省的資源就是指數級啊,完美解決了性能問題。

iOS6之後我們一般在代碼裡這樣處理cell

先注冊

[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"UITableViewCell"];

[self.tableView registerNib:[UINib nibWithNibName:@"NibTableViewCell" bundle:nil] forCellReuseIdentifier:@"NibTableViewCell"];

在代理方法裡獲取

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
   UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"UITableViewCell" forIndexPath:indexPath];
   // do something
   return cell;
}

那麼具體在代碼裡是怎麼實現的呢?我們可以大膽的猜測一下。

UITableView有幾個屬性(假想的):

NSMutableDictionary *registerCellInfo;
NSMutableDictionary *reusableCellsDictionary;
NSMutableArray *visibleCells;

我們推測兩個注冊方法的實現

- (void)registerNib:(UINib *)nib forCellReuseIdentifier:(NSString *)identifier{
     [self.registerCellInfo setObject:nib forKey:identifier];
     [self.registerCellsDictionary setObject:[NSMutableArray array] forKey:identifier];
}
- (void)registerClass:(Class)cellClass forCellReuseIdentifier:(NSString  *)identifier{
    [self.registerCellInfo setObject:cellClass forKey:identifier];
    [self.registerCellsDictionary setObject:[NSMutableArray array] forKey:identifier];
}

然後推測最關鍵的獲取方法

- (UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath{
    //indexPath這個參數是為了重置`cell`的大小,相關的處理並不是本文的重點,所以暫不實現
    NSMutableArray *array = self.reusableCellsDictionary[identifier];
    UITableViewCell *cell = nil;
    if(array.count){
        cell = array.lastObject;
        [self.visibleCells addObject:cell];
        [array removeLastObject];
        
    }else{
        id obj = self.registerCellInfo[identifier];
        if([obj isKindOfClass:[UINib class]]){
            cell = [[((UINib *)obj) instantiateWithOwner:nil options:nil] lastObject];
        }else{
            cell = [[(Class)obj alloc] init];
        }
        if(cell){
            [self.visibleCells addObject:cell];
         }
    }
    
    return cell;
}

請忽略以上所有推測方法的不嚴謹,許多該有的條件判斷並沒有去處理。但是寫到這裡相信親愛的讀者已經了解了UITableView復用機制的原理了。現在,你已經具備了自己動手寫一個UITableView的基礎了(當然,假設你已經對UIScrollView有了充足的了解)。如果我的文章對你有用,煩請點個喜歡,好激勵我繼續寫下去。

關於UITableView的更多知識我們後續再談。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved