Does ‘KeyPath’ produce memory leaks?

Hello there!

When using KeyPath, it seems like the object remains in memory.
Do you think it's a bug? Or is it because for performance reasons, it is kept in memory like a static variable after the first use?
Please let me know if i'm missing something.

Here's an example code:

class Foo {
    let name = "Hello World"
}

var foo: Any = Foo()
Observer.onDeinit(for: foo as! Foo) {
    print("🔥 deinit foo")
}
foo = 0

var keyPath: Any = \Foo.name
Observer.onDeinit(for: keyPath as! KeyPath<Foo, String>) {
    print("🔥 deinit keyPath")
}
keyPath = 0

// Prints 🔥 deinit foo

Observer is implemented as below.

protocol ObjectType: AnyObject { }

@objc class DeinitHandler: NSObject {

    var blocks: [() -> Void] = []

    deinit {
        blocks.forEach({ $0() })
    }
}

enum Observer {

    static var deinitKey = "DEINIT_KEY"

    static func onDeinit(
        for object: ObjectType,
        block: @escaping () -> Void
    ) {
        let handler = deinitHandler(for: object)
        handler.blocks.append(block)
    }

    private static func deinitHandler(for object: ObjectType) -> DeinitHandler {
        if let handler = objc_getAssociatedObject(object, &deinitKey) as? DeinitHandler {
            return handler
        } else {
            let handler = DeinitHandler()
            objc_setAssociatedObject(object, &deinitKey, handler, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            return handler
        }
    }
}

extension KeyPath: ObjectType { }
extension Foo: ObjectType { }

Yes, KeyPath instances are cached on first use and remain in memory until the end of the program. cc: @Joe_Groff

4 Likes

Thanks for the answer!

More specifically, this optimization only occurs for literal key paths that don't capture any generic type information or subscript indices. If you do something like:

protocol P { var name: String { get } }

extension Foo: P {}

func getKeyPath<T: P>(for: T.Type) -> KeyPath<T, String> { return \T.name }

then that key path will be destroyed normally, since it depends on the type of T that is passed in.

6 Likes

Is there any document for this answer? I can't find it

Thank you :pray:.