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

AsyncThrowingStream搭配OS Logger引发微小内存泄漏成因咨询

AsyncThrowingStream中catch块调用Logger引发1字节内存泄漏的原因分析

问题场景

使用Xcode自带的Leaks工具对AsyncThrowingStream进行内存检查时,发现catch块中调用Logger会引发1字节的微小内存泄漏。相关代码如下:

private lazy var stream: AsyncThrowingStream<SomeData, Error> = {
    AsyncThrowingStream<SomeData, Error> { [weak self] continuation in
        guard let self else { return }
        self.continuation = continuation
        streamTask = Task {
            var isAlive = true
            while isAlive && self.task?.closeCode == .invalid {
                do {
                    if let message = try await self.task?.receive() {
                        Self.logger.trace("\(SocketStrings.streamMessageYielded())")
                        if let socketData = SocketMessageMapper.map(message) {
                            self.track(socketData)
                            continuation.yield(socketData)
                        }
                    } else {
                        throw APIError.genericError
                    }
                } catch {
                    Self.logger.critical("\(SocketStrings.streamMessageNotReceived(error.localizedDescription))")
                    continuation.finish(throwing: error)
                    isAlive = false
                }
            }
        }
    }
}()

Leaks工具检测到泄漏发生在catch块内的Logger调用行:

} catch {
    Self.logger.critical("\(SocketStrings.streamMessageNotReceived(error.localizedDescription))")
...

可能的原因

1. Error对象的临时内存残留

在拼接日志字符串时,error.localizedDescription会生成临时的String实例。在异步Task的上下文下,ARC对这个临时对象的回收可能存在微小延迟,被Leaks工具捕捉为1字节的泄漏。这类小内存块通常是字符串的附加标记位或底层存储的微小残留。

2. AsyncThrowingStream与Task的生命周期衔接问题

虽然代码中使用了[weak self]避免循环引用,但streamTask作为self的属性持有了Task的强引用,而Task内部又访问了self的task属性。当catch块调用continuation.finish()后,Task的收尾逻辑可能存在微小的内存块没有被及时释放,Logger的调用刚好成为了泄漏的检测触发点。

3. Logger框架的内部临时对象

如果使用的是系统OSLog或第三方Logger框架,在处理日志输出时可能会生成一些元数据临时对象。这些对象的内存可能因为异步调度队列的原因,没有被立即回收,被Leaks工具判定为泄漏。1字节的规模通常是框架内部的状态标记或小枚举值残留。

验证与解决建议

  • 优化Error描述的获取时机:将error.localizedDescription提前存储为局部变量,再传入Logger,避免字符串拼接时的临时对象残留:
    } catch {
        let errorDescription = error.localizedDescription
        Self.logger.critical("\(SocketStrings.streamMessageNotReceived(errorDescription))")
        continuation.finish(throwing: error)
        isAlive = false
    }
    
  • 完善Task的生命周期管理:在对象的deinit方法中主动取消streamTask,确保Task能及时释放内部资源:
    deinit {
        streamTask?.cancel()
    }
    
  • 确认是否为误报:1字节的泄漏通常不会影响应用性能,可通过多次重复测试、更换测试设备或系统版本,确认是否是稳定复现的泄漏,或是Leaks工具的偶发误报。

内容的提问来源于stack exchange,提问作者Kuba Gawecki

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 13:16:12