本篇來探索
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方法2
和upload方法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)建文件,之后上傳fileUrlupload方法4
調(diào)用了uploadable.task方法,
uploadable.task 方法會(huì)根據(jù)之前對(duì)于多表單數(shù)據(jù)大小的判斷
創(chuàng)建對(duì)應(yīng)類型的URLSessionTask
, 最后調(diào)用URLSessionTask
的resume方法
開始上傳.
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)文
的樣子.
以多部分表單上傳
為例:
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
方法之后,MultipartFormData
向bodyParts數(shù)組
中添加BodyPart
對(duì)象.
3.MultipartFormData
的encode()
方法被調(diào)用后,遍歷bodyParts數(shù)組
,調(diào)用encode(bodyPart)
編碼每一個(gè)bodyPart
得到每一個(gè)bodyPart
編碼后的data
, 把這個(gè)data
添加到encode
中構(gòu)造總報(bào)文.