Alamofire Upload

本篇來探索Alamofire的上傳數(shù)據(jù)邏輯

1.多部分表單數(shù)據(jù)上傳

示例:

SessionManager.default
            .upload(multipartFormData: { (multipartFormData) in
                
                multipartFormData.append("自定義數(shù)據(jù)0".data(using: .utf8)!, withName: "data0")
                
                multipartFormData.append("自定義數(shù)據(jù)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 多表單上傳為外界提供了一個(gè)閉包,方便構(gòu)造表單數(shù)據(jù)

來看下upload(multipartFormData:)方法相關(guān)的源碼

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()
            //調(diào)用暴露給外界的閉包 
            multipartFormData(formData)

            var tempFileURL: URL?

            do {
                //設(shè)置表單數(shù)據(jù)的類型 Content-Type
                var urlRequestWithContentType = try urlRequest.asURLRequest()
                urlRequestWithContentType.setValue(formData.contentType, forHTTPHeaderField: "Content-Type")
                
                //判斷當(dāng)前URLSession 是不是background類型
                let isBackgroundSession = self.session.configuration.identifier != nil
                
                //判斷當(dāng)前的表單數(shù)據(jù)大小是否小于某一閾值 && 不是backgroud類型的URLSession
                if formData.contentLength < encodingMemoryThreshold && !isBackgroundSession {
                   //真正構(gòu)造多表單數(shù)據(jù)
                    let data = try formData.encode()
                   
                   //判斷此次多表單數(shù)據(jù)編碼是否成功
                    let encodingResult = MultipartFormDataEncodingResult.success(
                        //調(diào)用upload 方法3.1
                        request: self.upload(data, with: urlRequestWithContentType),
                        streamingFromDisk: false,
                        streamFileURL: nil
                    )
                    //主線程調(diào)用 encodingCompletion 閉包
                    (queue ?? DispatchQueue.main).async { encodingCompletion?(encodingResult) }
                } else {
                   //當(dāng)前構(gòu)造的多表單數(shù)據(jù)大小大于某一閾值

                    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)
                  
                   //保存了一個(gè)文件url
                    tempFileURL = fileURL

                    var directoryError: Error?
                     
                    //創(chuàng)建文件
                    // 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
                        }
                    }
                   // 創(chuàng)建失敗就拋異常
                    if let directoryError = directoryError { throw directoryError }

                    //把構(gòu)造的多表單數(shù)據(jù)寫入文件
                    try formData.writeEncodedData(to: fileURL)

                    //調(diào)用另一個(gè)上傳 upload方法3.2,傳遞了fileURL
                    let upload = self.upload(fileURL, with: urlRequestWithContentType)
                   
                    //Taskdelegate.queue中添加 移除臨時(shí)文件的操作,再上傳結(jié)束后會(huì)被執(zhí)行
                    // 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
                        )
                          //主線程調(diào)用 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,這個(gè)是私有方法哦~
//此方法真正 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
               
              //這里面調(diào)用adapt 適配器,得到最終的請(qǐng)求
                switch self {
               //創(chuàng)建上傳data的task
                case let .data(data, urlRequest):
                    let urlRequest = try urlRequest.adapt(using: adapter)
                    task = queue.sync { session.uploadTask(with: urlRequest, from: data) }     
                 //創(chuàng)建上傳file的task
                case let .file(url, urlRequest):
                    let urlRequest = try urlRequest.adapt(using: adapter)
                    task = queue.sync { session.uploadTask(with: urlRequest, fromFile: url) }
                
                 //創(chuàng)建上傳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)
            }
        }
    }

}

方法流程跟下來還是挺長的,不知道有什么更直觀的說明方法,所以我在方法中加了必要的注釋,這里總結(jié)下:

  • 我們外界的調(diào)用的upload方法經(jīng)過 upload方法1-->upload方法2-->upload方法3.1 或者 upload方法3.2->upload方法4,其中upload方法2upload方法4是比較重要的環(huán)節(jié).

  • upload方法2 主要處理兩種情況:
    情況1:當(dāng)我們要上傳的數(shù)據(jù)大小小于我們?cè)O(shè)置的閾值時(shí),直接上傳拼接好的多部分表單數(shù)據(jù).
    情況2:當(dāng)我們要上傳的數(shù)據(jù)大于我們?cè)O(shè)置的閾值時(shí),先在本地沙盒創(chuàng)建文件,之后上傳fileUrl

  • upload方法4調(diào)用了 uploadable.task方法,uploadable.task 方法會(huì)根據(jù)之前對(duì)于多表單數(shù)據(jù)大小的判斷 創(chuàng)建對(duì)應(yīng)類型的 URLSessionTask, 最后調(diào)用 URLSessionTaskresume方法開始上傳.

2.data上傳

有了對(duì)于多部分表單數(shù)據(jù)上傳的分析,data上傳的流程應(yīng)該也是類似,來看源碼:

