美文网首页
Alamofire Upload

Alamofire Upload

作者: 好有魔力 | 来源:发表于2019-08-26 17:33 被阅读0次

本篇来探索Alamofire的上传数据逻辑

1.多部分表单数据上传

示例:

SessionManager.default
            .upload(multipartFormData: { (multipartFormData) in
                
                multipartFormData.append("自定义数据0".data(using: .utf8)!, withName: "data0")
                
                multipartFormData.append("自定义数据1".data(using: .utf8)!, withName: "data1")
                
                 //这里只是为了演示,才和上面的写到一起           
               multipartFormData.append(UIImage().jpegData(compressionQuality: 1.0)!, withName: "img0", fileName: "aaa.jpg", mimeType: "image/jpeg")
                
            }, to: "your url") { (result) in
                
                debugPrint(result)
        }
  • Alamofire 多表单上传为外界提供了一个闭包,方便构造表单数据

来看下upload(multipartFormData:)方法相关的源码

open class SessionManager {

//upload方法1
open func upload(
        multipartFormData: @escaping (MultipartFormData) -> Void,
        usingThreshold encodingMemoryThreshold: UInt64 = SessionManager.multipartFormDataEncodingMemoryThreshold,
        to url: URLConvertible,
        method: HTTPMethod = .post,
        headers: HTTPHeaders? = nil,
        queue: DispatchQueue? = nil,
        encodingCompletion: ((MultipartFormDataEncodingResult) -> Void)?)
    {
        do {
            let urlRequest = try URLRequest(url: url, method: method, headers: headers)

            return upload(
                multipartFormData: multipartFormData,
                usingThreshold: encodingMemoryThreshold,
                with: urlRequest,
                queue: queue,
                encodingCompletion: encodingCompletion
            )
        } catch {
            (queue ?? DispatchQueue.main).async { encodingCompletion?(.failure(error)) }
        }
    }

//upload方法2
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?

            do {
                //设置表单数据的类型 Content-Type
                var urlRequestWithContentType = try urlRequest.asURLRequest()
                urlRequestWithContentType.setValue(formData.contentType, forHTTPHeaderField: "Content-Type")
                
                //判断当前URLSession 是不是background类型
                let isBackgroundSession = self.session.configuration.identifier != nil
                
                //判断当前的表单数据大小是否小于某一阈值 && 不是backgroud类型的URLSession
                if formData.contentLength < encodingMemoryThreshold && !isBackgroundSession {
                   //真正构造多表单数据
                    let data = try formData.encode()
                   
                   //判断此次多表单数据编码是否成功
                    let encodingResult = MultipartFormDataEncodingResult.success(
                        //调用upload 方法3.1
                        request: self.upload(data, with: urlRequestWithContentType),
                        streamingFromDisk: false,
                        streamFileURL: nil
                    )
                    //主线程调用 encodingCompletion 闭包
                    (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)
                  
                   //保存了一个文件url
                    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)

                    //调用另一个上传 upload方法3.2,传递了fileURL
                    let upload = self.upload(fileURL, with: urlRequestWithContentType)
                   
                    //Taskdelegate.queue中添加 移除临时文件的操作,再上传结束后会被执行
                    // Cleanup the temp file once the upload is complete
                    upload.delegate.queue.addOperation {
                        do {
                            try FileManager.default.removeItem(at: fileURL)
                        } catch {
                            // No-op
                        }
                    }
                   
                 
                    (queue ?? DispatchQueue.main).async {
                        let encodingResult = MultipartFormDataEncodingResult.success(
                            request: upload,
                            streamingFromDisk: true,
                            streamFileURL: fileURL
                        )
                          //主线程调用 encodingCompletion 闭包
                        encodingCompletion?(encodingResult)
                    }
                }
            } catch {
                // Cleanup the temp file in the event that the multipart form data encoding failed
                if let tempFileURL = tempFileURL {
                    do {
                        try FileManager.default.removeItem(at: tempFileURL)
                    } catch {
                        // No-op
                    }
                }

                (queue ?? DispatchQueue.main).async { encodingCompletion?(.failure(error)) }
            }
        }
    }

