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
@UIApplicationMain
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 }
object
.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.
.never()
}
// 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`.
object
.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 }
object
.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.
object
.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 likeString
orUIView
. - 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).