美文网首页
Alamofire之form多表单上传

Alamofire之form多表单上传

作者: lb_ | 来源:发表于2019-08-26 16:02 被阅读0次

    使用 AFNetworking 进行文件上传的使用方法大家想必已经很熟悉. 那么作为其 Swift 版本, 我们当然也不能少.

    先来个例子.

    upload(multipartFormData: { (multipartFormData) in
        
        multipartFormData.append("lb".data(using: .utf8)!, withName: "username")
        multipartFormData.append("123456".data(using: .utf8)!, withName: "password")
        multipartFormData.append("wdms82jnds".data(using: .utf8)!, withName: "token")
        
    }, to: "http://www.baidu.com") { (result) in
        print(result)
    }
    

    打开 Charles或者其他抓包工具, 运行项目查看我们本次请求.


    其具体结构很清晰

    • alamofire.bounday.91b32560f55a049f 分隔符
    • Content-Dispositon:form-data;name="name" (key)
    • /r/n
    • value

    接下来分析下其内部逻辑是如何处理的.
    点击进入 upload 具体实现方法, 中间过渡方法省略.

    open func upload(
        multipartFormData: @escaping (MultipartFormData) -> Void,
        usingThreshold encodingMemoryThreshold: UInt64 = SessionManager.multipartFormDataEncodingMemoryThreshold,
        with urlRequest: URLRequestConvertible,
        queue: DispatchQueue? = nil,
        encodingCompletion: ((MultipartFormDataEncodingResult) -> Void)?)
    {
        DispatchQueue.global(qos: .utility).async {
            let formData = MultipartFormData()
            multipartFormData(formData)
    
            var tempFileURL: URL?
            
            var urlRequestWithContentType = try urlRequest.asURLRequest()
            urlRequestWithContentType.setValue(formData.contentType, forHTTPHeaderField: "Content-Type")
    
            let isBackgroundSession = self.session.configuration.identifier != nil
    
            if formData.contentLength < encodingMemoryThreshold && !isBackgroundSession {
                let data = try formData.encode()
    
                let encodingResult = MultipartFormDataEncodingResult.success(
                    request: self.upload(data, with: urlRequestWithContentType),
                    streamingFromDisk: false,
                    streamFileURL: nil
                )
    
                (queue ?? DispatchQueue.main).async { encodingCompletion?(encodingResult) }
            } else {
                let fileManager = FileManager.default
                let tempDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory())
                let directoryURL = tempDirectoryURL.appendingPathComponent("org.alamofire.manager/multipart.form.data")
                let fileName = UUID().uuidString
                let fileURL = directoryURL.appendingPathComponent(fileName)
    
                tempFileURL = fileURL
    
                var directoryError: Error?
    
                // Create directory inside serial queue to ensure two threads don't do this in parallel
                self.queue.sync {
                   try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
                }
    
                if let directoryError = directoryError { throw directoryError }
    
                try formData.writeEncodedData(to: fileURL)
    
                let upload = self.upload(fileURL, with: urlRequestWithContentType)
    
                //...
                (queue ?? DispatchQueue.main).async {
                    let encodingResult = MultipartFormDataEncodingResult.success(
                        request: upload,
                        streamingFromDisk: true,
                        streamFileURL: fileURL
                    )
    
                    encodingCompletion?(encodingResult)
                }
            }
            
        }
    }
    

    提示:

    • (中间有一些 try catch 异常处理因篇幅原因省略掉了. 不影响)
    • 先看 if else 逻辑控制语句. 搞清楚后把其种之一折起来, 有助于在查看源码时理清思路.

    该方法具体操作如下:

    • 1️⃣: 在全局队列开启异步执行下面任务.
    • 2️⃣: 初始化一个 MultipartFormData 对象并调用闭包参数。调用用户在外界闭包中所做的处理, 也就是收取用户拼接的数据
    let formData = MultipartFormData()
    multipartFormData(formData)
    

    执行用户传递的 multipartFormData 闭包, 我们在这个闭包中调用了 MultipartFormDataappend 方法. 存到了这个临时变量里.

    • 3️⃣: 设置请求头格式
    • 4️⃣: 根据数据长度来区分处理, 长度为该方法的 usingThreshold 参数. 用户不传时为默认阈值.
    public static let multipartFormDataEncodingMemoryThreshold: UInt64 = 10_000_000
    

    接下来:

    • 当满足长度阈值和 SessionManager 的会话环境为默认时, 进行 formData 编码, 上传.
    • 否则,则将数据写入文件, 上传.

    那么我们分别探讨.

    Alamofire form表单数据编码

    由于 upload 方法中, 调用了用户所添加的参数, 也就是调用 multipartFormData.append , 那我们点进去 append 方法

    public func append(_ data: Data, withName name: String) {
        let headers = contentHeaders(withName: name)
        let stream = InputStream(data: data)
        let length = UInt64(data.count)
    
        append(stream, withLength: length, headers: headers)
    }
    

    先处理头 进入

    private func contentHeaders(withName name: String, fileName: String? = nil, mimeType: String? = nil) -> [String: String] {
        var disposition = "form-data; name=\"\(name)\""
        if let fileName = fileName { disposition += "; filename=\"\(fileName)\"" }
    
        var headers = ["Content-Disposition": disposition]
        if let mimeType = mimeType { headers["Content-Type"] = mimeType }
    
        return headers
    }
    

    对照我们抓包结果来看, 其实就是
    Content-Disposition: form-data; name=\"\(name)\"
    然后进行 append

    append(stream, withLength: length, headers: headers)
    

    这里就是对 bodyParts 这个数组中添加 BodyPart 对象进去. 那么我们回到 判断条件 满足阈值判定满足时, 首先调用 encode 方法.

    let data = try formData.encode()
    

    直接点击进入 encode 方法

    public func encode() throws -> Data {
        if let bodyPartError = bodyPartError {
            throw bodyPartError
        }
    
        var encoded = Data()
    
        bodyParts.first?.hasInitialBoundary = true
        bodyParts.last?.hasFinalBoundary = true
    
        for bodyPart in bodyParts {
            let encodedData = try encode(bodyPart)
            encoded.append(encodedData)
        }
    
        return encoded
    }
    

    对头尾进行标识. 然后遍历调用 encode .

    private func encode(_ bodyPart: BodyPart) throws -> Data {
        var encoded = Data()
    
        let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData()
        encoded.append(initialData)
    
        let headerData = encodeHeaders(for: bodyPart)
        encoded.append(headerData)
    
        let bodyStreamData = try encodeBodyStream(for: bodyPart)
        encoded.append(bodyStreamData)
    
        if bodyPart.hasFinalBoundary {
            encoded.append(finalBoundaryData())
        }
    
        return encoded
    }
    

    针对数组中的头尾和中间三种情况,分别处理, 返回.

    • 是首元素,则调用 initialBoundaryData() 函数。
    • 是中间元素,则调用 encapsulatedBoundaryData() 函数。
    • 是尾元素,则调用 finalBoundaryData() 函数。

    通过type区分最终调用如下函数:

    static func boundaryData(forBoundaryType boundaryType: BoundaryType, boundary: String) -> Data {
        let boundaryText: String
    
        switch boundaryType {
        case .initial:
            boundaryText = "--\(boundary)\(EncodingCharacters.crlf)"
        case .encapsulated:
            boundaryText = "\(EncodingCharacters.crlf)--\(boundary)\(EncodingCharacters.crlf)"
        case .final:
            boundaryText = "\(EncodingCharacters.crlf)--\(boundary)--\(EncodingCharacters.crlf)"
        }
    
        return boundaryText.data(using: String.Encoding.utf8, allowLossyConversion: false)!
    }
    
    • 其中 boundary 为分隔符. 也就是
    static func randomBoundary() -> String {
        return String(format: "alamofire.boundary.%08x%08x", arc4random(), arc4random())
    }
    
    • EncodingCharacters.crlf 为换行
    struct EncodingCharacters {
        static let crlf = "\r\n"
    }
    

    还有值得一提的一点是在循环数组进行 encode 方法中还调用了一个
    encodeBodyStream 方法

    private func encodeBodyStream(for bodyPart: BodyPart) throws -> Data {
        let inputStream = bodyPart.bodyStream
        inputStream.open()
        defer { inputStream.close() }
    
        var encoded = Data()
    
        while inputStream.hasBytesAvailable {
            var buffer = [UInt8](repeating: 0, count: streamBufferSize)
            let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize)
    
            if let error = inputStream.streamError {
                throw AFError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: error))
            }
    
            if bytesRead > 0 {
                encoded.append(buffer, count: bytesRead)
            } else {
                break
            }
        }
    
        return encoded
    }
    

    使用数据流stream 的方式读取数据, 利用其优化措施, 防止内存读取存储暴增问题.

    Alamofire 表单上传 - 写入文件方式

    也就是我们刚刚 if else 时, else 的情况
    upload 方法中 else 部分:

    let fileManager = FileManager.default
    let tempDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory())
    let directoryURL = tempDirectoryURL.appendingPathComponent("org.alamofire.manager/multipart.form.data")
    let fileName = UUID().uuidString
    let fileURL = directoryURL.appendingPathComponent(fileName)
    
    tempFileURL = fileURL
    
    var directoryError: Error?
    
    // Create directory inside serial queue to ensure two threads don't do this in parallel
    self.queue.sync {
        do {
            try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
        } catch {
            directoryError = error
        }
    }
    
    if let directoryError = directoryError { throw directoryError }
    
    try formData.writeEncodedData(to: fileURL)
    
    let upload = self.upload(fileURL, with: urlRequestWithContentType)
    

    前面的文件操作就不多赘述, 直接来到

    try formData.writeEncodedData(to: fileURL)
    

    点击进入方法

    public func writeEncodedData(to fileURL: URL) throws {
        if let bodyPartError = bodyPartError {
            throw bodyPartError
        }
    
        if FileManager.default.fileExists(atPath: fileURL.path) {
            throw AFError.multipartEncodingFailed(reason: .outputStreamFileAlreadyExists(at: fileURL))
        } else if !fileURL.isFileURL {
            throw AFError.multipartEncodingFailed(reason: .outputStreamURLInvalid(url: fileURL))
        }
    
        guard let outputStream = OutputStream(url: fileURL, append: false) else {
            throw AFError.multipartEncodingFailed(reason: .outputStreamCreationFailed(for: fileURL))
        }
    
        outputStream.open()
        defer { outputStream.close() }
    
        self.bodyParts.first?.hasInitialBoundary = true
        self.bodyParts.last?.hasFinalBoundary = true
    
        for bodyPart in self.bodyParts {
            try write(bodyPart, to: outputStream)
        }
    }
    

    老样子 前面文件名重复, 文件路径无效等异常处理直接过.
    我们看到同样是开流的方式. outputStream.open / .close. 然后标识头尾. 最后循环调用 .write.

    private func write(_ bodyPart: BodyPart, to outputStream: OutputStream) throws {
        try writeInitialBoundaryData(for: bodyPart, to: outputStream)
        try writeHeaderData(for: bodyPart, to: outputStream)
        try writeBodyStream(for: bodyPart, to: outputStream)
        try writeFinalBoundaryData(for: bodyPart, to: outputStream)
    }
    

    然后分别调用四个过渡方法, 最终来到

    private func write(_ buffer: inout [UInt8], to outputStream: OutputStream) throws {
        var bytesToWrite = buffer.count
    
        while bytesToWrite > 0, outputStream.hasSpaceAvailable {
            let bytesWritten = outputStream.write(buffer, maxLength: bytesToWrite)
    
            if let error = outputStream.streamError {
                throw AFError.multipartEncodingFailed(reason: .outputStreamWriteFailed(error: error))
            }
    
            bytesToWrite -= bytesWritten
    
            if bytesToWrite > 0 {
                buffer = Array(buffer[bytesWritten..<buffer.count])
            }
        }
    }
    

    循环写入到流中, 最后使用文件 URL 上传.

    private func upload(_ uploadable: UploadRequest.Uploadable) -> UploadRequest {
        do {
            let task = try uploadable.task(session: session, adapter: adapter, queue: queue)
            let upload = UploadRequest(session: session, requestTask: .upload(uploadable, task))
    
            if case let .stream(inputStream, _) = uploadable {
                upload.delegate.taskNeedNewBodyStream = { _, _ in inputStream }
            }
    
            delegate[task] = upload
    
            if startRequestsImmediately { upload.resume() }
    
            return upload
        } catch {
            return upload(uploadable, failedWith: error)
        }
    }
    

    这里就回到了我们熟悉的 request 的流程 , 启动 -> 响应 -> 回调. 不熟悉的可以去阅读一下
    Alamofire之Request(二)和队列执行顺序分析
    Alamofire之Request(一)
    这两篇文章. 本文就不重复阐述了.
    .

    相关文章

      网友评论

          本文标题:Alamofire之form多表单上传

          本文链接:https://www.haomeiwen.com/subject/ggprectx.html