//upload 方法3.1
@discardableResult
    open func upload(_ data: Data, with urlRequest: URLRequestConvertible) -> UploadRequest {
        do {
            let urlRequest = try urlRequest.asURLRequest()
            return upload(.data(data, urlRequest))
        } catch {
            return upload(nil, failedWith: error)
        }
    }

//upload 方法3.2
 @discardableResult
 open func upload(_ fileURL: URL, with urlRequest: URLRequestConvertible) -> UploadRequest {
        do {
            let urlRequest = try urlRequest.asURLRequest()
            return upload(.file(fileURL, urlRequest))
        } catch {
            return upload(nil, failedWith: error)
        }
    }

//upload 方法4,这个是私有方法哦~
//此方法真正 resume task.
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)
        }
    }

}

open class UploadRequest: DataRequest {

    // MARK: Helper Types
    enum Uploadable: TaskConvertible {
        case data(Data, URLRequest)
        case file(URL, URLRequest)
        case stream(InputStream, URLRequest)

        func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
            do {
                let task: URLSessionTask
               
              //这里面调用adapt 适配器,得到最终的请求
                switch self {
               //创建上传data的task
                case let .data(data, urlRequest):
                    let urlRequest = try urlRequest.adapt(using: adapter)
                    task = queue.sync { session.uploadTask(with: urlRequest, from: data) }     
                 //创建上传file的task
                case let .file(url, urlRequest):
                    let urlRequest = try urlRequest.adapt(using: adapter)
                    task = queue.sync { session.uploadTask(with: urlRequest, fromFile: url) }
                
                 //创建上传stream的task
                case let .stream(_, urlRequest):
                    let urlRequest = try urlRequest.adapt(using: adapter)
                    task = queue.sync { session.uploadTask(withStreamedRequest: urlRequest) }
                }

                return task
            } catch {
                throw AdaptError(error: error)
            }
        }
    }

}

方法流程跟下来还是挺长的,不知道有什么更直观的说明方法,所以我在方法中加了必要的注释,这里总结下:

  • 我们外界的调用的upload方法经过 upload方法1-->upload方法2-->upload方法3.1 或者 upload方法3.2->upload方法4,其中upload方法2upload方法4是比较重要的环节.

  • upload方法2 主要处理两种情况:
    情况1:当我们要上传的数据大小小于我们设置的阈值时,直接上传拼接好的多部分表单数据.
    情况2:当我们要上传的数据大于我们设置的阈值时,先在本地沙盒创建文件,之后上传fileUrl

  • upload方法4调用了 uploadable.task方法,uploadable.task 方法会根据之前对于多表单数据大小的判断 创建对应类型的 URLSessionTask, 最后调用 URLSessionTaskresume方法开始上传.

2.data上传

有了对于多部分表单数据上传的分析,data上传的流程应该也是类似,来看源码:

//upload 方法3.1
//调用data上传
SessionManager.default
            .upload(Data(), to: "your url")
            .uploadProgress(closure: { (progress) in
                
            })
            .response { (response) in
            debugPrint(response)
        }

@discardableResult
    open func upload(_ data: Data, with urlRequest: URLRequestConvertible) -> UploadRequest {
        do {
            let urlRequest = try urlRequest.asURLRequest()
           //调用upload方法4
            return upload(.data(data, urlRequest))
        } catch {
            return upload(nil, failedWith: error)
        }
    }

  • 外界调用data 上传 实际上是调用上面提到的 upload方法4,这里就不赘述了

3.stream 上传

直接上代码:

//外界调用 stream 上传
 let data = Data()
        let inputStream = InputStream(data: data)
        SessionManager.default.upload(inputStream, to: "", method: .post, headers: ["":""]).response { (response) in
              debugPrint(response)
        }

//upload方法3.3
 @discardableResult
    open func upload(
        _ stream: InputStream,
        to url: URLConvertible,
        method: HTTPMethod = .post,
        headers: HTTPHeaders? = nil)
        -> UploadRequest
    {
        do {
            let urlRequest = try URLRequest(url: url, method: method, headers: headers)
            return upload(stream, with: urlRequest)
        } catch {
            return upload(nil, failedWith: error)
        }
    }
  • stream 上传实际上是调用 upload方法3.3,最终又会调用 upload方法4,可见其实 upload方法3.x 都是 upload方法4的装饰器.

