Jon_Shier
(Jon Shier)
1
I'm attempting to track down the cause of a reference cycle in Alamofire. I've narrowed it down to this code:
@discardableResult
public func validate<S: Sequence>(contentType acceptableContentTypes: @escaping @autoclosure () -> S) -> Self where S.Iterator.Element == String {
return validate { [unowned self] _, response, data in
return self.validate(contentType: acceptableContentTypes(), response: response, data: data)
}
}
@discardableResult
public func validate() -> Self {
return validate(statusCode: acceptableStatusCodes).validate(contentType: { [weak self] () -> [String] in
return ["*/*"]
}())
}
}
Any self reference in the contentType closure (inline for testing in this case) causes a reference cycle, even if self is never used within the closure. This occurs for both weak and unowned captures. The same closure, without the capture, works fine, as does a direct array value. So where's the strong reference here?
1 Like
pyrtsa
(Pyry Jahkola)
2
Could it be that in the forwarding method call, your @escaping @autoclosure parameter actually captures self, through the member acceptableContentTypes, strongly?
Edit: The workaround might be to bind (self.)acceptableContentTypes to a local variable before the method call and use that value instead.
Jon_Shier
(Jon Shier)
3
There doesn't seem to be an implicit capture anywhere, as the cycle only appears if the contentType closure explicitly captures self. It's the fact that the cycle happens whether we capture unowned or weak, even if self is never used within the closure, that's really confusing.
pyrtsa
(Pyry Jahkola)
4
Can you show me an example where self isn't used in the capture and the reference cycle still exists? Because in the above example, there's an implicit self.
pyrtsa
(Pyry Jahkola)
5
…In this (sub)expression. ^
Jon_Shier
(Jon Shier)
6
That expression is irrelevant to the behavior, as acceptableStatusCodes is just an Array. The only thing that makes a difference is the capture seen in the contentType closure. Capturing self into that closure at all triggers the cycle, omitting the capture does not.
pyrtsa
(Pyry Jahkola)
7
Unless it's a global, it appears to be an array-valued member of self, no? And since it is passed as an escaping autoclosure argument, its evaluation is delayed all the way until acceptableContentTypes() in the first method. So there's the implicit capture of self, strongly.
Am I making sense? 
Jon_Shier
(Jon Shier)
8
No, as the status code version of validate is a different code path that does not take an @autoclosure. That validator by itself doesn't not produce a cycle.
pyrtsa
(Pyry Jahkola)
9
Can you try this workaround nevertheless, and see if it changes things?
@discardableResult
public func validate() -> Self {
let workaround = acceptableStatusCodes
return validate(statusCode: workaround).validate(contentType: { [weak self] () -> [String] in
return ["*/*"]
}())
}
}
Jon_Shier
(Jon Shier)
10
I'll go one better: removing validate(statusCode: acceptableStatusCodes) altogether doesn't change the behavior I'm seeing. It only triggers when capturing self into the contentType closure.
pyrtsa
(Pyry Jahkola)
11
No but that's the same thing! When—inside an @autoclosured expression—you declare to capture weak self, that capturing is delayed all the way until that autoclosure argument is evaluated.
Edit: You could better avoid this problem if autoclosure expressions could have their own capture lists (ugly!) or if there existed syntax for passing ("splatting") a closure as the autoclosure argument.
Jon_Shier
(Jon Shier)
12
Now that's something I hadn't considered: an implicit capture by the @autoclosure since it needs to pass self into the closure when the outer @autoclosure is called. Let me see if I can trigger a difference.
1 Like
pyrtsa
(Pyry Jahkola)
13
@escaping and @autoclosure together should be read as a warning sign. If it needs to escape, better make it a closure at the call site so that it remains explicit.
Jon_Shier
(Jon Shier)
14
It seems like this avoids this issue:
{
@discardableResult
public func validate() -> Self {
let contentType: () -> [String] = { [unowned self] in
return self.acceptableContentTypes
}
return validate(statusCode: self.acceptableStatusCodes).validate(contentType: contentType())
}
}
As for the usage of @escaping @autoclosure, that seems to be the only way to capture self lazily so that users could validate the received Content-Type matches the sent Accept, or other behaviors.
I'll test this solution more, but I think this will work. Thanks!
2 Likes
Actually you can use @escaping @autoclosure and capture weak self. Just do the next
var str: String = "Some value"
func validate(arg: @escaping @autoclosure () -> String?) { }
func main() {
weak var weakSelf = self
validate(arg: weakSelf?.str)
}