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

Swift中ResponseTimeoutQueue实例未释放致应用卡顿的解决求助

类实例无法释放导致应用卡顿的解决方案

问题背景

与雷达通信数分钟后,ResponseTimeoutQueue类的实例数超过65个,引发应用卡顿。注释writeNotify()方法后应用恢复正常,尝试在调用timeoutThread.run()后设置timeoutThread = nil也无法减少实例数。

问题类代码(ResponseTimeoutQueue)

import Foundation

class ResponseTimeoutQueue{

    private let mMsgOut: [UInt8]

    private let mResponseMsgId: Int

    private let mTimeoutMilliSec: UInt32

    private let mNumRetries: Int

    private var timeoutWorkItem: DispatchWorkItem?

    init(responseMsgId: Int, msgOut: [UInt8], timeoutMilliSec: UInt32, numRetries: Int) {
        mResponseMsgId = responseMsgId
        mMsgOut = msgOut
        mTimeoutMilliSec = timeoutMilliSec
        mNumRetries = numRetries
    }

    func run() {
    
        // create a work item with the custom code
        timeoutWorkItem = DispatchWorkItem {
                 // Insert your code here
           
               var retryNum: Int = 0
               var doRetry: Bool = true
               while (doRetry) {
                   Thread.sleep(forTimeInterval: TimeInterval(self.mTimeoutMilliSec))
                   // If we are here then it means the last send did not receive a response before
                   // timing out.  Write with no timeout or num retries so we don't spawn another
                   // ResponseTimeoutQueue.
     

                   retryNum += 1
                   if (retryNum <= self.mNumRetries) {
                       SessionController.sharedController.invokeWriteData(responseMsgId: self.mResponseMsgId, bytes: self.mMsgOut)
                   } else {
                       doRetry = false
                   }
               }
               // Notify the handler.
               NotificationCenter.default.post(name: .timeOutMessage, object: -1)
           }

          //Create dispatch group
           let dispatchGroup = DispatchGroup()

          // execute the workItem with dispatchGroup
           DispatchQueue.global().async(group: dispatchGroup, execute: timeoutWorkItem!)

          //Handle code after the completion of global queue
           dispatchGroup.notify(queue: DispatchQueue.global()) {
               }

            timeoutWorkItem?.cancel()
    }

    func interrupt() {
        timeoutWorkItem?.cancel()
        timeoutWorkItem = nil
    }
}

调用run()方法的代码

func writeNotify(responseMsgId: Int, buffer: [UInt8], timeoutMilliSec: UInt32, numRetries: Int) {
    if (timeoutMilliSec > 0) {
        mResponseMonitorThreadMap[responseMsgId]?.interrupt()
        let timeoutThread: ResponseTimeoutQueue =
        ResponseTimeoutQueue(responseMsgId: responseMsgId, msgOut: buffer,
                             timeoutMilliSec: timeoutMilliSec, numRetries: numRetries)
        
        timeoutThread.run()
        mResponseMonitorThreadMap[responseMsgId] = timeoutThread
    }
}

取消超时的业务代码

func cancelResponseTimeout(responseMsgId: Int) {
    mResponseMonitorThreadMap[responseMsgId]?.interrupt()
    if mResponseMonitorThreadMap.keys.contains(responseMsgId){
        mResponseMonitorThreadMap[responseMsgId] = nil
    }
}

问题根源

  • DispatchWorkItem强引用实例:run()方法中创建的闭包默认强引用self,而DispatchQueue会持有该work item直到执行完毕。即使调用interrupt()将timeoutWorkItem置空,只要闭包还在运行(比如Thread.sleep或while循环未结束),就会一直持有ResponseTimeoutQueue实例,导致无法释放。
  • run()中立即cancel无效:刚提交work item就调用cancel(),无法终止已经启动的Thread.sleep和循环逻辑,闭包依然会持续执行并持有实例。
  • Thread.sleep无法被中断:Thread.sleep(forTimeInterval:)是阻塞调用,cancel()无法唤醒正在休眠的线程,闭包会一直执行到休眠结束,甚至完成整个重试循环。

修复方案

1. 打破闭包对self的强引用

使用[weak self]避免强引用,同时在闭包内校验实例是否存在:

timeoutWorkItem = DispatchWorkItem { [weak self] in
    guard let self = self else { return }
    var retryNum: Int = 0
    var doRetry: Bool = true
    // 每次循环都检查是否已取消
    while doRetry && !(self.timeoutWorkItem?.isCancelled ?? true) {
        // 替换Thread.sleep为可中断的延迟
        let semaphore = DispatchSemaphore(value: 0)
        let delayTask = DispatchQueue.global().asyncAfter(deadline: .now() + TimeInterval(self.mTimeoutMilliSec)) {
            semaphore.signal()
        }
        
        // 等待延迟结束,若work item被取消则提前退出
        if semaphore.wait(timeout: .distantFuture) == .timedOut || (self.timeoutWorkItem?.isCancelled ?? true) {
            delayTask.cancel()
            break
        }
        
        retryNum += 1
        if retryNum <= self.mNumRetries {
            SessionController.sharedController.invokeWriteData(responseMsgId: self.mResponseMsgId, bytes: self.mMsgOut)
        } else {
            doRetry = false
        }
    }
    // 仅当未取消时发送超时通知
    if !(self.timeoutWorkItem?.isCancelled ?? true) {
        NotificationCenter.default.post(name: .timeOutMessage, object: -1)
    }
}

2. 移除run()中无效的cancel调用

run()方法最后一行的timeoutWorkItem?.cancel()完全没必要,刚提交就取消会导致work item无法正常执行,直接删除该行。

3. 优化实例替换逻辑

在writeNotify中,先移除旧实例再创建新实例,避免旧实例被临时变量额外持有:

func writeNotify(responseMsgId: Int, buffer: [UInt8], timeoutMilliSec: UInt32, numRetries: Int) {
    guard timeoutMilliSec > 0 else { return }
    
    // 先中断并移除旧实例
    mResponseMonitorThreadMap[responseMsgId]?.interrupt()
    mResponseMonitorThreadMap.removeValue(forKey: responseMsgId)
    
    let timeoutThread = ResponseTimeoutQueue(responseMsgId: responseMsgId, msgOut: buffer, timeoutMilliSec: timeoutMilliSec, numRetries: numRetries)
    timeoutThread.run()
    mResponseMonitorThreadMap[responseMsgId] = timeoutThread
}

4. 简化cancelResponseTimeout方法

直接使用removeValue简化移除逻辑:

func cancelResponseTimeout(responseMsgId: Int) {
    mResponseMonitorThreadMap[responseMsgId]?.interrupt()
    mResponseMonitorThreadMap.removeValue(forKey: responseMsgId)
}

验证效果

修复后,当收到雷达响应调用cancelResponseTimeout时,interrupt()会标记work item为取消状态,闭包内的循环会检测到取消并退出,同时[weak self]会打破强引用,实例可以被正常释放,不会再出现实例堆积的问题。

内容的提问来源于stack exchange,提问作者Joice George

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 22:50:02