斯威夫特:用NSOperation保留周期

时间:2015-12-30 11:09:20

标签: ios swift nsoperation nsoperationqueue retain-cycle

在我的应用程序中,我使用图像加载器类从Web加载图像以获取集合视图。该类跟踪下载操作,并在集合视图中不再显示图像的单元格时取消它们。此实现基于NSOperation的raywenderlich教程:http://www.raywenderlich.com/76341/use-nsoperation-nsoperationqueue-swift

我使用NSOperation从网上下载图像。我注意到仪器公司没有发布任何NS操作。这会为下载的每个图像增加已用内存。在完成块中,我引用了“self”。所以我发现我创建了一个保留周期。

我在互联网上阅读了很多例子。我知道我可以使用“弱自我”或“无主自我”的捕获列表。我尝试了这个完成块,但仍然没有发布操作。

我的图像加载器类代码如下:

import Foundation
import UIKit

class ImageLoader {
    lazy var downloadsInProgress = [NSIndexPath:NSOperation]()  
    lazy var downloadQueue:NSOperationQueue = {
        var queue = NSOperationQueue()
        queue.name = "Image Download queue"
        return queue
    }()

    let cache = NSCache()       // contains NSData objects for images

    init() {
        // Max. cache size is 10% of available physical memory (in MB's)
        cache.totalCostLimit = 200 * 1024 * 1024    // TODO: change to 10%
    }

    /**
     * Download image based on url for given indexpath. 
     * The download is only started if the indexpath is still present in the downloadsInProgress array
     */

    func startDownloadForUrl(url: String, indexPath: NSIndexPath, completion: (imageData: NSData?) -> Void) {
        // check if download request is already present
        if downloadsInProgress[indexPath] != nil {
            return
        }

        // check cache
        if let imageData = self.cache.objectForKey(url) as? NSData {
            NSOperationQueue.mainQueue().addOperationWithBlock() {
                //remove indexpath from progress queue
                self.downloadsInProgress.removeValueForKey(indexPath)
                completion(imageData: imageData)
            }
            return
        }

        // prepare the download
        let downloader = ImageDownloader(url: url)

        downloader.completionBlock = {
            [unowned self] in

            if downloader.cancelled {
                return
            }

            // image is retrieved from web
            NSOperationQueue.mainQueue().addOperationWithBlock() {
                [unowned self] in

                //remove indexpath from progress queue
                self.downloadsInProgress.removeValueForKey(indexPath)

                // add image to cache
                if downloader.imageData != nil {
                    self.cache.setObject(downloader.imageData!, forKey: url, cost: downloader.imageData!.length)
                }
                completion(imageData: downloader.imageData)
            }
        }

        // add downloader to operations in progress and start the operation
    NSOperationQueue.mainQueue().addOperationWithBlock() {
            [unowned self] in

            self.downloadsInProgress[indexPath] = downloader
            self.downloadQueue.addOperation(downloader)
        }
    } 


    /**
     * Suspends queue for downloading images
     */

    func suspendAllOperations() {
        downloadQueue.suspended = true
    }


    /**
     * Resumes queue for downloading images
     */

    func resumeAllOperations() {
        downloadQueue.suspended = false
    }


    /**
     * Cancels downloads for NOT visible indexpaths. The parameter specifies an array of visible indexpaths!
     */

    func cancelDownloads(visibleIndexPaths: [NSIndexPath]) {
        let allPendingOperations = Set(downloadsInProgress.keys)
        let visiblePaths = Set(visibleIndexPaths)

        // cancel all pending operations for indexpaths that are not visible
        var toBeCancelled = allPendingOperations
        toBeCancelled.subtractInPlace(visiblePaths)

        for indexPath in toBeCancelled {
            if let pendingDownloadOperation = downloadsInProgress[indexPath] {
                pendingDownloadOperation.cancel()
            }

            downloadsInProgress.removeValueForKey(indexPath)
        }
    }
}


class ImageDownloader: NSOperation {
    var url: String
    var imageData: NSData?

    init(url: String) {
        self.url = url
    }

    override func main() {
        if self.cancelled {
            return
        }

        if let imageUrl = NSURL(string: url) {
            // retrieve data from web
            setNetworkActivityIndicatorVisible(true)
            imageData = NSData(contentsOfURL: imageUrl)
            setNetworkActivityIndicatorVisible(false)

            if self.cancelled {
                imageData = nil
                return
            }

            // scale image
            if imageData != nil {
                if let image = UIImage(data: imageData!) {
                    let imageData2 = UIImageJPEGRepresentation(image, 1.0)
                    let compressionRate = Float(imageData!.length) / Float(imageData2!.length)

                    let scaleWidth = 244 / image.size.width
                    let scaleHeight = 244 / image.size.height
                    let imageScale = min(scaleWidth, scaleHeight)

                    let rect = CGRectMake(0.0, 0.0, image.size.width * imageScale, image.size.height * imageScale)

                    UIGraphicsBeginImageContext(rect.size)
                    image.drawInRect(rect)
                    let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
                    let scaledImageData = UIImageJPEGRepresentation(scaledImage, CGFloat(compressionRate))
                    UIGraphicsEndImageContext()

                    imageData = scaledImageData
                }
            }
        }
    }

    private func setNetworkActivityIndicatorVisible(visible: Bool) {
        NSOperationQueue.mainQueue().addOperationWithBlock() {
            let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
            appDelegate.setNetworkActivityIndicatorVisible(visible)
        }
    }
}

我在哪里创建保留周期?我该如何解决这个问题? 什么时候应该使用'无主',什么时候应该使用'弱'?

如果有人能解释解决方案,我将不胜感激,因此我可以从错误中吸取教训。

1 个答案:

答案 0 :(得分:1)

我发现了问题。保留周期不是由引用self引起的,而是通过在NSOperation的完成块中引用NSOperation来引起的!

在函数startDownloadForUrl(...)中,我声明了变量 downloader 。接下来,我为此变量声明一个完成块。在此完成块中,我引用了变量 downloader 。这会导致保留周期。

我通过在完成块中使用[unowned downloader]解决了这个问题。

这造成了另一个问题。在完成块中,我异步调用主线程。在此调用中,使用了变量 downloader.imageData 。由于此异步调用,NSOperation可能已经结束,变量 downloader 可能不再存在。为了避免崩溃,我为imageData声明了一个新变量,因此在主线程中使用时数据仍然可用。

完成块现在看起来像:

downloader.completionBlock = {
    [unowned downloader] in
    if downloader.cancelled {
        return
    }

    let imageData = downloader.imageData    // retain the imageData. It will be used asynchrounous in the main thread. The downloader operation might already be finished and downloader will no longer exists.

    // image is retrieved from web
    NSOperationQueue.mainQueue().addOperationWithBlock() {
        //remove indexpath from progress queue
        self.downloadsInProgress.removeValueForKey(indexPath)

        // add image to cache
        if imageData != nil {
            self.cache.setObject(imageData!, forKey: url, cost: imageData!.length)
        }
        completion(imageData: imageData)
    }
}