I have an issue on URLSession
and URLSessionWebSocketTask
that the internal instances of the classes are not released after being invalidated and canceled in both of the classes.
I expected the instances of the classes NSURLError
, NSURL
, CFURLCache
... to be deallocated after invalidating and canceling them, but as you can see from the screenshot, they are not released.
I took the screenshot below after connecting and disconnecting twice.
I think I missed something in my code, can someone take a look at my code? Any ideas or suggestions? Thanks!
Here is my repo for the case: https://github.com/Joohae/URLSessionIssueSample
and the source code, the files are the same file from the repo above
ViewController.swift
import UIKit
class ViewController: UIViewController {
public var webSocket: VKWebsocket?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let button = UIButton()
button.layer.borderColor = UIColor.black.cgColor
button.layer.borderWidth = 1
button.setTitle("Hit me!", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(didTapButton(_:)), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(button)
view.addConstraints([
NSLayoutConstraint(item: button, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1, constant: 0),
NSLayoutConstraint(item: button, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1, constant: 0),
NSLayoutConstraint(item: button, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: 200),
NSLayoutConstraint(item: button, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: 100),
])
}
@objc func didTapButton(_ button: UIButton) {
if webSocket == nil {
connect()
} else {
disconnect()
}
}
private func connect() {
Task {
webSocket = VKWebsocket(request: URLRequest(url: URL(string: "wss://socketsbay.com/wss/v2/1/demo/")!))
await webSocket?.setDelegate(self)
await webSocket?.connect()
}
}
private func disconnect() {
Task {
await webSocket?.disconnect()
}
}
}
extension ViewController: WebsocketDelegate {
func websocket(_ websocket: VKWebsocket, didReceive message: URLSessionWebSocketTask.Message) {
print("\(#function): \(message)")
}
func websocket(_ websocket: VKWebsocket, didError error: Error?) {
if error != nil {
print("\(#function): \(String(describing: error))")
} else {
print("\(#function): the message has been sent")
}
}
func websocket(_ websocket: VKWebsocket, didChange connectionState: VKWebsocket.ConnectionState) {
Task {
print("\(#function): \(connectionState)")
switch connectionState {
case .connected:
let message = "Have a good day!"
print("Connected, and seinding message '\(message)'")
await websocket.sendMessage(message: .string(message))
case .connecting:
print("Connecting...")
case .disconnected:
print("Disconnected!")
self.webSocket = nil
}
}
}
}
VKSocket.swift
import Foundation
public protocol WebsocketDelegate: AnyObject {
func websocket(_ websocket: VKWebsocket, didReceive message: URLSessionWebSocketTask.Message)
func websocket(_ websocket: VKWebsocket, didError error: Error?)
func websocket(_ websocket: VKWebsocket, didChange connectionState: VKWebsocket.ConnectionState)
}
public actor VKWebsocket {
public enum ConnectionState {
case connected
case connecting
case disconnected
}
private(set) weak var delegate: WebsocketDelegate?
private(set) var reconnectOnFailure = true
private(set) var connectionState = ConnectionState.disconnected {
didSet {
guard oldValue != connectionState else { return }
let newState = connectionState
self.delegate?.websocket(self, didChange: newState)
}
}
private let request: URLRequest
private var session: URLSession?
private var task: URLSessionWebSocketTask?
private var messagesQueue: [URLSessionWebSocketTask.Message] = []
private lazy var urlSessionDelegate = WebsocketURLSessionDelegate(websocket: self)
public init(request: URLRequest) {
self.request = request
}
deinit {
print("deinit \(type(of: self))")
task?.cancel()
session?.finishTasksAndInvalidate()
}
public func setDelegate(_ delegate: WebsocketDelegate) {
self.delegate = delegate
}
public func connect() {
guard connectionState == .disconnected else { return }
connectionState = .connecting
session = URLSession(configuration: .ephemeral,
delegate: urlSessionDelegate,
delegateQueue: nil)
task = session?.webSocketTask(with: request)
task?.resume()
}
public func disconnect() {
task?.cancel(with: .normalClosure, reason: nil)
session?.finishTasksAndInvalidate()
session = nil
task = nil
connectionState = .disconnected
}
public func sendMessage(message: URLSessionWebSocketTask.Message) {
guard connectionState == .connected else {
messagesQueue.append(message)
return
}
// VKWebsocket does not re-enqueue messages that fail. Clients implementing VKWebsocket
// should handle resending failed messages.
task?.send(message, completionHandler: { [weak self] error in
guard let self = self else { return }
Task {
await self.delegate?.websocket(self, didError: error)
}
})
}
private func awaitMessage() {
guard connectionState == .connected else { return }
task?.receive(completionHandler: { [weak self] result in
guard let self = self else { return }
Task {
await self.onReceive(result: result)
}
})
}
private func onReceive(result: Result<URLSessionWebSocketTask.Message, Error>) {
switch result {
case .success(let message):
self.delegate?.websocket(self, didReceive: message)
self.awaitMessage()
case .failure(let error):
self.delegate?.websocket(self, didError: error)
self.handleFailure()
}
}
private func handleFailure() {
disconnect()
}
fileprivate func websocketConnectionDidOpen() {
connectionState = .connected
awaitMessage()
messagesQueue.forEach { message in
self.sendMessage(message: message)
}
messagesQueue = []
}
fileprivate func websocketConnectionDidClose() {
connectionState = .disconnected
}
fileprivate func websocketConnectionDidError(_ error: Error?) {
delegate?.websocket(self, didError: error)
handleFailure()
}
}
// URLSession strongly retains its delegate, for whatever reason. We use an intermediate
// class to break the resulting retain cycle.
private class WebsocketURLSessionDelegate: NSObject, URLSessionWebSocketDelegate {
weak var websocket: VKWebsocket?
init(websocket: VKWebsocket) {
self.websocket = websocket
}
deinit {
print("deinit \(type(of: self))")
}
func urlSession(_ session: URLSession,
webSocketTask: URLSessionWebSocketTask,
didOpenWithProtocol protocol: String?) {
Task {
await self.websocket?.websocketConnectionDidOpen()
}
}
func urlSession(_ session: URLSession,
webSocketTask: URLSessionWebSocketTask,
didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
reason: Data?) {
Task {
await self.websocket?.websocketConnectionDidClose()
}
}
func urlSession(_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?) {
// This method will be called when you deinit VKWebsocket while connection is open.
Task {
await self.websocket?.websocketConnectionDidError(error)
}
}
}
I tracked down the memory allocation stack from instrument, I can see the memory is holded by an internal array, but I have no idea where the array is maintained.
I tried to invalidate after cancel the task, but it wasn't helped
I appreciate any hint or advice thank you!
I found a document told not to use
URLSessionWebSocketTask
and received an email advice to use NWConnection https://developer.apple.com/documentation/network/nwconnection
I wish my answer can save some others time