关于iOS端大文件的上传一

35 阅读3分钟

大文件上传通常会采用分片的形式充分利用多线程并发任务,来进行文件上传操作,其中过程:

  1. 文件分割:将要上传的文件分割成固定大小的数据块或者按照其他顺序拆分的数据块;
  2. 并行上传:为每个数据块创建一个单独的上传任务,并使用多个线程处理这些任务,充分利用内核资源,每个单独的任务单独管理分片数据的上传状态。
  3. 上传合并:上传合并:服务器接收到这些块后,根据预定义的规则将它们合并成原始文件。这可能涉及到将块按顺序组合、按块的位置信息进行排序或者使用其他算法来重建原始文件。

另外大文件上传过程中涉及到一些不可避免的问题,需要我们想好应对措施,针对于上传操作过程中可能会遇到网络环境差、服务端错误、客户端错误等原因导致传输中断的问题,所以需要对文件上传记录进行本地化存储,以便于下次联网从指定的偏移位置进行继续上传操作。下面开始实施代码部分

分片函数,将文件分割成固定大小的数据段集合

func splitFile(filePath: String, chunkSize: Int) -> [Data] {
    var fileChunks: [Data] = []
    let fileManager = FileManager.default
    
    if let fileHandle = FileHandle(forReadingAtPath: filePath) {
        defer {
            fileHandle.closeFile()
        }
        
        let fileData = fileHandle.readDataToEndOfFile()
        let totalSize = fileData.count
        var offset = 0
        
        while offset < totalSize {
            let chunk = fileData.subdata(in: offset..<min(offset + chunkSize, totalSize))
            fileChunks.append(chunk)
            offset += chunkSize
        }
    }
    
    return fileChunks
}

上传操作类,自定义封装Operation类,负责上传每一个分片数据块

import Foundation

class UploadOperation: Operation {
    private var data: Data
    private var url: URL
    private var chunkIndex: Int
    private var completion: (Bool) -> Void
    
    init(data: Data, url: URL, chunkIndex: Int, completion: @escaping (Bool) -> Void) {
        self.data = data
        self.url = url
        self.chunkIndex = chunkIndex
        self.completion = completion
    }
    
    override func main() {
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.httpBody = data
        request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
        // 后端根据索引进行文件合并
        request.setValue(String(chunkIndex), forHTTPHeaderField: "Chunk-Index")
        
        let task = URLSession.shared.dataTask(with: request) { (_, response, error) in
            if let error = error {
                print("Upload chunk \(self.chunkIndex) failed: \(error)")
                self.completion(false)
            } else if let response = response as? HTTPURLResponse, response.statusCode == 200 {
                print("Upload chunk \(self.chunkIndex) succeeded")
                self.completion(true)
            } else {
                self.completion(false)
            }
        }
        task.resume()
    }
}

文件上传管理类, 负责文件上传的具体工作

class FileUploader {
    /// 最大上传并发数
    private let maxConcurrentUploads = 4
    /// 上传队列
    private var uploadQueue: OperationQueue
    /// 保存已上传的分片索引
    private var uploadedChunks = Set<Int>()
    ///用于将已上传分片的数组存储本地的key
    private let progressKey = "UploadProgress"
    
    init() {
        /// 初始化队列
        self.uploadQueue = OperationQueue()
        self.uploadQueue.maxConcurrentOperationCount = maxConcurrentUploads
        /// 获取断点已上传的索引集合
        loadUploadedChunks()
    }
    
    private func saveUploadedChunks() {
        UserDefaults.standard.set(Array(uploadedChunks), forKey: progressKey)
    }
    
    private func loadUploadedChunks() {
        if let savedChunks = UserDefaults.standard.array(forKey: progressKey) as? [Int] {
            uploadedChunks = Set(savedChunks)
        }
    }
    
    func uploadFileChunks(chunks: [Data], url: URL, completion: @escaping (Bool) -> Void) {
        let totalChunks = chunks.count
        var completedChunks = 0
        
        for (index, chunk) in chunks.enumerated() {
            if uploadedChunks.contains(index) {
                completedChunks += 1
                /// 跳过已经上传的分片,并+1
                continue
            }
            
            let uploadOperation = UploadOperation(data: chunk, url: url, chunkIndex: index) { [weak self] success in
                guard let self = self else { return }
                if success {
                    self.uploadedChunks.insert(index)
                    self.saveUploadedChunks()
                }
                completedChunks += 1
                if completedChunks == totalChunks {
                    completion(self.uploadedChunks.count == totalChunks)
                    UserDefaults.standard.removeObject(forKey: self.progressKey) // Clear progress on completion
                }
            }
            uploadQueue.addOperation(uploadOperation)
        }
    }
}

使用示例

let filePath = "path/to/your/large/file"
let uploadURL = URL(string: "https://your.server/upload")!
let chunkSize = 1024 * 1024 // 1MB per chunk

let fileUploader = FileUploader()
let fileChunks = splitFile(filePath: filePath, chunkSize: chunkSize)

fileUploader.uploadFileChunks(chunks: fileChunks, url: uploadURL) { success in
    if success {
        print("All chunks uploaded successfully")
    } else {
        print("Failed to upload file")
    }
}