Skip to content

Commit

Permalink
Fix severe memory leak when using async resizable cells
Browse files Browse the repository at this point in the history
Previously, the sizeChangedHandler callback set on resizable cells in BrickCollectionView was accidentally capturing a collection cell reference from outside the closure and causing a retain cycle, even though the cell was passed into the closure as an argument. This change resolves that issue so that the argument is referenced instead of any captured variables.

This also includes a test to ensure that the collection cell is deallocated properly if the brick collection view that owns it is also deallocated. I needed to make a new test async cell since the existing one uses an NSTimer which interferes with the deinitialization in ways I don't fully understand.

I recommend releasing a new version of BrickKit as soon as this is merged since this can leak a lot of memory, especially if the async brick is a collection brick containing child collection views/cells.
  • Loading branch information
klundberg committed Mar 4, 2017
1 parent d09c789 commit 1b7fab5
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 8 deletions.
13 changes: 7 additions & 6 deletions Source/ViewControllers/BrickCollectionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -531,13 +531,14 @@ extension BrickCollectionView: UICollectionViewDataSource {

if let brickCell = cell as? BrickCell {
if var resizable = brickCell as? AsynchronousResizableCell {
resizable.sizeChangedHandler = { [weak collectionView] cell in
let height = cell.heightForBrickView(withWidth: brickCell.frame.width)
if let brickCollectionView = collectionView as? BrickCollectionView {
brickCollectionView.performBatchUpdates({
brickCollectionView.layout.updateHeight(indexPath, newHeight: height)
}, completion: nil)
resizable.sizeChangedHandler = { [weak brickCollectionView] resizedCell in
guard let brickCollectionView = brickCollectionView else {
return
}
let height = resizedCell.heightForBrickView(withWidth: resizedCell.frame.width)
brickCollectionView.performBatchUpdates({
brickCollectionView.layout.updateHeight(indexPath, newHeight: height)
}, completion: nil)
}
}

Expand Down
17 changes: 15 additions & 2 deletions Tests/Cells/AsynchronousResizableBrick.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ import UIKit
import BrickKit

class AsynchronousResizableBrick: Brick {

var didChangeSizeCallBack: (() -> Void)?
var newHeight: CGFloat = 200

}

class AsynchronousResizableBrickCell: BrickCell, Bricklike, AsynchronousResizableCell {
Expand All @@ -35,5 +33,20 @@ class AsynchronousResizableBrickCell: BrickCell, Bricklike, AsynchronousResizabl
sizeChangedHandler?(cell: self)
brick.didChangeSizeCallBack?()
}
}

class DeinitNotifyingAsyncBrickCell: BrickCell, Bricklike, AsynchronousResizableCell {
typealias BrickType = DeinitNotifyingAsyncBrick

var sizeChangedHandler: CellSizeChangedHandler?

deinit {
NSNotificationCenter.defaultCenter().postNotificationName("DeinitNotifyingAsyncBrickCell.deinit", object: nil)
}
}

class DeinitNotifyingAsyncBrick: Brick {
override class var cellClass: UICollectionViewCell.Type? {
return DeinitNotifyingAsyncBrickCell.self
}
}
12 changes: 12 additions & 0 deletions Tests/ViewControllers/BrickCollectionViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -683,5 +683,17 @@ class BrickCollectionViewTests: XCTestCase {
XCTAssertEqual(innerSection.brickCollectionView, brickView)
}

func testThatBrickCollectionViewDoesNotCreateARetainCycleWithAsyncrhonousResizableCells() {

expectationForNotification("DeinitNotifyingAsyncBrickCell.deinit", object: nil, handler: nil)

autoreleasepool {
let brick = DeinitNotifyingAsyncBrick(size: BrickSize(width: .Fill, height: .Fill))

brickView.setupSingleBrickAndLayout(brick)
brickView = nil
}

waitForExpectationsWithTimeout(1, handler: nil)
}
}

0 comments on commit 1b7fab5

Please sign in to comment.