学计算机的那个

不是我觉到、悟到,你给不了我,给了也拿不住;只有我觉到、悟到,才有可能做到,能做到的才是我的.

0%

【译】Asynchronous UITableViewCell content loading done right

问题

你有没有想过为什么你的UITableView加载“几乎”完美?我的意思是,当然——你已经向iOS明确表示,所有重要的cell工作(如从远程URL下载图像或渲染内容)都将在后台线程上异步计算。但有时这还不够,主要有两个原因:

  1. 一旦cell离开了可见区域,您调用的异步操作仍在工作。这通常会导致不必要的系统资源使用,甚至由于操作不知道完成后返回到哪个cell而导致错误的table行为。

  2. UITableViewCells经常被重用。这意味着加载到视图中的单元格有时可能包含最初加载到完全不同的单元格中的数据。这通常会导致“cell切换”行为,这可能会让你完全生气。

解决

首先,重要的是要明白有很多方法可以解决这个问题——有些是很好的想法,有些是,嗯,糟糕的。我在这里描述的方法是基于苹果提供的演示(即WWDC 2012 session 211),你知道这些人对iOS略知一二。

对于我们的例子,我将使用一个简单的UITableView实例来显示你的facebook好友的名字和头像。主要思想是,当UITableViewDataSourcetableView:cellForRowAtIndexPath:被调用时,我们开始加载配置文件图像。如果操作成功并且单元格仍然在视图中,那么我们只需将图像添加到单元格的配置文件图像视图中(在主线程中)。如果不是,我们确保不执行“setImage”部分。

在开始之前,需要做一些准备工作:定义一个NSOperationQueue用于运行后台操作——在这个例子中,我们称之为imageLoadingOperationQueue。同样,定义一个NSMutableDictionary来存储对特定操作的引用——在这个例子中,我们将把facebook的唯一ids映射到facebookUidtoimagedownloadoperations字典上的operations

大多数重要的东西都在代码中注释了,所以确保你阅读了注释来理解发生了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
FacebookFriend *friend = [self.facebookFriends objectAtIndex:indexPath.row];
FacebookFriendCell *cell = [tableView dequeueReusableCellWithIdentifier:FB_CELL_IDENTIFIER forIndexPath:indexPath];
cell.lblName.text = friend.name;

//Create a block operation for loading the image into the profile image view
NSBlockOperation *loadImageIntoCellOp = [[NSBlockOperation alloc] init];
//Define weak operation so that operation can be referenced from within the block without creating a retain cycle
__weak NSBlockOperation *weakOp = loadImageIntoCellOp;
[loadImageIntoCellOp addExecutionBlock:^(void){
//Some asynchronous work. Once the image is ready, it will load into view on the main queue
UIImage *profileImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:friend.imageUrl]]];
[[NSOperationQueue mainQueue] addOperationWithBlock:^(void) {
//Check for cancelation before proceeding. We use cellForRowAtIndexPath to make sure we get nil for a non-visible cell
if (!weakOp.isCancelled) {
FacebookFriendCell *theCell = (FacebookFriendCell *)[tableView cellForRowAtIndexPath:indexPath];
[theCell.ivProfile setImage:profileImage];
[self.facebookUidToImageDownloadOperations removeObjectForKey:friend.uid];
}
}];
}];

//Save a reference to the operation in an NSMutableDictionary so that it can be cancelled later on
if (friend.uid) {
[self.facebookUidToImageDownloadOperations setObject:loadImageIntoCellOp forKey:friend.uid];
}

//Add the operation to the designated background queue
if (loadImageIntoCellOp) {
[self.imageLoadingOperationQueue addOperation:loadImageIntoCellOp];
}

//Make sure cell doesn't contain any traces of data from reuse -
//This would be a good place to assign a placeholder image
cell.ivProfile.image = nil;

return cell;
}

现在剩下要做的就是利用iOS 6.0中引入的一个新的UITableViewDelegate方法:它被称为”tableView:didEndDisplayingCell:forRowAtIndexPath: “它在我们加载数据的cell不再需要时被调用。听起来像是下面代码的完美位置,它获取相关操作并取消它:

1
2
3
4
5
6
7
8
9
10
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
FacebookFriend *friend = [self.facebookFriends objectAtIndex:indexPath.row];
//Fetch operation that doesn't need executing anymore
NSBlockOperation *ongoingDownloadOperation = [self.facebookUidToImageDownloadOperations objectForKey:friend.uid];
if (ongoingDownloadOperation) {
//Cancel operation and remove from dictionary
[ongoingDownloadOperation cancel];
[self.facebookUidToImageDownloadOperations removeObjectForKey:friend.uid];
}
}

此外,不要忘记利用NSOperationQueue并在不再需要表时调用“cancelallooperations”:

1
2
3
- (void)viewDidDisappear:(BOOL)animated {
[self.imageLoadingOperationQueue cancelAllOperations];
}

就是这样!现在你有了一个运行得像法拉利一样流畅的UITableView。不客气😉

参考

  1. Asynchronous UITableViewCell content loading done right
  2. 在UITableViewCell中异步加载图片-Swift