大数据量后台上传

问题描述 投票:0回答:3

我需要从我的 iOS 设备上传大量照片到服务器。例如,500 张照片。我应该如何正确地做到这一点?

我使用 NSURLSession 为每张照片创建了带有后台会话配置的上传任务。我尝试制作自己的自定义队列,其中下一个任务将在上一个任务完成后启动。但有一刻新的任务还没有开始。我想,因为这一切都是在后台发生的。有关此问题的更多信息,您可以阅读此处

(如果使用队列的方法是正确的,请您建议异步任务的队列的良好实现,因为我可能会在实现中弄乱一些东西)

因此,在上面链接的文章之后,我的猜测是我应该立即启动所有上传任务(而不是一个接一个)。但我有效率问题。如果我为每张照片创建一个任务,它将有 500 个后台异步任务和大约 1 GB 的并行数据上传。我想,这会导致网络出现问题。

综上所述,iOS中后台上传大块数据(我的情况是500张照片)的正确方法是什么?

ios image-uploading nsurlsession nsurlsessionuploadtask
3个回答
5
投票

不幸的是,由于糟糕的设计决策,Apple 的 API 在这项任务上表现得很糟糕。 您面临几个主要障碍:

  • 您可以同时创建的任务数量是有限的。 我认为性能在大约 100 个任务的某个地方开始崩溃。
  • 操作系统唤醒应用程序的频率是有限制的。 唤醒您的应用程序的频率越高,再次唤醒它之前等待的时间就越长。 在某些时候,这将导致无法安排新任务。
  • 如果发生失败,上传不会从中断处继续;他们重新开始。 这可能会导致在糟糕的网络上产生巨大的带宽成本。

我怀疑最好的方法是:

  • 将请求分成几个大组,每个组都可以写入一个 ZIP 存档,该存档不会太大而导致用户耗尽磁盘空间,但也不会太小而导致上传太快。
  • 将第一组文件写入单个文件(例如 ZIP 格式)。
  • 在服务器端使用自定义脚本,让您可以通过添加额外的 CGI 参数从中断处恢复上传。
  • 失败时,询问服务器获得了多少数据,然后截断文件的前面并从当前位置重新上传。
  • 成功后,计算第一个大文件上传完成的速度,如果不是 O(分钟),则合并接下来的几组。 将一个或多个集合写入文件,然后开始下一个请求。

需要注意的是,所有这些都必须相当快地完成。 您可能会发现有必要提前将文件预先合并到 ZIP 存档中,以避免被杀死。 但不要试图将它们合并到一个文件中,因为这样在重试时截断头部时会花费太长时间。 (表面上,您还可以提供任何参数作为 URL 的一部分,并使 POST 正文成为原始数据,并提供一个文件流以从 ZIP 存档中从偏移量开始读取。)

如果你还没有用头撞墙,那么你很快就会这么做。 :-)


0
投票

要下载这么多图像,您可能需要询问 用户不要停止应用程序或进入后台模式。

因为在这两种情况下我们将无法执行这么大的任务来完成。

如果用户手机处于激活状态,

创建一个

NSoperation
对应每个上传过程。在你的情况下可能是 500 左右。

并将

NSOperations
添加到名为
NSOperationQueue
的队列中,然后启动
NSOperationQueue
任务。

它将一一执行。

有关缓存等更多详细信息..请关注SO POST

这是 Swift 版本


0
投票

我最近有一个类似的需求,我需要处理大量媒体文件的后台上传(我的例子是 75 个)。我能够使用带有后台配置的 URLSession 成功实现它。关键是正确管理会话、处理进度更新,并确保即使应用程序进入后台也能继续上传。这是我解决问题的方法...

// Helper extension to append Data
extension Data {
mutating func append(_ string: String) {
    if let data = string.data(using: .utf8) {
        append(data)
    }
  }
}

