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
相关产品推荐
相关产品推荐

