问题
In my app I use an image loader class to load images from the web for a collection view. 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.
I use NSOperation for downloading an image from the web. I noticed with Instruments that none of the NSoperations is released. This creates an increase of the used memory for each image that is downloaded. In the completion block I references '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.
回答1:
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!
In the function startDownloadForUrl(...) I declare the variable downloader. 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.
This created anaother problem. In the completion block I asynchronously call the main thread. In this call the variable downloader.imageData was used. Because of this asynchronous call, the NSOperation may be already ended and the variable downloader may not longer exists. To avoid crashes I declare a new variable for the imageData, so the data will still be available when used in the main thread.
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)
}
}
来源:https://stackoverflow.com/questions/34528333/swift-retain-cycle-with-nsoperation