class BackgroundUploader: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDataDelegate {
var backgroundSession: URLSession!
var uploadCompletionHandlers: [Int: (String) -> Void] = [:] // Store completion handlers for each task
var files = String()
override init() {
    super.init()
    
    // Create a background configuration
    let config = URLSessionConfiguration.background(withIdentifier: "com.yourapp.upload")
    config.isDiscretionary = false // Ensure uploads run even when the app is in the background
    config.sessionSendsLaunchEvents = true // App will relaunch if terminated during upload
    
    // Create the background session
    backgroundSession = URLSession(configuration: config, delegate: self, delegateQueue: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(appWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
}
deinit {
    NotificationCenter.default.removeObserver(self) // Remove observer when this object is deallocated
}
// Function to upload an array of data with headers and completion handler
func uploadDataArray(dataArray: [Data], to url: URL, completionHandler: @escaping (String) -> Void) {
    let boundary = "Boundary-\(UUID().uuidString)" // Generate a unique boundary for the request
    let multipartBody = createMultipartBody(dataArray: dataArray, boundary: boundary)
    
    // Write multipart body to a temporary file
    let tempDirectory = FileManager.default.temporaryDirectory
    let tempFileURL = tempDirectory.appendingPathComponent(UUID().uuidString)
    
    do {
        // Write the multipart body to a temp file
        try multipartBody.write(to: tempFileURL)
        
        // Upload the file
        uploadFile(fileURL: tempFileURL, to: url, boundary: boundary, completionHandler: completionHandler)
        
    } catch {
        print("Failed to write multipart body to temporary file: \(error)")
    }
}
// Create a multipart body with all files from the array
private func createMultipartBody(dataArray: [Data], boundary: String) -> Data {
    var body = Data()
    
    for (index, data) in dataArray.enumerated() {
        // Add each file to the body as "files[]"
        let filename = "file\(index + 1).jpg" // Update the filename for each file
        let mimeType = "image/jpeg" // Change MIME type as needed

        body.append("--\(boundary)\r\n")
        body.append("Content-Disposition: form-data; name=\"files[]\"; filename=\"\(filename)\"\r\n")
        body.append("Content-Type: \(mimeType)\r\n\r\n")
        body.append(data)
        body.append("\r\n")
    }
    
    // Final boundary to signal the end of the multipart form
    body.append("--\(boundary)--\r\n")
    
    return body
}
// Upload the file using file URL with headers
func uploadFile(fileURL: URL, to url: URL, boundary: String, completionHandler: @escaping (String) -> Void) {
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    
    // Set custom headers (multipart/form-data and Authorization headers)
    request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
    request.setValue("Bearer \(AIUser.sharedManager.token)", forHTTPHeaderField: "Authorization")
    request.setValue(APP_VERSION ?? "", forHTTPHeaderField: "app-version")
    
    // Create a background upload task for the file
    let task = backgroundSession.uploadTask(with: request, fromFile: fileURL)
    
    // Store the completion handler associated with the task ID
    uploadCompletionHandlers[task.taskIdentifier] = completionHandler
    
    task.resume()
}
// Handle response and pass uploaded file name to completion handler
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
    do {
        // Parse the JSON response
        if let responseJSON = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
            print("Response JSON: \(responseJSON)")
            
            // Check if responseData contains images
            if let responseData = responseJSON["responseData"] as? [String: Any],
               let images = responseData["images"] as? [String] {
                
              
                // Combine the image names into a single string (if needed)
                let joinedFileNames = images.joined(separator: ", ")
                self.files = joinedFileNames
                if let completionHandler = uploadCompletionHandlers[dataTask.taskIdentifier] {
                    completionHandler(joinedFileNames) // Pass the uploaded file names back
                }
                
                // Call the associated completion handler with the uploaded file names
                
            } else if let successMessage = responseJSON["message"] as? String {
                // Handle the message if images aren't present
                if let completionHandler = uploadCompletionHandlers[dataTask.taskIdentifier] {
                    completionHandler(successMessage) // Pass the response message back
                }
            }
        }
    } catch {
        print("Failed to parse response: \(error)")
    }
}
// URLSessionTaskDelegate: Handle completion in the background
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
    if let error = error {
        print("Upload failed with error: \(error.localizedDescription)")
    } else {
        print("Upload completed successfully for task: \(task.taskIdentifier)")
        scheduleUploadCompletionNotification()
    }
    
    // Remove the stored handler for the completed task
    uploadCompletionHandlers.removeValue(forKey: task.taskIdentifier)
}

// Optional: Handle app relaunch after task completion in background
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    // Notify the system that all background events have been handled
    print("All background events have been handled.")
}

@objc func appWillEnterForeground() {
       // Check if there are any active tasks
       backgroundSession.getAllTasks { tasks in
           if tasks.isEmpty {
               // No active tasks, call your API from the controller
               DispatchQueue.main.async {
                   // Assume you have a reference to your controller, call the API here
                   print("Entered in forground and uploaded files are \(self.files)")
                   
               }
           } else {
               print("There are still \(tasks.count) uploads in progress.")
           }
       }
   }
func scheduleUploadCompletionNotification() {
    let content = UNMutableNotificationContent()
    content.title = "Upload Complete"
    content.body = "All your files have been uploaded successfully."
    content.sound = .default

    // Set a trigger to show the notification immediately
    let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)

    // Create the notification request
    let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)

    // Add the notification request to the system
    let center = UNUserNotificationCenter.current()
    center.add(request) { error in
        if let error = error {
            print("Error scheduling notification: \(error)")
        } else {
            print("Notification scheduled!")
        }
      }
    }
  }

use:-
var uploader : BackgroundUploader!
uploader.uploadDataArray(dataArray: [Data], to: URL, completionHandler: (String) -> Void)
© www.soinside.com 2019 - 2024. All rights reserved.