在UITableViewCell中异步加载图片

iOS应用如何在列表中高效地加载图片?

问题

当我们在UITableViewCell中进行比较费时的工作(比如绘图,或者加载网络图片)的时候,通常会出现下面的问题:

  1. UITableViewCell移出视图的时候,这个UITableViewCell对应的异步操作仍然在执行。这通常会造成系统资源浪费,还可能由于这个异步操作不知道返回到哪个UITableViewCell而导致UITableView一些诡异的行为。
  2. UITableViewCell通常是重复利用的实例,这就会导致当前UITableViewCell可能会加载之前出现在视图中但是现在不在视图中的UITableViewCell的内容,在你面前变换一下UITableViewCell里面的内容,这是我们都不想看到的。

解决

2012年的WWDC中的Session 211 Building Concurrent User Interfaces on iOS很好地讲解了如何在一个UITableViewCell里面做比较费时的事情(比如绘图,或者网络请求图片),而保持app流畅,我下面的代码和Advanced issues: Asynchronous UITableViewCell content loading done right这一篇文章都是根据这个session实现的。

解决上述两个问题的基本想法:当tableView:cellForRowAtIndexPath:被调用的时候,去网络请求UITableViewCell对应的图片。当请求成功时,判断当前的UITableViewCell是否仍然在视图中:如果在,将请求的图片设置到UITableViewCell中,否则,不设置。另外,当UITableViewCell移出视图的时候,要取消其对应的请求,防止对后续请求造成影响。

LazyTableImages: Populating UITableView content asynchronously这个实现相对简单一点:这个实现通过UIScrollViewDelegate中的方法,保证在用户滚动UITableViewUITableView停止之前都停止UITableViewCell里面的更新,只有当UITableView静止的时候,才去更新当前视图中的所有UITableViewCell。而上面的实现则能保证在用户进行滚动的同时,请求并加载图片到正确的位置。

主要代码:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UIScrollViewDelegate {

    var data = [String]()
    var rowIndexToOperationDictionary = [String:NSBlockOperation]()
    let downloadImageOperationQueue: NSOperationQueue = {
        var queue = NSOperationQueue()
        queue.name = "download image queue"
        queue.maxConcurrentOperationCount = 4
        return queue
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        for i in 1..<100 {
            data.append("\(i)")
        }
        let tableView = UITableView(frame: self.view.bounds)
        tableView.dataSource = self
        tableView.delegate = self
        self.view.addSubview(tableView)
    }
    
    // when table view disappears, cancel all operations
    override func viewDidDisappear(animated: Bool) {
        downloadImageOperationQueue.cancelAllOperations()
    }
    
    func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        return 100
    }
    
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return data.count
    }
    
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        var cell = tableView.dequeueReusableCellWithIdentifier("cell")
        if cell == nil {
            cell = UITableViewCell(style: .Default, reuseIdentifier: "cell")
        }
        
        cell!.textLabel!.text = data[indexPath.row]
        
        let loadImageInfoCellOperation = NSBlockOperation()
        weak var weakLoadImageInfoCellOperation = loadImageInfoCellOperation
        func loadImageOperationBlock() {
            let imageData = NSData(contentsOfURL: NSURL(string: "http://dummyimage.com/100x100/222/fff.png&text="+data[indexPath.row])!)
            var imageIcon:UIImage?
            if let imageData = imageData {
                imageIcon = UIImage(data: imageData)
            }

            func updateImageOperationBlock() {
                if let operation = weakLoadImageInfoCellOperation {
                    if operation.cancelled {
                        rowIndexToOperationDictionary.removeValueForKey(data[indexPath.row])
                    } else {
                        let theCell = tableView.cellForRowAtIndexPath(indexPath)
                        theCell?.imageView!.image = imageIcon
                        theCell?.setNeedsLayout()
                        rowIndexToOperationDictionary.removeValueForKey(data[indexPath.row])
                    }
                }
            }
            NSOperationQueue.mainQueue().addOperationWithBlock(updateImageOperationBlock)
        }
        loadImageInfoCellOperation.addExecutionBlock(loadImageOperationBlock)
        
        rowIndexToOperationDictionary[data[indexPath.row]] = loadImageInfoCellOperation
        downloadImageOperationQueue.addOperation(loadImageInfoCellOperation)
        cell?.imageView!.image = nil
        
        return cell!
    }
    
    // when cell is out of scene, cancel the operation
    func tableView(tableView: UITableView, didEndDisplayingCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
        let loadImageInfoCellOperation:NSBlockOperation? = rowIndexToOperationDictionary[data[indexPath.row]]
        if let op = loadImageInfoCellOperation {
            op.cancel()
            rowIndexToOperationDictionary.removeValueForKey(data[indexPath.row])
        }
        
    }

}

其它问题

  1. 修改应用安全权限,确保能下载http资源。参考:How do I load an HTTP URL with App Transport Security enabled in iOS 9?

  2. 当设置的UITableViewCell中的图片的时候,需要调用一下[cell setNeedsLayout]确保其显示出来。否则,只有当你点击的时候图片才会更新出来。参考 cell imageView in UITableView doesn’t appear until selected

资源

这是一个神奇的网站!可以在线定制图片颜色和内容,然后生成对应的url。参考:Dynamic Dummy Image Generator。所以才会有下面的效果,方便检查每个cell的图片是否正确:

async_load_online_images_into_uitableviewcells.png