須要上傳多表單數據時,須要將data封裝在body中,而且使用分隔符分隔開,Alamofire封裝了MultipartFormData類來操做多表單data的封裝檢測,拼接操做符等操做。
功能:swift
MutipartUpload類的做用爲把封裝處理好的MultipartFormData對象中的數據編碼進URLRequest的body中,在編碼時會進行判斷,若是data的大小超過了限制,則會先存入臨時文件,而後把文件url封裝爲UploadRequest.Uploadable回調給UploadRequest初始化使用。數組
封裝回車換行字符串:緩存
enum EncodingCharacters {
static let crlf = "\r\n"
}
複製代碼
封裝多表單數據的分隔符,該分隔符須要存放在body頭中,類型分爲:開頭,中間,結尾三種,不一樣類型先後帶有的換行符不一樣。
默認會生成隨機的分隔符,使用時也能夠本身制定分隔符字符串。
輸出格式爲Data,在對錶單數據編碼時,會按順序插入data間隔中。markdown
enum BoundaryGenerator {
enum BoundaryType {
case initial//起始: --分隔符\r\n
case encapsulated//中間: \r\n--分隔符\r\n
case final//結束: \r\n--分隔符--\r\n
}
/// 隨機分隔符
/// 隨機生成兩個32位無符號整數,而後轉成十六進制展現,使用0補足8個字符,加上前綴
static func randomBoundary() -> String {
let first = UInt32.random(in: UInt32.min...UInt32.max)
let second = UInt32.random(in: UInt32.min...UInt32.max)
return String(format: "alamofire.boundary.%08x%08x", first, second)
}
//生成分隔符Data, 拼接數據用
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 Data(boundaryText.utf8)
}
}
複製代碼
封裝了多表單數據中每個表單數據對象,持有數據的頭數據,body的數據長度,使用hasInitialBoundary與hasFinalBoundary來記錄數據先後分隔符的類型。app
class BodyPart {
// 每一個body的頭
let headers: HTTPHeaders
// body數據stream
let bodyStream: InputStream
// 數據長度
let bodyContentLength: UInt64
/// 使用下面兩個變量來控制數據先後分隔符的類型,在最終編碼時,把BodyParts數組的頭尾對應開關打開, 兩個都爲false表明中間表單數據
// 是否有開始分隔符
var hasInitialBoundary = false
// 是否有結尾分隔符
var hasFinalBoundary = false
init(headers: HTTPHeaders, bodyStream: InputStream, bodyContentLength: UInt64) {
self.headers = headers
self.bodyStream = bodyStream
self.bodyContentLength = bodyContentLength
}
}
複製代碼
/// 編碼數據時,最大的內存容量,默認10MB,超過則把數據編碼到磁盤臨時文件中
public static let encodingMemoryThreshold: UInt64 = 10_000_000
/// 多表單數據的頭部Content-Type, 定義了multipart/form-data與分隔符
open lazy var contentType: String = "multipart/form-data; boundary=\(self.boundary)"
/// 全部表單數據部分的data大小, 不包括分隔符
public var contentLength: UInt64 { bodyParts.reduce(0) { $0 + $1.bodyContentLength } }
/// 用來分割表單數據的分隔符
public let boundary: String
/// 添加fileurl類型的數據時用來操做文件使用, 以及將data寫入臨時文件時用
let fileManager: FileManager
/// 保存多表單數據的數組
private var bodyParts: [BodyPart]
/// 追加表單數據時出現的錯誤, 會拋給上層
private var bodyPartError: AFError?
/// 讀寫IOStream時的buffer大小,默認1024Byte
private let streamBufferSize: Int
public init(fileManager: FileManager = .default, boundary: String? = nil) {
self.fileManager = fileManager
self.boundary = boundary ?? BoundaryGenerator.randomBoundary()
bodyParts = []
//
// The optimal read/write buffer size in bytes for input and output streams is 1024 (1KB). For more
// information, please refer to the following article:
// - https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Streams/Articles/ReadingInputStreams.html
//
streamBufferSize = 1024
}
複製代碼
提供了5個方法來追加3中不一樣的表單數據類型:dom
上面5個方法中,前4個最終都會調用到第5個,Data與fileurl類型都會轉換成InputStream類型,而後跟表單頭一塊兒封裝成BodyPart類型保存。函數
/// 最終編碼的表單數據格式:
/// - `前分隔符(如果第一個數據塊, 就沒有前分隔符)`
/// - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (表單頭)
/// - `Content-Type: #{mimeType}` (表單頭)
/// - `Data`
/// - `後分隔符(如果最後一個數據塊, 後分隔符是終結分隔符)`
public func append(_ data: Data, withName name: String, fileName: String? = nil, mimeType: String? = nil) {
let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
let stream = InputStream(data: data)
let length = UInt64(data.count)
append(stream, withLength: length, headers: headers)
}
複製代碼
/// 最終編碼的表單數據格式:
/// - `前分隔符(如果第一個數據塊, 就沒有前分隔符)`
/// - `Content-Disposition: form-data; name=#{name}; filename=#{根據fileurl獲取到的文件名}`
/// - `Content-Type: #{根據fileurl獲取到的mime類型}`
/// - fileurl讀取出來的Data
/// - `後分隔符(如果最後一個數據塊, 後分隔符是終結分隔符)`
/// 文件名與mime類型是根據fileurl最後path中的文件名與擴展名獲取
public func append(_ fileURL: URL, withName name: String) {
// 獲取文件名與mime類型, 若讀取不到就記錄錯誤並return
let fileName = fileURL.lastPathComponent
let pathExtension = fileURL.pathExtension
if !fileName.isEmpty && !pathExtension.isEmpty {
// 使用輔助函數獲取mime類型字符串
let mime = mimeType(forPathExtension: pathExtension)
// 調用下面方法3繼續處理
append(fileURL, withName: name, fileName: fileName, mimeType: mime)
} else {
setBodyPartError(withReason: .bodyPartFilenameInvalid(in: fileURL))
}
}
複製代碼
/// 最終編碼的表單數據格式:
/// - `前分隔符(如果第一個數據塊, 就沒有前分隔符)`
/// - `Content-Disposition: form-data; name=#{name}; filename=#{filename}`
/// - `Content-Type: #{mimeType}`
/// - fileurl讀取出來的Data
/// - `後分隔符(如果最後一個數據塊, 後分隔符是終結分隔符)`
public func append(_ fileURL: URL, withName name: String, fileName: String, mimeType: String) {
//封裝表單頭
let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
// 1.檢測url是否合法, 不合法記錄錯誤並return
guard fileURL.isFileURL else {
setBodyPartError(withReason: .bodyPartURLInvalid(url: fileURL))
return
}
// 2.檢測文件url是否能夠訪問
do {
let isReachable = try fileURL.checkPromisedItemIsReachable()//這個方法能夠快速檢測文件是否能夠訪問, 當不可訪問並有錯誤時, 會記錄錯誤並return
guard isReachable else {
setBodyPartError(withReason: .bodyPartFileNotReachable(at: fileURL))
return
}
} catch {
// catch異常並記錄錯誤並return
setBodyPartError(withReason: .bodyPartFileNotReachableWithError(atURL: fileURL, error: error))
return
}
// 3.檢測url是不是目錄, 是目錄直接記錄錯誤並return
var isDirectory: ObjCBool = false
let path = fileURL.path
guard fileManager.fileExists(atPath: path, isDirectory: &isDirectory) && !isDirectory.boolValue else {
setBodyPartError(withReason: .bodyPartFileIsDirectory(at: fileURL))
return
}
// 4.檢測是否能獲取到文件大小, 沒法獲取文件大小時記錄錯誤並return
let bodyContentLength: UInt64
do {
guard let fileSize = try fileManager.attributesOfItem(atPath: path)[.size] as? NSNumber else {
setBodyPartError(withReason: .bodyPartFileSizeNotAvailable(at: fileURL))
return
}
bodyContentLength = fileSize.uint64Value
} catch {
setBodyPartError(withReason: .bodyPartFileSizeQueryFailedWithError(forURL: fileURL, error: error))
return
}
// 5.檢測可否建立InputStream, 沒法建立InputStream時記錄錯誤並return
guard let stream = InputStream(url: fileURL) else {
setBodyPartError(withReason: .bodyPartInputStreamCreationFailed(for: fileURL))
return
}
// 6.調用下面的方法5繼續處理
append(stream, withLength: bodyContentLength, headers: headers)
}
複製代碼
/// 最終編碼的表單數據格式:
/// - `前分隔符(如果第一個數據塊, 就沒有前分隔符)`
/// - `Content-Disposition: form-data; name=#{name}; filename=#{filename}`
/// - `Content-Type: #{mimeType}`
/// - fileurl讀取出來的Data
/// - `後分隔符(如果最後一個數據塊, 後分隔符是終結分隔符)`
public func append(_ stream: InputStream, withLength length: UInt64, name: String, fileName: String, mimeType: String) {
//封裝下表單頭
let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
//使用下面的方法5繼續
append(stream, withLength: length, headers: headers)
}
複製代碼
/// 最終編碼的表單數據格式:
/// - `前分隔符(如果第一個數據塊, 就沒有前分隔符)`
/// - `表單頭`
/// - `表單數據`
/// - `後分隔符(如果最後一個數據塊, 後分隔符是終結分隔符)`
public func append(_ stream: InputStream, withLength length: UInt64, headers: HTTPHeaders) {
//封裝成BodyPart對象
let bodyPart = BodyPart(headers: headers, bodyStream: stream, bodyContentLength: length)
//存入數組
bodyParts.append(bodyPart)
}
複製代碼
Alamofire提供了兩個公開方法來將封裝的BodyPart對象編碼爲Data類型,用來塞入URLRequest的bodydata使用post
公開方法:學習
/// 內存編碼, 編碼爲Data類型, 注意大文件容易爆內存, 大文件使用下面的編碼到fileUrl方法.
public func encode() throws -> Data {
// 檢測是否有保存錯誤
if let bodyPartError = bodyPartError {
// 有保存錯誤的話, 直接拋出異常
throw bodyPartError
}
// 準備追加數據
var encoded = Data()
// 設置頭尾分隔符
bodyParts.first?.hasInitialBoundary = true
bodyParts.last?.hasFinalBoundary = true
// 遍歷編碼data, 而後追加
for bodyPart in bodyParts {
let encodedData = try encode(bodyPart)
encoded.append(encodedData)
}
return encoded
}
複製代碼
私有方法:用來編碼單個BodyPart數據
// 編碼單個BodyPart數據
private func encode(_ bodyPart: BodyPart) throws -> Data {
// 準備追加用的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 {
//格式爲: `表單頭1名字: 表單頭1值\r\n表單頭2名字: 表單頭2值\r\n...\r\n`
let headerText = bodyPart.headers.map { "\($0.name): \($0.value)\(EncodingCharacters.crlf)" }
.joined()
+ EncodingCharacters.crlf
// utf8編碼
return Data(headerText.utf8)
}
// 編碼表單數據
private func encodeBodyStream(for bodyPart: BodyPart) throws -> Data {
let inputStream = bodyPart.bodyStream
// 打開stream
inputStream.open()
// 方法結束要關閉stream
defer { inputStream.close() }
var encoded = Data()
// 直接循環讀取
while inputStream.hasBytesAvailable {
// buffer,長度爲1024Byte
var buffer = [UInt8](repeating: 0, count: streamBufferSize)
// 一次讀取一個1024Byte的數據
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
}
複製代碼
使用IOStream往文件寫數據,適合大文件處理 公開方法:
/// 使用IOStream往文件寫數據, 適合處理大文件
public func writeEncodedData(to fileURL: URL) throws {
if let bodyPartError = bodyPartError {
// 1.有錯誤直接拋出
throw bodyPartError
}
if fileManager.fileExists(atPath: fileURL.path) {
// 2.文件已存在拋出錯誤
throw AFError.multipartEncodingFailed(reason: .outputStreamFileAlreadyExists(at: fileURL))
} else if !fileURL.isFileURL {
// 3.url不是文件url拋出錯誤
throw AFError.multipartEncodingFailed(reason: .outputStreamURLInvalid(url: fileURL))
}
guard let outputStream = OutputStream(url: fileURL, append: false) else {
// 4.建立OutputStream失敗拋出錯誤
throw AFError.multipartEncodingFailed(reason: .outputStreamCreationFailed(for: fileURL))
}
// 打開OutputStream
outputStream.open()
// 方法結束關閉OutputStream
defer { outputStream.close() }
// 設置頭尾分隔符標誌
bodyParts.first?.hasInitialBoundary = true
bodyParts.last?.hasFinalBoundary = true
//遍歷使用私有方法寫數據
for bodyPart in bodyParts {
try write(bodyPart, to: outputStream)
}
}
複製代碼
中間私有方法(只封裝處理下,還沒有往OStream裏寫數據,真正寫數據的只有兩個方法):
/// 編碼單個BodyPart到OStream, 會派發個四個子方法
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 writeInitialBoundaryData(for bodyPart: BodyPart, to outputStream: OutputStream) throws {
// 起始分隔符類型(可能爲起始分隔符, 也可能爲中間分隔符)編碼成Data
let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData()
// 繼續派發
return try write(initialData, to: outputStream)
}
/// 編碼表單頭
private func writeHeaderData(for bodyPart: BodyPart, to outputStream: OutputStream) throws {
// 把表單頭編碼成Data
let headerData = encodeHeaders(for: bodyPart)
// 繼續派發
return try write(headerData, to: outputStream)
}
/// 編碼表單數據
private func writeBodyStream(for bodyPart: BodyPart, to outputStream: OutputStream) throws {
let inputStream = bodyPart.bodyStream
// 打開IStream
inputStream.open()
// 方法結束要關閉IStream
defer { inputStream.close() }
// 循環讀取Bytes
while inputStream.hasBytesAvailable {
// 緩存, 大小爲1024Byte
var buffer = [UInt8](repeating: 0, count: streamBufferSize)
// 一次讀1024Byte
let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize)
if let streamError = inputStream.streamError {
// 有錯誤就拋出
throw AFError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: streamError))
}
if bytesRead > 0 {
// 若讀出來的數據小於緩存, 取前面有效數據
// 上面轉成Data不用這樣處理是由於不須要往文件裏寫
if buffer.count != bytesRead {
buffer = Array(buffer[0..<bytesRead])
}
// 繼續派發(把字節數據數組寫入OStream)
try write(&buffer, to: outputStream)
} else {
break
}
}
}
// 編碼最終分隔符
private func writeFinalBoundaryData(for bodyPart: BodyPart, to outputStream: OutputStream) throws {
// 只有最後一個表單數據才編碼最終分隔符
if bodyPart.hasFinalBoundary {
//編碼成Data, 而後繼續派發
return try write(finalBoundaryData(), to: outputStream)
}
}
複製代碼
最終往OStream中寫數據的兩個方法(最底層社畜(ಥ▽ಥ)o)
// 以Data格式寫入OStream
private func write(_ data: Data, to outputStream: OutputStream) throws {
//拷貝成字節數組
var buffer = [UInt8](repeating: 0, count: data.count)
data.copyBytes(to: &buffer, count: data.count)
// 繼續派發
return try write(&buffer, to: outputStream)
}
// 以字節數組格式寫入OStream
private func write(_ buffer: inout [UInt8], to outputStream: OutputStream) throws {
var bytesToWrite = buffer.count
// 循環往OStream中寫數據
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
// buffer除去寫入的數據
if bytesToWrite > 0 {
buffer = Array(buffer[bytesWritten..<buffer.count])
}
}
}
複製代碼
輔助處理操做,包括:
由於是內部類,全部所有屬性都是intenal的,模塊外部沒法訪問
// 懶加載屬性result, 首次讀取時會調用build方法編碼MultipartFormData數據
lazy var result = Result { try build() }
// 是不是後臺任務, 若是是, 就會把表單數據編碼到臨時文件中
let isInBackgroundSession: Bool
// 表單數據
let multipartFormData: MultipartFormData
// 最大內存開銷, 表單數據大於該值就會被編碼到臨時文件中
let encodingMemoryThreshold: UInt64
// 關聯的URLRequestConvertible協議對象
let request: URLRequestConvertible
// 操做臨時文件
let fileManager: FileManager
init(isInBackgroundSession: Bool, encodingMemoryThreshold: UInt64, request: URLRequestConvertible, multipartFormData: MultipartFormData) {
self.isInBackgroundSession = isInBackgroundSession
self.encodingMemoryThreshold = encodingMemoryThreshold
self.request = request
fileManager = multipartFormData.fileManager
self.multipartFormData = multipartFormData
}
複製代碼
//編碼數據, 並返回建立的URLRequest與UploadRequest.Uploadable關聯的元組
func build() throws -> (request: URLRequest, uploadable: UploadRequest.Uploadable) {
// 建立URLRequest
var urlRequest = try request.asURLRequest()
// 設置請求頭的Content-Type字段的, 值爲:`multipart/form-data; boundary={表單分隔符}`
urlRequest.setValue(multipartFormData.contentType, forHTTPHeaderField: "Content-Type")
// 編碼後的Uploadable
let uploadable: UploadRequest.Uploadable
if multipartFormData.contentLength < encodingMemoryThreshold && !isInBackgroundSession {
// 表單數據小於設置的內存開銷, 且不能爲後臺Session就直接編碼成Data類型
let data = try multipartFormData.encode()
uploadable = .data(data)
} else {
// 系統緩存目錄
let tempDirectoryURL = fileManager.temporaryDirectory
// 保存臨時表單文件的目錄
let directoryURL = tempDirectoryURL.appendingPathComponent("org.alamofire.manager/multipart.form.data")
// 臨時文件名
let fileName = UUID().uuidString
// 臨時文件url
let fileURL = directoryURL.appendingPathComponent(fileName)
// 建立臨時表單文件目錄
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
do {
// 把表單數據編碼到臨時文件
try multipartFormData.writeEncodedData(to: fileURL)
} catch {
// 若編碼失敗, 刪除臨時文件, 並拋出異常
try? fileManager.removeItem(at: fileURL)
throw error
}
// 返回的UploadRequest.Uploadable, 並設置須要完成後刪除臨時文件
uploadable = .file(fileURL, shouldRemove: true)
}
// 返回建立的URLRequest與UploadRequest.Uploadable關聯的元組
return (request: urlRequest, uploadable: uploadable)
}
複製代碼
extension MultipartUpload: UploadConvertible {
func asURLRequest() throws -> URLRequest {
try result.get().request
}
func createUploadable() throws -> UploadRequest.Uploadable {
try result.get().uploadable
}
}
複製代碼
以上純屬我的理解,不免有誤,如發現有錯誤的地方,歡迎評論指出,將第一時間修改,很是感謝~