简体   繁体   English

Swift:用NSOperation保留循环

[英]Swift: Retain cycle with NSOperation

In my app I use an image loader class to load images from the web for a collection view. 在我的应用程序中,我使用图像加载器类从Web加载图像以获取集合视图。 The class keeps track of the download operations and cancels them when the cells for the images are no longer visible in the collection view. 该类跟踪下载操作,并在集合视图中不再显示图像的单元格时取消它们。 This implementation is based on the raywenderlich tutorial for NSOperation: http://www.raywenderlich.com/76341/use-nsoperation-nsoperationqueue-swift . 此实现基于NSOperation的raywenderlich教程: http ://www.raywenderlich.com/76341/use-nsoperation-nsoperationqueue-swift。

I use NSOperation for downloading an image from the web. 我使用NSOperation从网上下载图像。 I noticed with Instruments that none of the NSoperations is released. 我注意到仪器公司没有发布任何NS操作。 This creates an increase of the used memory for each image that is downloaded. 这会为下载的每个图像增加已用内存。 In the completion block I references 'self'. 在完成块中,我引用了“self”。 So I figured out that I created a retain cycle. 所以我发现我创建了一个保留周期。

I read a lot of examples on internet. 我在网上看了很多例子。 I understand that I can use capture lists with 'weak self' or 'unowned self'. 我知道我可以使用“弱自我”或“无主自我”的捕获列表。 I tried this for the completion block, but still the operations are not released. 我尝试了这个完成块,但仍然没有发布操作。

My code for the image loader class is as follows: 我的图像加载器类的代码如下:

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)
        }
    }
}

Where exactly do I create the retain cycle? 我究竟在哪里创建保留周期? And how do I solve this? 我该如何解决这个问题? When should I use 'unowned' and when should I use 'weak'? 什么时候应该使用'无主',什么时候应该使用'弱'?

I would appreciate it if someone can explain the solution, so I can learn from my mistake. 如果有人能解释解决方案我会很感激,所以我可以从错误中吸取教训。

I found the problem. 我发现了这个问题。 The retain cycle is not caused by referencing self, but by referencing the NSOperation in the completion block of the NSOperation! 保留周期不是由引用self引起的,而是通过在NSOperation的完成块中引用NSOperation引起的!

In the function startDownloadForUrl(...) I declare the variable downloader . 在函数startDownloadForUrl(...)中,我声明了变量下载器 Next I declare an completion block for this variable. 接下来,我为此变量声明一个完成块。 In this completion block I reference the variable downloader . 在这个完成块中,我引用了变量下载器 This causes the retain cycle. 这导致保留周期。

I solved this by using [unowned downloader] within the completion block. 我通过在完成块中使用[unowned downloader]解决了这个问题。

This created anaother problem. 这造成了另一个问题。 In the completion block I asynchronously call the main thread. 在完成块中,我异步调用主线程。 In this call the variable downloader.imageData was used. 在此调用中,使用了变量downloader.imageData Because of this asynchronous call, the NSOperation may be already ended and the variable downloader may not longer exists. 由于此异步调用,NSOperation可能已经结束,并且变量下载器可能不再存在。 To avoid crashes I declare a new variable for the imageData, so the data will still be available when used in the main thread. 为了避免崩溃,我为imageData声明了一个新变量,因此在主线程中使用时数据仍然可用。

The completion block now looks like: 完成块现在看起来像:

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)
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM