使用完成处理程序进行异步调用的多个 URLSession dataTask 导致内存上升

Posted

技术标签:

【中文标题】使用完成处理程序进行异步调用的多个 URLSession dataTask 导致内存上升【英文标题】:Multiple URLSession dataTask on asynchronous call with completion handler causes memory goes up 【发布时间】:2019-02-06 09:25:26 【问题描述】:

我正在快速开发一个上传项目。我正在使用 imagepickercontroller 获取非常大的文件(视频、大小超过 500 MB 的图片)并将此文件分成大小为 1 MB 的块。然后我将这些块发送到远程服务器并在服务器中对它们进行碎片整理,并将此文件显示给用户。

如果文件大小低于 300 MB,我没有问题。但是在这个大小之后,内存会增加太多并且应用程序正在崩溃。实际上,在每种情况下,内存使用量都在增加,但没有崩溃。

当我在控制台上观看进度时,我看到 URLSession 任务开始了。但是,由于这些任务正在等待完成处理程序的响应,任务队列正在增长并且内存使用量上升。有没有办法在任务开始时,这个任务的完成处理程序也开始?我想如果我可以同时释放任务队列,我的问题就解决了。我在等你的帮助。

let url:URL = URL(string: "\(addressPrefix)UploadFile")!
let session = URLSession.shared
let request = NSMutableURLRequest(url: url)
request.cachePolicy = NSURLRequest.CachePolicy.reloadIgnoringCacheData
request.httpMethod = "POST"

let bodyData = "\(metaDataID)~\(chunkIndex)~\(chunkSize)~\(chunkHash)~\(wholeTicket)~\(fileDataString)"

request.httpBody = bodyData.data(using: String.Encoding(rawValue: String.Encoding.utf8.rawValue));
request.timeoutInterval = .infinity

let task = session.dataTask(with: request as URLRequest, completionHandler: (data, response, error) in
    guard let _:Data = data, let _:URLResponse = response, error == nil else 
       var attemptCounter = 1
       if attemptCounter <= 3 
            completion("\(attemptCounter).attempt",chunkSize, error)
            attemptCounter += 1
        
         return
     
    let jsonStr = String(data: data!, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue))
    completion(jsonStr, chunkSize, error) 
 SingletonConnectionManager.sharedConnectionDataManager.dataTasks["uploadFile"] = nil 
)  
SingletonConnectionManager.sharedConnectionDataManager.dataTasks["uploadFile"] = task 
task.resume()

---我在 tableview 控制器中从这个函数调用这个 URLSession 任务

 tmpConnection.uploadFile(chunk, metaDataID!, chunkIndex: chunkIndex, completion: (result, chunkSize, error) in
   // I want to enter immediately when 'uploadFile' get called )

【问题讨论】:

您是否一次安排所有 500 个请求?哎呀。您可能应该将文件分块在磁盘上(而不是在内存中),然后跟踪已发送的文件,一次最多发送八个左右,一旦完成就发送下一个新块或失败。而且,如果您控制服务器代码,那么将其分块在磁盘上也可以让您使用上传任务,而不必自己构建请求正文。 (只需将块 ID 和其他值放入查询字符串中,并使正文成为二进制数据的原始 blob。) 感谢@dgatwood 的回复。我在一个循环中调度请求并将它们发送到数据任务。正如你所说,我应该通过发送到服务器并获得响应来跟踪请求。但是,在这种情况下,请求正在等待,直到所有请求都发送完毕。之后,我得到了完成处理程序的响应,但内存正在增加,直到得到响应。在得到完成处理程序的响应之前,我无法完成任何任务。是否有机会在不等待完成处理程序的情况下获得响应? 【参考方案1】:

请求并没有真正等到所有请求都发送完毕。当一切正常时,每个回调都会在相关请求完成时发生,并且更快地发生这种情况是没有意义的,因为回调提供了来自服务器的响应(在请求之后您可能无法返回)已全部发出)。

