本文是投稿文章,原文
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的更多知識我們後續再談。