平時比較忙,十一閒下來終於有時間寫點東西,這篇文章記錄對tableView的一些思考。提到tableView相信大家都非常熟悉,它是我們開發中最常見的控件之一,繼承自scrollView( UIScrollView的底層實現看這裡 )。它是個非常神奇的控件,仿佛有無窮無盡的子控件,在它之上可以顯示成千上萬行cell,卻不會導致內存飙升,界面卡頓。但如果tableView真的創建了成千上萬個cell,就可能導致各種問題。它是如何做到盛放成千上萬的子控件而不卡頓、內存不爆表?
相信大家都知道它的核心在於使用了重用機制,但是它是如何實現的?相信大部分人還是理解的不太清楚、不夠深刻。下面我將帶大家一起實現一個簡易的tableView,重點放在 重用機制 的實現。讀完這篇文章相信大家能對tableView有一個更加深刻的認識。
cell的重用,使用享元模式。下邊帶領大家一步步實現重用機制,由於本人能力有限,我盡量用簡短的語言寫的通俗易懂,如果您覺得寫的不好,也請不要噴我。
首先tableView肯定繼承自UIScrollView,在UIScrollView滑動的時候我們需要不停的檢查是否有新的cell進入界面需要顯示,舊的cell離開界面需要移除。這一步我們可以通過重寫layoutSubviews或者setContentOffset方法來實現,然後在此方法中首先我們需要計算當下要顯示第幾行到第幾行的cell,然後拿到需要顯示的cell放在界面,最後移除離開屏幕的cell。下面我們來一步一步實現。
計算需要顯示第幾行:一個全局的數組中存放的是一個個存儲cell信息的對象,這些對象中包括cell開始位置、高度、以及所屬的indexPath。我們能通過遍歷或者二分查找快速找到當下需要顯示的cell的開始行和結束行。二分查找的時間復雜度是:O()=O(logn),10000次查找最多也只需要14次,所以我們采用二分查找,因為在Foundation框架中有對二分查找的封裝,我們直接采用就行,當然也可以自己實現。代碼如下:
// 計算將要顯示的是第幾行到第幾行 - (NSRange)numberOfRowsWillShowInPGLTableView:(CGFloat)start end:(CGFloat)end { PGLRowDetail *startDetail = [[PGLRowDetail alloc] init]; startDetail.startY = start; PGLRowDetail *endDetail = [[PGLRowDetail alloc] init]; endDetail.startY = end; NSInteger startIndex = [self.rowRecords indexOfObject:startDetail inSortedRange:NSMakeRange(0, self.rowRecords.count) options:NSBinarySearchingInsertionIndex usingComparator:^NSComparisonResult(PGLRowDetail * obj1, PGLRowDetail * obj2) { if (obj1.startY < obj2.startY) return NSOrderedAscending; return NSOrderedDescending; }]; if (startIndex > 0) startIndex--; NSInteger endIndex = [self.rowRecords indexOfObject:endDetail inSortedRange:NSMakeRange(0, self.rowRecords.count - 1) options:NSBinarySearchingInsertionIndex usingComparator:^NSComparisonResult(PGLRowDetail * obj1, PGLRowDetail * obj2) { if (obj1.startY < obj2.startY) return NSOrderedAscending; return NSOrderedDescending; }]; if (endIndex > 0) endIndex--; return NSMakeRange(startIndex, endIndex - startIndex + 1); }
判斷要顯示的cell是否已經在界面上,如不在從cellForRow方法中獲取cell,cellForRow首先會從重用池中查找對應標識符的cell,如果找到從緩存池中移除,如果找不到重新創建,然後添加在界面上,代碼如下:
// 放置需要顯示的cell for (NSUInteger i = range.location; i < range.location + range.length; i++) { NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0]; PGLTableViewCell *cell = [self.visibleCells objectForKey:@(i)]; if (cell == nil) { cell = [self.dataSource pgtableView:self cellForRowAtIndexPath:indexPath]; [self.visibleCells setObject:cell forKey:@(i)]; PGLRowDetail *detail = self.rowRecords[i]; cell.frame = CGRectMake(0, detail.startY, self.frame.size.width, detail.rowHeight); [self addSubview:cell]; } } // 從重用池中獲取cell - (PGLTableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier { PGLTableViewCell *reuseCell = nil; for (PGLTableViewCell *cell in self.reusePool) { if ([cell.reuseIdentifier isEqualToString:identifier]) { reuseCell = cell; break; } } if (reuseCell) { [self.reusePool removeObject:reuseCell]; } return reuseCell; }
判斷cell是否已經離開屏幕,如果離開就從屏幕上移除,加入重用池。代碼如下:
// 移除離開屏幕的cell,同時放入重用池 NSArray *allVisibleCells = [self.visibleCells allKeys]; for (NSNumber *numb in allVisibleCells) { if (!NSLocationInRange([numb integerValue], range)) { PGLTableViewCell *cell = [self.visibleCells objectForKey:numb]; [self.reusePool addObject:cell]; [self.visibleCells removeObjectForKey:numb]; [cell removeFromSuperview]; } }
以上就是重用機制的實現,如果不懂可以在這裡看詳細代碼。
總結:當然tableView有許多強大的功能,我們只是演示了一個簡單的重用機制,比如各種代理以及數據源方法,有時間我會盡量補充,如果感興趣你可以嘗試去實現它,我相信對你來說應該是個小問題。