//upload 方法3.1
//調(diào)用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()
           //調(diào)用upload方法4
            return upload(.data(data, urlRequest))
        } catch {
            return upload(nil, failedWith: error)
        }
    }

  • 外界調(diào)用data 上傳 實(shí)際上是調(diào)用上面提到的 upload方法4,這里就不贅述了

3.stream 上傳

直接上代碼:

//外界調(diào)用 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 上傳實(shí)際上是調(diào)用 upload方法3.3,最終又會(huì)調(diào)用 upload方法4,可見其實(shí) upload方法3.x 都是 upload方法4的裝飾器.

4.上傳報(bào)文的構(gòu)建過程

上面敘述了三種上傳方式的方法流程,但是上傳請(qǐng)求要想成功,正確的報(bào)文格式是必須的,Alamofire幫助我們封裝了報(bào)文構(gòu)建的過程,為我們的開發(fā)提供了極大的便利,在開始探索報(bào)文構(gòu)造流程之前,先來看看HTTP 上傳報(bào)文的樣子.

多部分表單上傳為例:

上傳數(shù)據(jù)請(qǐng)求體

Alamofire 報(bào)文構(gòu)造過程,實(shí)際上就是構(gòu)造類似于上述數(shù)據(jù)的過程,還記得upload方法2中有一段let data = try formData.encode()嗎? 來看源碼:

//在upload方法2 中有這樣一段代碼:
let data = try formData.encode()

open class MultipartFormData {

//BodyPart 內(nèi)部類
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 數(shù)據(jù),拼接data
        for bodyPart in bodyParts {
            let encodedData = try encode(bodyPart)
            encoded.append(encodedData)
        }

        return encoded
    }

//編碼每一個(gè) BodyPart對(duì)象
 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 生成相關(guān)方法       -------*/
 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 生成相關(guān)方法       -------*/

//這是我外界調(diào)用的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)
        
         //調(diào)用append方法2
        append(stream, withLength: length, headers: headers)
    }

//構(gòu)造數(shù)據(jù)頭信息的方法
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) {
        //構(gòu)造 BodyPart , 添加到bodyParts數(shù)組中
        let bodyPart = BodyPart(headers: headers, bodyStream: stream, bodyContentLength: length)
        bodyParts.append(bodyPart)
    }
}

  • 相關(guān)代碼還是很多,總結(jié)一下:
    1.Alamofire構(gòu)造上傳請(qǐng)求的流程就是用 MultipartFormData 傳遞給外界
    2.外界調(diào)用append方法之后, MultipartFormDatabodyParts數(shù)組中添加BodyPart對(duì)象.
    3.MultipartFormDataencode()方法被調(diào)用后,遍歷bodyParts數(shù)組,調(diào)用encode(bodyPart)編碼每一個(gè)bodyPart得到每一個(gè)bodyPart編碼后的data, 把這個(gè)data 添加到encode中構(gòu)造總報(bào)文.
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市捧搞,隨后出現(xiàn)的幾起案子箕昭,更是在濱河造成了極大的恐慌栅炒,老刑警劉巖秫筏,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件种冬,死亡現(xiàn)場(chǎng)離奇詭異付燥,居然都是意外死亡敷钾,警方通過查閱死者的電腦和手機(jī)枝哄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來阻荒,“玉大人挠锥,你說我怎么就攤上這事∏壬模” “怎么了蓖租?”我有些...
    開封第一講書人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長羊壹。 經(jīng)常有香客問我蓖宦,道長,這世上最難降的妖魔是什么油猫? 我笑而不...
    開封第一講書人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任稠茂,我火速辦了婚禮,結(jié)果婚禮上情妖,老公的妹妹穿的比我還像新娘睬关。我一直安慰自己,他們只是感情好毡证,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開白布电爹。 她就那樣靜靜地躺著,像睡著了一般料睛。 火紅的嫁衣襯著肌膚如雪丐箩。 梳的紋絲不亂的頭發(fā)上摇邦,一...
    開封第一講書人閱讀 51,462評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音雏蛮,去河邊找鬼涎嚼。 笑死阱州,一個(gè)胖子當(dāng)著我的面吹牛挑秉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播苔货,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼犀概,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了夜惭?” 一聲冷哼從身側(cè)響起姻灶,我...
    開封第一講書人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎诈茧,沒想到半個(gè)月后产喉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡敢会,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年曾沈,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸥昏。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡塞俱,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出吏垮,到底是詐尸還是另有隱情障涯,我是刑警寧澤,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布膳汪,位于F島的核電站唯蝶,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏遗嗽。R本人自食惡果不足惜粘我,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望媳谁。 院中可真熱鬧涂滴,春花似錦、人聲如沸晴音。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽锤躁。三九已至搁料,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背郭计。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來泰國打工霸琴, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人昭伸。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓梧乘,卻偏偏與公主長得像,于是被迫代替她去往敵國和親庐杨。 傳聞我的和親對(duì)象是個(gè)殘疾皇子选调,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容