这里的问题是,您同时启动了太多任务,从而完全阻塞了会话。 NSURLSession 中有一个已知错误,当​​您在单个会话中同时创建大量任务时,它会导致它开始崩溃。当您在会话 IIRC 中获得太多任务时,会话完全停止调用回调,并且基本上会话变得不可用。 (几年前讨论过另一个 Stack Overflow 问题,但我现在似乎找不到。)

并且由于任务永远不会完成,您的应用最终会泄漏您用于正文数据的所有内存,这意味着您的应用只会分配越来越多的内存,直到它被驱逐。

解决此问题的唯一方法是立即停止将所有请求添加到会话中。首先启动几个任务(最多八个部分左右),然后等待发送下一个部分,直到前面的一个部分完成或失败。这种方法不仅可以防止您将 NSURLSession 变砖,还可以防止您分配大量内存来保存所有请求主体 NSData 对象,这些对象目前都同时位于 RAM 中。

我建议保留一个代表每个未发送块的 NSNumber 对象的 NSMutableArray。这样,您就知道还剩下什么要发送,您可以循环到 8 并提取前 8 个数字,然后发送带有这些数字的块。当请求成功完成时,从数组中取出下一个数字并发送具有该数字的块。

此外,您不应在特定次数的重试后停止。相反,当请求失败时,检查失败以决定是重试(网络故障)还是放弃(服务器错误)。然后使用可达性等到合适的时间再试一次,当它说目标主机可达时再试一次。仅当用户通过单击取消按钮或类似按钮明确要求您取消上传时,才取消上传。如果用户要求您取消上传,请拆除您的数据结构以便您不会启动任何新请求,然后使 URL 会话无效。

【讨论】:

实际上,我尝试一直等待请求,直到任务队列数达到 10。但是,我不能让他们再次继续。我应该使用调度队列还是操作队列?如果我可以启动等待的请求,我认为问题会解决。我试过 DispatchGroups,但可能用错了。这些人可以处理这个问题吗? 不要做任何这些事情。你把事情弄得太复杂了。只需有一个获取文件 URL 并返回块号 n 的方法(即,如果您要求块 0,您将获得第一兆字节的数据)。保留一个包含从 0 到 MBInFileRoundedUp 的数字的数组。然后,编写另一个方法,从该可变数组的前面弹出一个数字,请求该块,然后开始上传。在循环中调用该方法 8 次。在完成处理程序中,调用从数组前面弹出数字的方法,请求块,然后开始上传。 为了简化数据管理,将可变数组传递给所有这些方法。当最后一次从空数组中弹出东西的尝试返回时,空数组就会消失,因为不再有块保留它。 这是一个清晰而好的算法@dgatwood。我正在尝试这个。但是有些地方我无法理解。首先,我保存数字的数组是否应该包含 chunkSize(即 1MB,1MB)或 chunkNo(即 0. ,1. , 2. chunk)。其次,如何在循环返回 8 次后让 chunkNo 继续。它像递归函数吗?很抱歉这些愚蠢的问题,但这个项目真的很混乱,而且我对 Swift 有点陌生。此外,我必须将此文件分块为数据,这对内存来说是一个很大的负担。因为,当我将此块发送到服务器时,我必须对所有块进行哈希和十六进制。 后者。一个包含实际数字 0, 1, 2, ... n 的数组,其中 n 是文件中的兆字节数,四舍五入。并且不要提前将文件分块为数据对象。相反,使用 dataWithContentsOfFile:options:error: 对文件进行内存映射,以便只有您正在积极使用的位正在积极使用 RAM,然后使用 subdataWithRange: 创建一个仅包含第 n 个块的数据对象准备好发送那个块,计算它的哈希值,等等。

以上是关于使用完成处理程序进行异步调用的多个 URLSession dataTask 导致内存上升的主要内容,如果未能解决你的问题,请参考以下文章

Angular 2调用多个异步方法

如何在异步函数中处理多个等待

Firebase 和 Swift:异步调用、完成处理程序

检查多个异步网络操作何时完成

Jsp页面中的异步与同步

使用 SwiftUI 时异步填充/预填充多个用户可变的“@State”值?