Swift: сохранить цикл с помощью NSOperation

В моем приложении я использую класс загрузчика изображений для загрузки изображений из Интернета для просмотра коллекции. Класс отслеживает операции загрузки и отменяет их, когда ячейки для изображений больше не видны в представлении коллекции. Эта реализация основана на руководстве raywenderlich для NSOperation: http://www.raywenderlich.com/76341/use-nsoperation-nsoperationqueue-swift.

Я использую NSOperation для загрузки изображения из Интернета. Я заметил с помощью инструментов, что ни один из 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" и когда я должен использовать "слабый"?

Я был бы признателен, если бы кто-то смог объяснить решение, чтобы я мог учиться на своей ошибке.

1 ответ

Решение

Я нашел проблему. Цикл сохранения вызван не ссылкой на себя, а ссылкой на операцию NSOperation в блоке завершения операции NSOperation!

В функции startDownloadForUrl(...) я объявляю переменную загрузчик. Далее я объявляю блок завершения для этой переменной. В этом блоке завершения я ссылаюсь на переменную загрузчик. Это вызывает цикл сохранения.

Я решил это с помощью [unowned downloader] в блоке завершения.

Это создало еще одну проблему. В блоке завершения я асинхронно вызываю основной поток. В этом вызове была использована переменная downloader.imageData. Из-за этого асинхронного вызова операция NSO может быть уже завершена, и загрузчик переменной может больше не существовать. Чтобы избежать сбоев, я объявляю новую переменную для 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)
    }
}
Другие вопросы по тегам