2015-12-30 59 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