Skip to content

Commit 3572bdc

Browse files
committed
Fix a memory leak in DownloadTaskManager and DataTaskManager
`DownloadTaskManager` had a strong reference to `URLSession`, which retained its delegate, which was the `DownloadTaskManager`. This formed a retain cycle. Make the reference from `URLSession` to the delegate weak and only keep `DownloadTaskManager` alive as long as `DownloadTask`s need it. This causes the `DownloadTaskManager` to be deallocated once nobody holds a reference to it anymore and all the tasks it manages are finished. Finally, we need to call `invalidate` on the `URLSession` to tell it that we’re done and that it should release its delegate (which is now the `WeakDownloadTaskManager`wrapper). rdar://125012584
1 parent 8268caa commit 3572bdc

File tree

1 file changed

+113
-7
lines changed

1 file changed

+113
-7
lines changed

Sources/Basics/HTTPClient/URLSessionHTTPClient.swift

Lines changed: 113 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,50 @@ final class URLSessionHTTPClient {
8787
}
8888
}
8989

90-
private class DataTaskManager: NSObject, URLSessionDataDelegate {
90+
/// A weak wrapper around `DataTaskManager` that conforms to `URLSessionDataDelegate`.
91+
///
92+
/// This ensures that we don't get a retain cycle between `DataTaskManager.session` -> `URLSession.delegate` -> `DataTaskManager`.
93+
///
94+
/// The `DataTaskManager` is being kept alive by a reference from all `DataTask`s that it manages. Once all the
95+
/// `DataTasks` have finished and are deallocated, `DataTaskManager` will get deinitialized, which invalidates the
96+
/// session, which then lets go of `WeakDataTaskManager`.
97+
private class WeakDataTaskManager: NSObject, URLSessionDataDelegate {
98+
private weak var dataTaskManager: DataTaskManager?
99+
100+
init(_ dataTaskManager: DataTaskManager? = nil) {
101+
self.dataTaskManager = dataTaskManager
102+
}
103+
104+
105+
public func urlSession(
106+
_ session: URLSession,
107+
dataTask: URLSessionDataTask,
108+
didReceive response: URLResponse,
109+
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
110+
) {
111+
dataTaskManager?.urlSession(session, dataTask: dataTask, didReceive: response, completionHandler: completionHandler)
112+
}
113+
114+
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
115+
dataTaskManager?.urlSession(session, dataTask: dataTask, didReceive: data)
116+
}
117+
118+
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
119+
dataTaskManager?.urlSession(session, task: task, didCompleteWithError: error)
120+
}
121+
122+
public func urlSession(
123+
_ session: URLSession,
124+
task: URLSessionTask,
125+
willPerformHTTPRedirection response: HTTPURLResponse,
126+
newRequest request: URLRequest,
127+
completionHandler: @escaping (URLRequest?) -> Void
128+
) {
129+
dataTaskManager?.urlSession(session, task: task, willPerformHTTPRedirection: response, newRequest: request, completionHandler: completionHandler)
130+
}
131+
}
132+
133+
private class DataTaskManager {
91134
private var tasks = ThreadSafeKeyValueStore<Int, DataTask>()
92135
private let delegateQueue: OperationQueue
93136
private var session: URLSession!
@@ -96,8 +139,11 @@ private class DataTaskManager: NSObject, URLSessionDataDelegate {
96139
self.delegateQueue = OperationQueue()
97140
self.delegateQueue.name = "org.swift.swiftpm.urlsession-http-client-data-delegate"
98141
self.delegateQueue.maxConcurrentOperationCount = 1
99-
super.init()
100-
self.session = URLSession(configuration: configuration, delegate: self, delegateQueue: self.delegateQueue)
142+
self.session = URLSession(configuration: configuration, delegate: WeakDataTaskManager(self), delegateQueue: self.delegateQueue)
143+
}
144+
145+
deinit {
146+
session.finishTasksAndInvalidate()
101147
}
102148

103149
func makeTask(
@@ -110,6 +156,7 @@ private class DataTaskManager: NSObject, URLSessionDataDelegate {
110156
self.tasks[task.taskIdentifier] = DataTask(
111157
task: task,
112158
progressHandler: progress,
159+
dataTaskManager: self,
113160
completionHandler: completion,
114161
authorizationProvider: authorizationProvider
115162
)
@@ -192,6 +239,11 @@ private class DataTaskManager: NSObject, URLSessionDataDelegate {
192239
class DataTask {
193240
let task: URLSessionDataTask
194241
let completionHandler: LegacyHTTPClient.CompletionHandler
242+
/// A strong reference to keep the `DataTaskManager` alive so it can handle the callbacks from the
243+
/// `URLSession`.
244+
///
245+
/// See comment on `WeakDataTaskManager`.
246+
let dataTaskManager: DataTaskManager
195247
let progressHandler: LegacyHTTPClient.ProgressHandler?
196248
let authorizationProvider: LegacyHTTPClientConfiguration.AuthorizationProvider?
197249

@@ -202,18 +254,61 @@ private class DataTaskManager: NSObject, URLSessionDataDelegate {
202254
init(
203255
task: URLSessionDataTask,
204256
progressHandler: LegacyHTTPClient.ProgressHandler?,
257+
dataTaskManager: DataTaskManager,
205258
completionHandler: @escaping LegacyHTTPClient.CompletionHandler,
206259
authorizationProvider: LegacyHTTPClientConfiguration.AuthorizationProvider?
207260
) {
208261
self.task = task
209262
self.progressHandler = progressHandler
263+
self.dataTaskManager = dataTaskManager
210264
self.completionHandler = completionHandler
211265
self.authorizationProvider = authorizationProvider
212266
}
213267
}
214268
}
215269

216-
private class DownloadTaskManager: NSObject, URLSessionDownloadDelegate {
270+
/// This uses the same pattern as `WeakDataTaskManager`. See comment on that type.
271+
private class WeakDownloadTaskManager: NSObject, URLSessionDownloadDelegate {
272+
private weak var downloadTaskManager: DownloadTaskManager?
273+
274+
init(_ downloadTaskManager: DownloadTaskManager? = nil) {
275+
self.downloadTaskManager = downloadTaskManager
276+
}
277+
278+
func urlSession(
279+
_ session: URLSession,
280+
downloadTask: URLSessionDownloadTask,
281+
didWriteData bytesWritten: Int64,
282+
totalBytesWritten: Int64,
283+
totalBytesExpectedToWrite: Int64
284+
) {
285+
downloadTaskManager?.urlSession(
286+
session,
287+
downloadTask: downloadTask,
288+
didWriteData: bytesWritten,
289+
totalBytesWritten: totalBytesWritten,
290+
totalBytesExpectedToWrite: totalBytesExpectedToWrite
291+
)
292+
}
293+
294+
func urlSession(
295+
_ session: URLSession,
296+
downloadTask: URLSessionDownloadTask,
297+
didFinishDownloadingTo location: URL
298+
) {
299+
downloadTaskManager?.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location)
300+
}
301+
302+
public func urlSession(
303+
_ session: URLSession,
304+
task downloadTask: URLSessionTask,
305+
didCompleteWithError error: Error?
306+
) {
307+
downloadTaskManager?.urlSession(session, task: downloadTask, didCompleteWithError: error)
308+
}
309+
}
310+
311+
private class DownloadTaskManager {
217312
private var tasks = ThreadSafeKeyValueStore<Int, DownloadTask>()
218313
private let delegateQueue: OperationQueue
219314
private var session: URLSession!
@@ -222,8 +317,11 @@ private class DownloadTaskManager: NSObject, URLSessionDownloadDelegate {
222317
self.delegateQueue = OperationQueue()
223318
self.delegateQueue.name = "org.swift.swiftpm.urlsession-http-client-download-delegate"
224319
self.delegateQueue.maxConcurrentOperationCount = 1
225-
super.init()
226-
self.session = URLSession(configuration: configuration, delegate: self, delegateQueue: self.delegateQueue)
320+
self.session = URLSession(configuration: configuration, delegate: WeakDownloadTaskManager(self), delegateQueue: self.delegateQueue)
321+
}
322+
323+
deinit {
324+
session.finishTasksAndInvalidate()
227325
}
228326

229327
func makeTask(
@@ -238,6 +336,7 @@ private class DownloadTaskManager: NSObject, URLSessionDownloadDelegate {
238336
task: task,
239337
fileSystem: fileSystem,
240338
destination: destination,
339+
downloadTaskManager: self,
241340
progressHandler: progress,
242341
completionHandler: completion
243342
)
@@ -314,21 +413,28 @@ private class DownloadTaskManager: NSObject, URLSessionDownloadDelegate {
314413
let task: URLSessionDownloadTask
315414
let fileSystem: FileSystem
316415
let destination: AbsolutePath
317-
let completionHandler: LegacyHTTPClient.CompletionHandler
416+
/// A strong reference to keep the `DownloadTaskManager` alive so it can handle the callbacks from the
417+
/// `URLSession`.
418+
///
419+
/// See comment on `WeakDownloadTaskManager`.
420+
private let downloadTaskManager: DownloadTaskManager
318421
let progressHandler: LegacyHTTPClient.ProgressHandler?
422+
let completionHandler: LegacyHTTPClient.CompletionHandler
319423

320424
var moveFileError: Error?
321425

322426
init(
323427
task: URLSessionDownloadTask,
324428
fileSystem: FileSystem,
325429
destination: AbsolutePath,
430+
downloadTaskManager: DownloadTaskManager,
326431
progressHandler: LegacyHTTPClient.ProgressHandler?,
327432
completionHandler: @escaping LegacyHTTPClient.CompletionHandler
328433
) {
329434
self.task = task
330435
self.fileSystem = fileSystem
331436
self.destination = destination
437+
self.downloadTaskManager = downloadTaskManager
332438
self.progressHandler = progressHandler
333439
self.completionHandler = completionHandler
334440
}

0 commit comments

Comments
 (0)