2015-12-30 87 views
3

在我的應用程序中,我使用圖像加載器類從Web上加載圖像作爲集合視圖。該類會跟蹤下載操作,並在圖像的單元格在集合視圖中不再可見時取消它們。該實現基於NSOperation的raywenderlich教程:http://www.raywenderlich.com/76341/use-nsoperation-nsoperationqueue-swiftSwift:使用NSOperation保留循環

我使用NSOperation從網上下載圖片。我注意到儀器沒有發佈任何NS操作。這會爲每個下載的圖像創建已用內存的增加。在完成塊我引用'自我'。所以我發現我創造了一個保留週期。

我在網上看了很多例子。我明白我可以使用'弱自我'或'無主自我'的捕獲列表。我試過這個完成塊,但仍然沒有發佈操作。

我的圖像加載類的代碼如下:

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

我發現了這個問題。保留週期不是通過引用self來引起的,而是通過在NSOperation的完成塊中引用NSOperation!

在函數startDownloadForUrl(...)中我聲明變量下載器。接下來我爲這個變量聲明一個完成塊。在此完成塊中,我引用變量下載程序。這導致保留週期。

我通過在完成塊中使用[unowned downloader]解決了這個問題。

這造成了另一個問題。在完成塊I中異步調用主線程。在這次調用中,使用變量downloader.imageData。由於這種異步調用,NSOperation可能已經結束,變量下載器可能不再存在。爲了避免崩潰,我爲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) 
    } 
} 
+0

我剛回來後這個答案找到你已經做到了!一個迂腐的筆記; '「//複製imageData」'你保留NSData對象,而不是複製它。 – alexkent

+0

你說得對。這不是迂腐。我改變了評論。 – Leontien