Skip to content

Commit dca7b2c

Browse files
committed
[iOS] Eliminate strong retain cycle from VSyncClient
1 parent 5632af9 commit dca7b2c

2 files changed

Lines changed: 36 additions & 1 deletion

File tree

engine/src/flutter/shell/platform/darwin/ios/framework/Source/VSyncClient.swift

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ public class VSyncClient: NSObject {
5858

5959
super.init()
6060

61-
let link = CADisplayLink(target: self, selector: #selector(onDisplayLink(_:)))
61+
let relay = DisplayLinkRelay(target: self)
62+
let link = CADisplayLink(target: relay, selector: #selector(DisplayLinkRelay.onDisplayLink(_:)))
6263
link.isPaused = true
6364
self.displayLink = link
6465

@@ -186,3 +187,21 @@ public class VSyncClient: NSObject {
186187
callback(timestamp, targetTimestamp)
187188
}
188189
}
190+
191+
/// A weak proxy target for `CADisplayLink` callbacks to prevent retain cycles.
192+
///
193+
/// `CADisplayLink` strongly retains its target. If the display link directly targeted `VSyncClient`,
194+
/// it would form a strong retain cycle (since `VSyncClient` also strongly retains the `CADisplayLink`
195+
/// instance). Instead, we route display link callbacks through this intermediate relay holding `VSyncClient` weakly.
196+
private final class DisplayLinkRelay: NSObject {
197+
private weak var target: VSyncClient?
198+
199+
init(target: VSyncClient) {
200+
self.target = target
201+
}
202+
203+
@objc
204+
func onDisplayLink(_ link: CADisplayLink) {
205+
target?.onDisplayLink(link)
206+
}
207+
}

engine/src/flutter/shell/platform/darwin/ios/framework/Source/VSyncClientTest.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,4 +197,20 @@ class VSyncClientTest: XCTestCase {
197197
waitForExpectations(timeout: 1.0, handler: nil)
198198
XCTAssertNil(weakClient)
199199
}
200+
201+
func testDeallocatesWithoutExplicitInvalidation() {
202+
let threadTaskRunner = TaskRunnerTestHelper.makeTaskRunner(withLabel: "VSyncClientTest")
203+
weak var weakClient: VSyncClient?
204+
205+
autoreleasepool {
206+
let client = VSyncClient(
207+
taskRunner: threadTaskRunner,
208+
isVariableRefreshRateEnabled: false,
209+
maxRefreshRate: 60.0
210+
) { _, _ in }
211+
weakClient = client
212+
}
213+
214+
XCTAssertNil(weakClient)
215+
}
200216
}

0 commit comments

Comments
 (0)