Internal instances of `URLSession` of `URLSessionWebSocketTask` are not being released in memory

79 views Asked by At

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.

Memory capture after connect and disconnect 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!

1

There are 1 answers

0
Joohae Kim On

I found a document told not to use URLSessionWebSocketTask

Unless you have a specific reason to use URLSession, use Network framework for new WebSocket code. For more options, see WebSocket alternatives. https://developer.apple.com/documentation/technotes/tn3151-choosing-the-right-networking-api

and received an email advice to use NWConnection https://developer.apple.com/documentation/network/nwconnection

I wish my answer can save some others time