【问题标题】:Swift: Retain cycle with NSOperationSwift:使用 NSOperation 保持循环
【发布时间】:2015-12-30 11:09:19
【问题描述】:

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

我使用 NSOperation 从网络下载图像。我注意到 Instruments 没有发布任何 NSoperations。这会增加下载的每个图像的已用内存。在完成块中,我引用了“自我”。所以我发现我创建了一个保留循环。

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

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

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

我究竟在哪里创建保留周期?我该如何解决这个问题? 什么时候应该使用“unowned”,什么时候应该使用“weak”?

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

【问题讨论】:

    标签: ios swift nsoperation nsoperationqueue retain-cycle


    【解决方案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)
        }
    }
    

    【讨论】:

    • 我刚回来发布这个答案,发现你已经完成了!一个迂腐的笔记; "// copy of imageData" 你保留了 NSData 对象,而不是复制它。
    • 你是对的。这不是迂腐。我更改了评论。
    猜你喜欢
    • 1970-01-01
    • 2021-04-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多