使用 AFNetworking
進行文件上傳的使用方法大家想必已經(jīng)很熟悉. 那么作為其 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或者其他抓包工具, 運行項目查看我們本次請求.
其具體結(jié)構(gòu)很清晰
-
alamofire.bounday.91b32560f55a049f
分隔符 Content-Dispositon:form-data;name="name" (key)
/r/n
value
接下來分析下其內(nèi)部邏輯是如何處理的.
點擊進入 upload
具體實現(xiàn)方法, 中間過渡方法省略.
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??: 在全局隊列開啟異步執(zhí)行下面任務(wù).
- 2??: 初始化一個
MultipartFormData
對象并調(diào)用閉包參數(shù)孩等。調(diào)用用戶在外界閉包中所做的處理, 也就是收取用戶拼接的數(shù)據(jù)
let formData = MultipartFormData()
multipartFormData(formData)
執(zhí)行用戶傳遞的 multipartFormData
閉包, 我們在這個閉包中調(diào)用了 MultipartFormData
的 append
方法. 存到了這個臨時變量里.
- 3??: 設(shè)置請求頭格式
- 4??: 根據(jù)數(shù)據(jù)長度來區(qū)分處理, 長度為該方法的
usingThreshold
參數(shù). 用戶不傳時為默認閾值.
public static let multipartFormDataEncodingMemoryThreshold: UInt64 = 10_000_000
接下來:
- 當滿足長度閾值和
SessionManager
的會話環(huán)境為默認時, 進行formData
編碼, 上傳. - 否則,則將數(shù)據(jù)寫入文件, 上傳.
那么我們分別探討.
Alamofire form表單數(shù)據(jù)編碼
由于 upload
方法中, 調(diào)用了用戶所添加的參數(shù), 也就是調(diào)用 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
}
對照我們抓包結(jié)果來看, 其實就是
Content-Disposition: form-data; name=\"\(name)\"
然后進行 append
append(stream, withLength: length, headers: headers)
這里就是對 bodyParts 這個數(shù)組中添加 BodyPart
對象進去. 那么我們回到 判斷條件 滿足閾值判定滿足時, 首先調(diào)用 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
}
對頭尾進行標識. 然后遍歷調(diào)用 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
}
針對數(shù)組中的頭尾和中間三種情況,分別處理, 返回.
- 是首元素艾君,則調(diào)用
initialBoundaryData()
函數(shù)蓝角。 - 是中間元素亭枷,則調(diào)用
encapsulatedBoundaryData()
函數(shù)。 - 是尾元素捻勉,則調(diào)用
finalBoundaryData()
函數(shù)权她。
通過type區(qū)分最終調(diào)用如下函數(shù):
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"
}
還有值得一提的一點是在循環(huán)數(shù)組進行 encode 方法中還調(diào)用了一個
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
}
使用數(shù)據(jù)流stream 的方式讀取數(shù)據(jù), 利用其優(yōu)化措施, 防止內(nèi)存讀取存儲暴增問題.
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)
}
}
老樣子 前面文件名重復(fù), 文件路徑無效等異常處理直接過.
我們看到同樣是開流的方式. outputStream.open / .close
. 然后標識頭尾. 最后循環(huán)調(diào)用 .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)
}
然后分別調(diào)用四個過渡方法, 最終來到
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])
}
}
}
循環(huán)寫入到流中, 最后使用文件 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
的流程 , 啟動 -> 響應(yīng) -> 回調(diào). 不熟悉的可以去閱讀一下
Alamofire之Request(二)和隊列執(zhí)行順序分析
Alamofire之Request(一)
這兩篇文章. 本文就不重復(fù)闡述了.
.