一.UITableView的重用機制
1.重用原理
重用方法:
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
如下圖顿膨,假設(shè)虛線范圍是屏幕的顯示區(qū)域锅锨;
A2、A6的cell有一部分在屏幕內(nèi)
A3恋沃、A4必搞、A5的cell全在屏幕內(nèi)
A1在屏幕外,現(xiàn)在它就被放到了重用池囊咏;
如果整個屏幕里面每個cell的identifier是一樣的恕洲,向上滑動的時候,A7就會去重用池里面取出A1存放的cell梅割;
簡單的說:就如同盤子使用了之后霜第,洗完可以繼續(xù)用;
2.手寫重用
目的:1.熟悉重用户辞;2.會自定義重用
實現(xiàn)要求:
給tableview增加自定義索引泌类,點擊按鈕可以切換索引的內(nèi)容;
索引上面的按鈕要進行復(fù)用底燎;
如下:點擊紅色按鈕刃榨,可以切換右側(cè)的索引條內(nèi)容。
實現(xiàn)思路:
1.自定義UITableView双仍,給UITableView增加索引條containerView枢希;UITableView負責數(shù)據(jù)顯示和布局;
2.containerView的按鈕從ViewReusePool中獲取
2.1重用的類ViewReusePool
重用池:
記錄正在使用殊校、等待使用的view晴玖;
提供獲取等待使用的view方法(取)为流、向重用池添加視圖的方法(放)呕屎、將所有視圖移動到重用隊列的方法(刪除);
補充:
這里的數(shù)據(jù)用NSSet保存敬察;因為我們這里只是從重用池隨機取出一個可用的就好了秀睛。NSSet可以提高讀取效率;
NSArray | NSSet |
---|---|
有序 | 無序 |
可以有相同的對象 | 有唯一的對象(重復(fù)的對象會被去掉) |
通過索引來提取對象 | 通過對比來提取對象 |
NSSet的效率確實是比NSArray高的莲祸,因為它主要用的是hash算
NSArray的話需要循環(huán)集合中所有的對象蹂安,來找到所需要的目標。所以锐帜,循環(huán)所有對象與直接去對象的位置獲取田盈,速度就顯而易見了。
iOS_NSSet與NSArray的區(qū)別
.h:
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
// 實現(xiàn)重用機制的類
@interface ViewReusePool : NSObject
// 從重用池當中取出一個可重用的view
- (UIView *)dequeueReusableView;
// 向重用池當中添加一個視圖
- (void)addUsingView:(UIView *)view;
// 重置方法缴阎,將當前使用中的視圖移動到可重用隊列當中
- (void)reset;
@end
.m
#import "ViewReusePool.h"
@interface ViewReusePool ()
// 等待使用的隊列
@property (nonatomic, strong) NSMutableSet *waitUsedQueue;
// 使用中的隊列
@property (nonatomic, strong) NSMutableSet *usingQueue;
@end
@implementation ViewReusePool
- (id)init{
self = [super init];
if (self) {
_waitUsedQueue = [NSMutableSet set];
_usingQueue = [NSMutableSet set];
}
return self;
}
- (UIView *)dequeueReusableView{
UIView *view = [_waitUsedQueue anyObject];
if (view == nil) {
return nil;
}
else{
// 進行隊列移動
[_waitUsedQueue removeObject:view];
[_usingQueue addObject:view];
return view;
}
}
- (void)addUsingView:(UIView *)view
{
if (view == nil) {
return;
}
// 添加視圖到使用中的隊列
[_usingQueue addObject:view];
}
- (void)reset{
UIView *view = nil;
while ((view = [_usingQueue anyObject])) {
// 從使用中隊列移除
[_usingQueue removeObject:view];
// 加入等待使用的隊列
[_waitUsedQueue addObject:view];
}
}
@end
2.2 自定義UITableView
.h:
#import <UIKit/UIKit.h>
//通過協(xié)議獲取索引條顯示的數(shù)據(jù)
@protocol IndexedTableViewDataSource <NSObject>
// 獲取一個tableview的字母索引條數(shù)據(jù)的方法
- (NSArray <NSString *> *)indexTitlesForIndexTableView:(UITableView *)tableView;
@end
@interface IndexedTableView : UITableView
@property (nonatomic, weak) id <IndexedTableViewDataSource> indexedDataSource;
@end
.m:
#import "IndexedTableView.h"
#import "ViewReusePool.h"
@interface IndexedTableView ()
{
UIView *containerView;
ViewReusePool *reusePool;
}
@end
@implementation IndexedTableView
- (void)reloadData{
[super reloadData];
// 懶加載(當需要的時候再創(chuàng)建)
if (containerView == nil) {
containerView = [[UIView alloc] initWithFrame:CGRectZero];
containerView.backgroundColor = [UIColor whiteColor];
// [self addSubview:containerView];//如果這樣寫允瞧,tableview滾動的時候containerView也會滾動
//避免索引條隨著table滾動
[self.superview insertSubview:containerView aboveSubview:self];
}
if (reusePool == nil) {
reusePool = [[ViewReusePool alloc] init];
}
// 標記所有視圖為可重用狀態(tài)
[reusePool reset];
// reload字母索引條
[self reloadIndexedBar];
}
- (void)reloadIndexedBar
{
// 獲取字母索引條的顯示內(nèi)容
NSArray <NSString *> *arrayTitles = nil;
if ([self.indexedDataSource respondsToSelector:@selector(indexTitlesForIndexTableView:)]) {
arrayTitles = [self.indexedDataSource indexTitlesForIndexTableView:self];
}
// 判斷字母索引條是否為空
if (!arrayTitles || arrayTitles.count <= 0) {
[containerView setHidden:YES];
return;
}
NSUInteger count = arrayTitles.count;
CGFloat buttonWidth = 60;
CGFloat buttonHeight = self.frame.size.height / count;
//移除之前view上的所有數(shù)據(jù)
[containerView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
for (int i = 0; i < [arrayTitles count]; i++) {
NSString *title = [arrayTitles objectAtIndex:i];
// 從重用池當中取一個Button出來
UIButton *button = (UIButton *)[reusePool dequeueReusableView];
// 如果沒有可重用的Button重新創(chuàng)建一個
if (button == nil) {
button = [[UIButton alloc] initWithFrame:CGRectZero];
button.backgroundColor = [UIColor whiteColor];
// 注冊button到重用池當中
[reusePool addUsingView:button];
NSLog(@"新創(chuàng)建一個Button");
}
else{
NSLog(@"Button 重用了");
}
// 添加button到父視圖控件
[containerView addSubview:button];
[button setTitle:title forState:UIControlStateNormal];
[button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
// 設(shè)置button的坐標
[button setFrame:CGRectMake(0, i * buttonHeight, buttonWidth, buttonHeight)];
}
[containerView setHidden:NO];
containerView.frame = CGRectMake(self.frame.origin.x + self.frame.size.width - buttonWidth, self.frame.origin.y, buttonWidth, self.frame.size.height);
}
@end
2.3 controller使用自定義UITableView
.m
#import "ViewController.h"
#import "IndexedTableView.h"
@interface ViewController ()<UITableViewDataSource,UITableViewDelegate,IndexedTableViewDataSource>
{
IndexedTableView *tableView;//帶有索引條的tableview
UIButton *button;
NSMutableArray *dataSource;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//創(chuàng)建一個Tableview
tableView = [[IndexedTableView alloc] initWithFrame:CGRectMake(0, 60, self.view.frame.size.width, self.view.frame.size.height - 60) style:UITableViewStylePlain];
tableView.delegate = self;
tableView.dataSource = self;
// 設(shè)置table的索引數(shù)據(jù)源
tableView.indexedDataSource = self;
[self.view addSubview:tableView];
//創(chuàng)建一個按鈕
button = [[UIButton alloc] initWithFrame:CGRectMake(0, 20, self.view.frame.size.width, 40)];
button.backgroundColor = [UIColor redColor];
[button setTitle:@"reloadTable" forState:UIControlStateNormal];
[button addTarget:self action:@selector(doAction:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
// 數(shù)據(jù)源
dataSource = [NSMutableArray array];
for (int i = 0; i < 100; i++) {
[dataSource addObject:@(i+1)];
}
// Do any additional setup after loading the view, typically from a nib.
}
#pragma mark IndexedTableViewDataSource
- (NSArray <NSString *> *)indexTitlesForIndexTableView:(UITableView *)tableView{
//奇數(shù)次調(diào)用返回6個字母,偶數(shù)次調(diào)用返回11個
static BOOL change = NO;
if (change) {
change = NO;
return @[@"A",@"B",@"C",@"D",@"E",@"F",@"G",@"H",@"I",@"J",@"K"];
}
else{
change = YES;
return @[@"A",@"B",@"C",@"D",@"E",@"F"];
}
}
#pragma mark UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [dataSource count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *identifier = @"reuseId";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
//如果重用池當中沒有可重用的cell,那么創(chuàng)建一個cell
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
}
// 文案設(shè)置
cell.textLabel.text = [[dataSource objectAtIndex:indexPath.row] stringValue];
//返回一個cell
return cell;
}
#pragma mark - UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 40;
}
- (void)doAction:(id)sender{
NSLog(@"reloadData");
[tableView reloadData];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
3.手寫重用池總結(jié)
1述暂、懶加載(在真正需要的時候創(chuàng)建痹升,以前并未注意這一條)
2、給自定義view提供數(shù)據(jù)的方法畦韭,之前使用的是屬性view.array = array;
疼蛾,今天學(xué)習到也可以使用代理;
3艺配、要合適的自定義view察郁,
- 之前的寫法:
在controller里面添加TableView,然后在controller的view上面添加索引的view妒挎;
缺點:如果要索引條去掉绳锅,就需要刪除controller里面的大量代碼;
- 今天學(xué)到的:
定義IndextTableView繼承 UITableView酝掩,在IndextTableView里面添加索引view鳞芙;
在contrlloer里面使用,創(chuàng)建UITableView的時候類型修改為IndexedTableView,實現(xiàn)IndexedTableView為索引提供數(shù)據(jù)的代理:
tableView = [[IndexedTableView alloc] initWithFrame:CGRectMake(0, 60, self.view.frame.size.width, self.view.frame.size.height - 60) style:UITableViewStylePlain];
tableView.delegate = self;
tableView.dataSource = self;
// 設(shè)置table的索引數(shù)據(jù)源
tableView.indexedDataSource = self;
[self.view addSubview:tableView];
優(yōu)點:
如果要索引條去掉
如果在controller里面創(chuàng)建UITableView的時候更改類型期虾,把為索引提供數(shù)據(jù)的代理去掉即可原朝。
4、重用池這部分代碼镶苞,以后直接文件拷出來用也可以喳坠;
二.數(shù)據(jù)源同步
例如數(shù)據(jù)刪除的時候,會有數(shù)據(jù)源同步的問題茂蚓;
場景:如下圖壕鹉,在子線程里面下拉刷新請求數(shù)據(jù)的時候,主線程刪除了一個一條數(shù)據(jù)聋涨,刷新UI顯示沒有問題晾浴;
但是請求下來的數(shù)據(jù)里面可能還包含刪除掉的那條數(shù)據(jù);刷新數(shù)據(jù)的時候就會有問題牍白,怎么解決這種數(shù)據(jù)源同步的問題呢脊凰?
解決:
方案1:并發(fā)訪問、數(shù)據(jù)拷貝
方案2:串行訪問
方案1:并發(fā)訪問茂腥、數(shù)據(jù)拷貝
如果進行并發(fā)訪問狸涌,刪除掉數(shù)據(jù)之后進行記錄;等待數(shù)據(jù)請求回來后最岗,把刪除掉的數(shù)據(jù)從請求回來的數(shù)據(jù)里面刪除帕胆;
方案2:串行訪問
采取串行訪問,在數(shù)據(jù)請求的時候不可以進行刪除般渡,等待數(shù)據(jù)訪問回來之后才可以進行刪除惶楼;
兩種方案優(yōu)缺點對比
- 并發(fā)訪問:記錄之后右蹦,遍歷并刪除數(shù)據(jù)這部分會有內(nèi)存、時間消耗
- 串行訪問:等待數(shù)據(jù)請求完成之后再刪除歼捐,就會讓用戶進行等待
需要根據(jù)實際情況選擇解決方案;
三.補充:
UITableView代理方法調(diào)用順序:
UITableView是繼承自UIScrollView的晨汹,需要先確定它的contentSize及每個Cell的位置豹储。
所以UITableView的回調(diào)順序是先多次調(diào)用tableView:heightForRowAtIndexPath:以確定contentSize及Cell的位置,然后才會調(diào)用tableView:cellForRowAtIndexPath:淘这,從而來顯示在當前屏幕的Cell
舉個例子來說:如果現(xiàn)在要顯示100個Cell剥扣,當前屏幕顯示5個。那么刷新(reload)UITableView時铝穷,UITableView會先調(diào)用100次tableView:heightForRowAtIndexPath:方法钠怯,然后調(diào)用5次tableView:cellForRowAtIndexPath:方法;滾動屏幕時曙聂,每當Cell滾入屏幕晦炊,都會調(diào)用一次tableView:heightForRowAtIndexPath:、tableView:cellForRowAtIndexPath:方法宁脊。