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

macOS应用程序中奇怪的内存泄漏问题求助

超大型XML解析的内存泄漏问题排查与修复

问题背景

  • 处理1.7TB超大型XML文件,原生XMLParser解析至5700万行时卡住,内存占用稳定在7-7.2GB(M1芯片,8GB物理内存)
  • 自行实现流解析后,处理3-4GB内容时内存飙升至12-13GB,最终突破20GB被迫终止

核心内存问题分析

  1. 日志数组无限累积:logs作为@State数组持续存储所有日志条目,无任何清理逻辑,随着处理量增加,内存会持续上涨
  2. 字符串创建错误:使用String(bytesNoCopy:)时,错误传入缓冲区总长度l(128MB)而非实际读取的numberOfBytesRead,导致字符串包含大量无效数据,额外占用内存
  3. 临时对象未及时回收:并发队列循环中未手动管理自动释放池,拆分字符串生成的大量临时对象无法及时回收,堆积在内存中
  4. Unsafe指针未释放:手动分配的UnsafeMutablePointer<UInt8>未调用deallocate(),造成永久内存泄漏
  5. 队列自动释放策略不合理:队列autoreleaseFrequency设为.inherit,无法及时清理任务产生的临时对象

修复方案

1. 限制内存日志数量

避免将所有日志存入内存,只保留最近N条或改用文件日志:

// 替换原logs.append逻辑
DispatchQueue.main.async {
    self.logs.append("[\(count)] String buffer count: \(str.count), Lines count: \(lines.count)")
    if self.logs.count > 100 { // 仅保留最新100条日志
        self.logs.removeFirst()
    }
}

2. 修正字符串创建逻辑

使用实际读取的字节数创建字符串,避免无效内存占用:

str = String(bytesNoCopy: buf, length: numberOfBytesRead, encoding: .utf8, freeWhenDone: false) ?? ""

3. 添加自动释放池管理临时对象

在循环内部加入autoreleasepool,确保每次循环的临时对象及时回收:

while inputStream.hasBytesAvailable {
    autoreleasepool {
        // 原循环内的所有处理逻辑
    }
}

4. 释放Unsafe指针

通过defer确保手动分配的缓冲区最终被释放:

let buf = UnsafeMutablePointer<UInt8>.allocate(capacity: l)
defer { buf.deallocate() } // 函数结束时自动释放

5. 调整队列自动释放频率

将队列的自动释放策略改为.workItem,让每个任务的临时对象及时清理:

let queue = DispatchQueue(label: "Parser", qos: .userInteractive, attributes: .concurrent, autoreleaseFrequency: .workItem)

修复后核心代码片段

func parseData(from inputStream: InputStream) {
    queue.async {
        var count = 0
        let l = 128*1024*1024
        let buf = UnsafeMutablePointer<UInt8>.allocate(capacity: l)
        defer { buf.deallocate() }
        
        var lastLine = ""
        var str = ""
        var lines: [String] = []
        
        inputStream.open()
        defer { inputStream.close() }
        
        while inputStream.hasBytesAvailable {
            autoreleasepool {
                let numberOfBytesRead = inputStream.read(buf, maxLength: l)
                guard numberOfBytesRead >= 0 else {
                    if let error = inputStream.streamError { print(error) }
                    return
                }
                
                str = String(bytesNoCopy: buf, length: numberOfBytesRead, encoding: .utf8, freeWhenDone: false) ?? ""
                lines = (lastLine + str).components(separatedBy: "\n").filter{$0.count > 3}
                lastLine = lines.removeLast()
                
                DispatchQueue.main.async {
                    self.logs.append("[\(count)] String buffer count: \(str.count), Lines count: \(lines.count)")
                    if self.logs.count > 100 {
                        self.logs.removeFirst()
                    }
                }
                
                while !lines.isEmpty {
                    var line = lines.removeFirst()
                    while line.first ?? "@" == " " { line.removeFirst() }
                    if line.first ?? "@" == "<" { line.removeFirst() }
                    if line.last ?? "@" == ">" { line.removeLast() }
                    
                    var name = ""
                    while line.first ?? "@" != " " && !line.isEmpty { name.append(line.removeFirst())}
                    if line.first ?? "@" == " " { line.removeFirst() }
                    
                    let array = line.components(separatedBy: "\" ")
                    
                    if array.count == 1 && name.first ?? "@" == "/" {
                        name.removeFirst()
                        self.didEndElement(name)
                    } else {
                        var attributes: [String: String] = [:]
                        for item in array {
                            var dic = item.components(separatedBy: "=\"")
                            
                            if dic.count == 2 {
                                if dic[1].last ?? "@" == "/" { dic[1].removeLast() }
                                if dic[1].last ?? "@" == "\"" { dic[1].removeLast() }
                                attributes[dic[0]] = dic[1]
                                
                            }
                        }
                        
                        self.didStartElement(name, attributes: attributes)
                        if line.last ?? "@" == "/" { self.didEndElement(name) }
                    }
                }
                
                count += 1
            }
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.24 23:54:42