You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

URLSession与URLSessionWebSocketTask内部实例内存无法释放求助

URLSessionWebSocketTask 内存泄漏问题求助

在使用URLSession和URLSessionWebSocketTask时遇到内存泄漏问题:执行cancel任务并对Session执行finishTasksAndInvalidate后,其内部的NSURLError、NSURL、CFURLCache等实例并未被释放。连接并断开两次后,内存快照显示这些实例仍持续占用内存。

怀疑代码存在疏漏,相关代码仓库:URLSessionIssueSample,核心代码如下:

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)
    }
  }
}

排查情况

  • 通过Instrument追踪内存分配栈,发现内存被一个内部数组持有,但无法确定该数组的维护位置。
  • 尝试先cancel任务再调用finishTasksAndInvalidate销毁Session,问题仍未解决。

恳请各位提供排查思路或修复建议,谢谢!

内容的提问来源于stack exchange,提问作者Joohae Kim

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 07:50:55