Confused about Strong Reference Cycles: Why do these two codes not behave the same?

When I run the MyApiCaller my instance of MyApiCaller calls the deinit and gets deallocated. However when I run the UserApi instance, I've called test in this example the UserApi instance's deinit does not run. What is the difference between these two pieces of code?

// MyApiCaller starts here
class MyApiCaller {
var gotData: Bool = false

func getTodos() {
    func test(data: Data?, resp: URLResponse?, err: Error?) {
        guard let todos = data else {
            print("No data")
            return
        }
        self.gotData = true
        print("Got data \(self.gotData) \(todos)")
    }
    
    let url = URL(string: "https://jsonplaceholder.typicode.com/todos")
    URLSession.shared.dataTask(with: url!, completionHandler: test)
    .resume()
    print("getTodos complete")
}

deinit {
    print("MyApiCaller is being deinitialized")
}
}

var caller: MyApiCaller? = MyApiCaller()
caller?.getTodos()
caller = nil

// UserApi starts here
class UserApi: BaseApi {
let domainRoot: String
let encoder: JSONEncoder
let decoder: JSONDecoder

init(domainRoot: String, encoder: JSONEncoder, decoder: JSONDecoder) {
    self.domainRoot = domainRoot
    self.encoder = encoder
    self.decoder = decoder
}

deinit {
    print("UserApi is deinit")
}

fileprivate static var people: [User] = [
    User(userName: "mrglasses"),
    User(userName: "longhair"),
    User(userName: "mylady"),
]

func currentLoggedInUser() -> User? {
    return UserApi.people[1]
}

func getFollowing() {
    let url = URL(string: "http://127.0.0.1:8080/users")
    
    URLSession.shared.dataTask(with: url!) {  (data, resp, err) in
        guard let data = data else {
            print("getFollowing data is nil")
            return
        }
        
        do {
            
            let followingUsers = try self.decoder.decode([User].self, from: data)
            //callback(followingUsers)
        } catch {
            print("getFollowing decode failed")
        }
        print("closure is ended")
    }
    .resume()
}

func addPerson(person: User) {
    UserApi.people.append(person)
}

func updatePerson(person: User) {
    var foundPerson = UserApi.people.first { p in
        p.id == person.id
    }
    foundPerson?.userName = person.userName
}
}

var test: UserApi? = UserApi(domainRoot: "http://127.0.0.1:8080", encoder: JSONEncoder(), decoder: JSONDecoder())
    currentUser = test?.currentLoggedInUser()
    test?.getFollowing()
    test = nil

On my machine, deinit is executed on both classes. Though I don't have BaseApi or User, so I need to strip that out, but I don't think that should affect the outcome. Note also that the URLSession can take a long time, so it is also possible that the deinit is delayed.

1 Like

Thank you so much for trying. Wow I'm now even more confused. Let's start with this question then. Why do they deinit even though the object instance and the closure instance both point to each other? In my own mind I could say well it's because the closure eventually completes and therefore goes out of scope.

But one could also argue the same thing about an object to object association no? The example below is the classic example of a strong reference cycle, but isn't this the same thing as what's happening in the object to closure case. An object instance, UserApi, exists. A closure is instantiated and captures the object instance. Now they both point to each other. Except that the closure is auto dereferencing by eventually going out of scope.

var obj1 = MyObjA()
var obj2 = MyObjB()
obj1.neighbor = obj2
obj2.neighbor = obj1
obj1 = nil
obj2 = nil

Let's draw the reference graph. For MyAPICaller:

URLSession.shared
(->) returned DataTask
-> test(data:esp:err:)
-> MyAPICaller

caller
-> MyAPICaller

I don't see a reference cycle here. Note though that I'm not sure if the shared URLSession retains the DataTask while it's executing. You need to check the doc for that. Still, it doesn't cause cycle either way.

Same with UserAPI.

1 Like

I really appreciate your help with this. So let me ask the question this way. When I read this link, Closures — The Swift Programming Language (Swift 5.7) (near the top it lists out global, nested, and closure expressions). It looks like to me that closures are basically unnamed nested functions. Which is why they have access to all the nearby context members and fields. For example the self in this example. This effectively makes the closure being passed to dataTask a nested function of getFollowing and this would normally give it the lifetime of the parent, UserApi. However the closure parameter to dataTask is @escaping. Which means that the closure then effectively has a lifetime that is greater than both the dataTask and the UserApi instance.

Why is that not sufficient to cause a strong reference cycle? The closure is a reference type with a reference to the UserApi instance (the capture of self). The UserApi instance also has a reference to the closure expression because it was declared inside one of the UserApi instances members (getFollowing). What am I missing?

Again thank you for your help.

That is correct to a very large extent. There is some minor nuances, but those don't apply here.

Escaping a closure by itself doesn't cause a reference cycle. Here are two simpler scenarios:

class A {
  var closure: () -> () = {}
  deinit { print("Deinit-ing A") }
}

var escapedClosure: (() -> ())?

func good(a: A, x: @escaping () -> ()) {
  escapedClosure = x
}
func bad(a: A, x: @escaping () -> ()) {
  a.closure = x
}

// Scenario 1
var a: Optional = A()
good(a: a!) { [a] in print(a!) } // No cycle
a = nil
print("A not deinited yet")
escapedClosure = nil // Deinit A
print("A is deinited")

// Scenario 2
a = A()
bad(a: a!) { [a] in print(a!) } // Has cycle
a = nil // A not deinited

In the first scenario, closure x captures a, and outlive the good function lifetime, hence @escaping. Still, the lifetime of x is still as long as escapedClosure. That's why, when we set escapedClosure to nil, x is destroyed, releasing the only reference to a, and a is also destroyed.

In the second scenario, however, x is stored into a.closure. So when a is set to nil, a still holds reference to x, which holds a reference to a. This is very problematic because we can no longer access from anywhere in the program, but a is still referenced (by itself), and so are not deinit-ed. That's why the reference cycle is a problem. It prevents the program from destroying an instance that's no longer used by anyone.

Your MyAPICaller is a good scenario:

  • a is the caller
  • x is test(data:resp:err:)
  • escapedClosure is stored somewhere by URLSession.shared

So while the MyAPICaller instance can outlive the variable caller, it will still get destroyed once URLSession.shared use the closure and stop referencing them.

UserAPI itself doesn't store the declared closure. You need to assign it to a variable, which is presumably done somewhere inside .shared. So .shared has a reference to the closure, but UserAPI doesn't.

1 Like