Unexpected memory leak (dependent on 3rd party code)

Okay I feel lost by this behavior. I'm currently debugging memory leaks in our application and having trouble understanding those. I found one really weird memory leak always reproducible which makes no sense to me. Any chance this is a false positive or how can I investigate further the 'why' and 'what' leaks?

The issue I'm going to explain next is dependent on two 3rd party libraries, I apologize for that up in front, but I wasn't able to re-produce the issue with just one of those and/or types from the stdlib.

Imagine an app target (iOS) with the whole code to re-produce the issue in the AppDelegate.swift, which has both frameworks included (I setup a small project ASAP for you to test):

import UIKit
import RxSwift 
// depends on `RxSwift` and we need one type from that lib to 
// re-produce the issue/bug
import RxBluetoothKit

final class AppDelegate : UIResponder {
  // A type that can emits some values to an observable stream that it 
  // also provides (from RxSwift)
  let object = PublishSubject<Any>()
  // A type that breaks ref-cycles which are part of RxSwift's design
  let disposeBag = DisposeBag()

  // The top two objects are just there so we can reproduce the issue.
  // `PublishSubject` can be replaced with other similar types and the
  // issue will still be there. `DisposeBag` is just there so that folks that
  // know RxSwift see that the issue is not part of the disposing process.

extension AppDelegate : UIApplicationDelegate {
  // Default entry of an iOS app where we want quickly re-produce the issue.
  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions:
      [UIApplicationLaunchOptionsKey: Any]?
  ) -> Bool {
    // More Infos: Types like `PublishSubject` are observable types and have
    // quite a few operators like `flatMap`, `flatMapLatest`, `scan` etc.
    // We're going to use only `flatMap` and `never` to demonstrate the memory leak.
    // func flatMap<O>(_ selector: @escaping (Self.E) throws -> O) -> RxSwift.Observable<O.E> where O : ObservableConvertibleType 
    // An observable sequence whose observers will never get called.
    // static func never() -> RxSwift.Observable<Self.E>
    // `subscribe(onNext: ((Self.E) -> Void)?)`

    // Test 1 - Leaks memory
    // `CentralManagerRestoredState` is from `RxBluetoothKit`, it's a struct
    // which if we use it will produce a memory leak
    let closure: (CentralManagerRestoredState) -> Void = { _ in }
      .flatMap { 
         // We're ignoring the parameter
         _ -> Observable<CentralManagerRestoredState> in 
         // Transform to an observable that never emits anything, so that the code 
         // compiles. This is not related to the issue, just for compilation purposes.
      // Here is a nested closure that is somehow related!!
      .subscribe(onNext: { closure($0) })
      .disposed(by: disposeBag)

    // Test 2 - Does NOT leak memory
    // We've removed the outer closure, and pass the `closure` instance
    // directly to `subscribe`.
      .flatMap { _ -> Observable<CentralManagerRestoredState> in .never() }
      .subscribe(onNext: closure) // No nested closure
      .disposed(by: disposeBag)

    // Test 3 - Does NOT leak memory
    // We've changed only the type from Test 1 but still nest the closure
    let otherClosure: (UIView) -> Void = { _ in }
      .flatMap { _ -> Observable<UIView> in .never() }
      .subscribe(onNext: { otherClosure($0) })
      .disposed(by: disposeBag)

    // Test 4 - Does NOT leak memory
    // No nested closure, and a different type.
      .flatMap { _ -> Observable<UIView> in .never() }
      .subscribe(onNext: otherClosure)
      .disposed(by: disposeBag)

To sum up:

  • I tried different types in places where CentralManagerRestoredState is written, but could not re-produce the issue with simpler types like String or UIView.
  • If we're removing the outer closure but still use CentralManagerRestoredState then the leak disappears.
  • There is no leak if we're not using CentralManagerRestoredState like in Test 3 and 4 from above.

This is super weird behavior and I'm not sure what is happening. It's almost never re-producible in the simulator, but always a real device. The compilation mode is set to Single File but it also appears with Whole Module optimization.

TBH this looks like a bug in Swift rather than an issue with the 3rd party library since a random type from which we use the type signature in the examples above starts with a combination of nested closure to leak memory.

I found the issue here and reduced it to the example from above which shows that only two things were relevant (nested closures and one random type).

Update: I found the same issue in our own codebase. The issue is related to nesting a function into a closure.

In the new case I have to wrote code like the following one to avoid a retain cycle:

.subscribe(onNext: { [unowned self] in self.doSomething($0) }) // this will leak

While I have to avoid the following since the subscribe function will escape the function passed to it:

.subscribe(onNext: doSomething) // will NOT leak but can create a ref-cycle due to implicit `self.` being captured

Let's see if I can create a new example without the dependency to RxBluetoothKit. Maybe I'll also find a way to reproduce it without RxSwift in general. This is either a false positive or true leaked memory due to some kind of a bug in Swift.

Did you tell which object(s) are leaked?

Sorry I didn't. In ever case it was Swift closure context and Malloc that was identified leaking memory.

Not the easiest beasts to track :-)