4.上传报文的构建过程

上面叙述了三种上传方式的方法流程,但是上传请求要想成功,正确的报文格式是必须的,Alamofire帮助我们封装了报文构建的过程,为我们的开发提供了极大的便利,在开始探索报文构造流程之前,先来看看HTTP 上传报文的样子.

多部分表单上传为例:

上传数据请求体

Alamofire 报文构造过程,实际上就是构造类似于上述数据的过程,还记得upload方法2中有一段let data = try formData.encode()吗? 来看源码:

//在upload方法2 中有这样一段代码:
let data = try formData.encode()

open class MultipartFormData {

//BodyPart 内部类
class BodyPart {
        let headers: HTTPHeaders
        let bodyStream: InputStream
        let bodyContentLength: UInt64
        var hasInitialBoundary = false
        var hasFinalBoundary = false

        init(headers: HTTPHeaders, bodyStream: InputStream, bodyContentLength: UInt64) {
            self.headers = headers
            self.bodyStream = bodyStream
            self.bodyContentLength = bodyContentLength
        }
    }

private var bodyParts: [BodyPart]


//编码方法
public func encode() throws -> Data {
        if let bodyPartError = bodyPartError {
            throw bodyPartError
        }

        var encoded = Data()

        bodyParts.first?.hasInitialBoundary = true
        bodyParts.last?.hasFinalBoundary = true
       
       //遍历 BodyPart 数据,拼接data
        for bodyPart in bodyParts {
            let encodedData = try encode(bodyPart)
            encoded.append(encodedData)
        }

        return encoded
    }

//编码每一个 BodyPart对象
 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
    }

private func encodeHeaders(for bodyPart: BodyPart) -> Data {
        var headerText = ""

        for (key, value) in bodyPart.headers {
            headerText += "\(key): \(value)\(EncodingCharacters.crlf)"
        }
        headerText += EncodingCharacters.crlf

        return headerText.data(using: String.Encoding.utf8, allowLossyConversion: false)!
    }

 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
    }
/* --------       boundary 生成相关方法       -------*/
 private func initialBoundaryData() -> Data {
        return BoundaryGenerator.boundaryData(forBoundaryType: .initial, boundary: boundary)
    }

    private func encapsulatedBoundaryData() -> Data {
        return BoundaryGenerator.boundaryData(forBoundaryType: .encapsulated, boundary: boundary)
    }

    private func finalBoundaryData() -> Data {
        return BoundaryGenerator.boundaryData(forBoundaryType: .final, boundary: boundary)
    }

struct BoundaryGenerator {
        enum BoundaryType {
            case initial, encapsulated, final
        }

        static func randomBoundary() -> String {
            return String(format: "alamofire.boundary.%08x%08x", arc4random(), arc4random())
        }

        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 生成相关方法       -------*/

//这是我外界调用的append方法,类似方法还有很多
//append 方法1
public func append(_ data: Data, withName name: String) {
        let headers = contentHeaders(withName: name)
        let stream = InputStream(data: data)
        let length = UInt64(data.count)
        
         //调用append方法2
        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
    }

//append 方法2
public func append(_ stream: InputStream, withLength length: UInt64, headers: HTTPHeaders) {
        //构造 BodyPart , 添加到bodyParts数组中
        let bodyPart = BodyPart(headers: headers, bodyStream: stream, bodyContentLength: length)
        bodyParts.append(bodyPart)
    }
}

  • 相关代码还是很多,总结一下:
    1.Alamofire构造上传请求的流程就是用 MultipartFormData 传递给外界
    2.外界调用append方法之后, MultipartFormDatabodyParts数组中添加BodyPart对象.
    3.MultipartFormDataencode()方法被调用后,遍历bodyParts数组,调用encode(bodyPart)编码每一个bodyPart得到每一个bodyPart编码后的data, 把这个data 添加到encode中构造总报文.

相关文章

网友评论

      本文标题:Alamofire Upload

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