ReactiveCocoa / ReactiveSwift

Streams of values over time

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Memory leak using MutableProperty.map

FranDepascuali opened this issue · comments

commented

Hey there! First of all, thanks for this awesome framework. It really shaped the way I code and I prefer it vastly over other options.

Today I had a memory leak I wanted to share:

class Foo {
    fileprivate var planes: [UUID: Plane] = [:]
    fileprivate(set) var plane: ReactiveSwift.Property<Plane?>!
    fileprivate let _currentPlaneIdentifier: MutableProperty<UUID?>

    init() {
        ...
        plane = _currentPlaneIdentifier.map { UUID in UUID.flatMap { [unowned self] in self.planes[$0] }}
    }
}

It's difficult to spot at a first sight, but the fix is to change

plane = _currentPlaneIdentifier.map { UUID in UUID.flatMap { [unowned self] in self.planes[$0] }}

to

plane = _currentPlaneIdentifier.map { [unowned self] UUID in UUID.flatMap { self.planes[$0] }}

As I understand, there is a memory leak in the first because the mutable property retains self via the closure (but as the closure isn't executed, self is not deallocated)? This example confuses me, if someone can provide some feedback it would be great.

Thanks!

(NOTE that the internal flatMap is optional's flatMap)

This is a gotcha of ARC that applies outside of ReactiveSwift as well (including in Objective-C). Here's something similar to your example using NotificationCenter:

// (1)
self.observation = NotificationCenter.default.addObserver(forName: Foo.notification, object: nil, queue: nil) { notification in
  let uuid = notification.userInfo?[Foo.uuidKey] as? UUID
  // (2)
  uuid.flatMap { [unowned self] in
    completionHandler(self.planes[$0])
  }
}

The main thing to note here is that when the [unowned self] capture list is evaluated, self is in the context of the first closure, (1). This creates a strong reference to self in the outer closure, in order to make the unowned reference in the inner closure, (2). This is effectively what the capture list does:

unowned let self = self // on the rhs, self is strong
uuid.flatMap { self.planes[$0] }

Hopefully that clears up where the strong reference is coming from.

commented

@sharplet Thank you very much!