Forward navigation:

Alamofire source learning directory collection

Introduction to the

When multi-form data needs to be uploaded, data needs to be encapsulated in body and separated by delimiters. Alamofire encapsulates MultipartFormData class to operate the encapsulation detection and splicing operator of multi-form data. Function:

  • Acceptable: Data, Stream, fileurl Data, together with name, mimeType
  • Data will eventually be encapsulated as InputStream. Small files can be directly passed in as Data. Large files need to use Stream or Fileurl, otherwise they will run out of memory
  • You can define a data delimiter. If not, use the default delimiter
  • The saved data type is the bodyParts array, and the final encoding into the URLRequest object is done in the MutipartUpload object.

MutipartUpload encodes the encapsulated MultipartFormData object into the body of the URLRequest. If the size of the data exceeds the limit, it will be stored in a temporary file first. Uploadable calls back to UploadRequest for initialization.

MultipartFormData:

We first encapsulate several helper types to process form data:

1. EncodingCharacters:

Encapsulate carriage return newline string:

    enum EncodingCharacters {
        static let crlf = "\r\n"
    }
Copy the code
2. BoundaryGenerator:

A delimiter that encapsulates multiple forms of data. This delimiter needs to be stored in the body header. There are three types of delimiters: beginning, middle, and end. Random delimiters are generated by default, and you can make your own delimiter strings when you use them. The output format is Data, which is inserted sequentially into the Data interval as the form Data is encoded.

    enum BoundaryGenerator {
        enum BoundaryType {
            case initial// Start: -- delimiter \r\n
            case encapsulated// Middle: \r\n-- delimiter \r\n
            case final// End: \r\n-- delimiter --\r\n
        }
        
        /// random delimiter
        /// Randomly generate two 32-bit unsigned integers, then convert to hexadecimal display, using 0 to complement 8 characters, prefixed
        static func randomBoundary(a) -> 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)
        }
        
        // Generate Data delimiter to concatenate 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)
        }
    }
Copy the code
3. The BodyPart class:

It encapsulates each form data object in multi-form data, holds the header data of the data, and the length of the body data. HasInitialBoundary and hasFinalBoundary are used to record the type of the delimiter before and after the data.

    class BodyPart {
        // The head of each body
        let headers: HTTPHeaders
        // body data stream
        let bodyStream: InputStream
        // Data length
        let bodyContentLength: UInt64
        
        /// Use the following two variables to control the type of the delimiter before and after the data. During the final encoding, turn on the BodyParts array header and tail switches, both of which are false for intermediate form data
        // Whether there is a start delimiter
        var hasInitialBoundary = false
        // Whether there is a trailing delimiter
        var hasFinalBoundary = false
        

        init(headers: HTTPHeaders.bodyStream: InputStream.bodyContentLength: UInt64) {
            self.headers = headers
            self.bodyStream = bodyStream
            self.bodyContentLength = bodyContentLength
        }
    }
Copy the code

Private attributes and initializations:

    When encoding data, the maximum memory capacity, default 10MB, encoding data to disk temporary files
    public static let encodingMemoryThreshold: UInt64 = 10 _000_000

    /// The content-type header for multi-form data defines multipart/form-data and delimiters
    open lazy var contentType: String = "multipart/form-data; boundary=\ [self.boundary)"

    /// The data size of all form data sections, excluding delimiters
    public var contentLength: UInt64 { bodyParts.reduce(0) { $0 + The $1.bodyContentLength } }

    /// The delimiter used to split the form data
    public let boundary: String

    /// add data of type fileurl and write data to temporary files
    let fileManager: FileManager

    /// Hold multiple forms of data array
    private var bodyParts: [BodyPart]
    /// Errors that occur while appending form data are thrown to the upper layer
    private var bodyPartError: AFError?
    /// Buffer size for reading and writing IOStream. Default: 1024 bytes
    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
    }
Copy the code

Append form data related

Five methods are provided to append three different form data types:

  1. Data+name (read directly from memory, can only be used for small files, file name and MIME type can be specified)
  2. Fileurl +name (not specifying the file name, mime type, based on the last filename and extension of fileurl, then call method 3)
  3. Fileurl +name+ file name+ MIME type (equivalent to 2)
  4. InputStream+data length + file name + MIME type (will wrap the MIME type as HTTPHeaders and call method 5)
  5. InputStream + + HTTPHeaders data length

Of the five methods above, the first four will eventually be called to the fifth one, and both Data and Fileurl will be converted to InputStream, which will then be encapsulated as BodyPart along with the form header.

1.Data+name (optional: +fileName+ MIME type)
    // Form data format for final encoding:
    /// - 'before delimiter (if the first block, there is no before delimiter)'
    /// - `Content-Disposition: form-data; name=#{name}; Filename =#{filename} '(form header)
    // - 'content-type: #{mimeType}'
    /// - `Data`
    /// - 'after the delimiter (if the last data block, the delimiter is the final delimiter)'
    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)
    }
Copy the code
2.fileurl+name
    // Form data format for final encoding:
    /// - 'before delimiter (if the first block, there is no before delimiter)'
    /// - `Content-Disposition: form-data; name=#{name}; Filename =#{filename from fileurl} '
    /// - 'content-type: #{mime Type obtained from fileurl}'
    /// -fileurl Reads Data
    /// - 'after the delimiter (if the last data block, the delimiter is the final delimiter)'
    /// The file name and MIME type are obtained from the file name and extension in the last path of fileurl
    public func append(_ fileURL: URL.withName name: String) {
        // Get the file name and mime type. If not, record an error and return
        let fileName = fileURL.lastPathComponent
        let pathExtension = fileURL.pathExtension

        if !fileName.isEmpty && !pathExtension.isEmpty {
            // Use the helper function to get the MIME type string
            let mime = mimeType(forPathExtension: pathExtension)
            // Call method 3 below to continue processing
            append(fileURL, withName: name, fileName: fileName, mimeType: mime)
        } else {
            setBodyPartError(withReason: .bodyPartFilenameInvalid(in: fileURL))
        }
    }
Copy the code
Fileurl +name+ file name+ MIME type
  • It can be used to upload large files, since InputStream is finally used to read temporary files.
  • Various judgments are made on fileurl, and any errors are thrown
    // Form data format for final encoding:
    /// - 'before delimiter (if the first block, there is no before delimiter)'
    /// - `Content-Disposition: form-data; name=#{name}; filename=#{filename}`
    /// - `Content-Type: #{mimeType}`
    /// -fileurl Reads Data
    /// - 'after the delimiter (if the last data block, the delimiter is the final delimiter)'
    public func append(_ fileURL: URL.withName name: String.fileName: String.mimeType: String) {
        // Encapsulate the form header
        let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
        // 1. Check whether the URL is valid, record an error and return
        guard fileURL.isFileURL else {
            setBodyPartError(withReason: .bodyPartURLInvalid(url: fileURL))
            return
        }
        // 2. Check whether the file URL is accessible
        do {
            let isReachable = try fileURL.checkPromisedItemIsReachable()// This method can quickly check whether the file is accessible. If the file is inaccessible and there is an error, it will record the error and return
            guard isReachable else {
                setBodyPartError(withReason: .bodyPartFileNotReachable(at: fileURL))
                return}}catch {
            // Catch the exception and record the error and return
            setBodyPartError(withReason: .bodyPartFileNotReachableWithError(atURL: fileURL, error: error))
            return
        }

        // 3. Check whether the URL is a directory. If the url is a directory, record the error and 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. Check whether the file size can be obtained. If the file size cannot be obtained, record an error and 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
        }

        Check whether InputStream can be created. If InputStream cannot be created, record an error and return

        guard let stream = InputStream(url: fileURL) else {
            setBodyPartError(withReason: .bodyPartInputStreamCreationFailed(for: fileURL))
            return
        }
        // 6. Call method 5 below to continue processing
        append(stream, withLength: bodyContentLength, headers: headers)
    }
Copy the code
4.InputStream+ Data length +name+ file name+ MIME type
    // Form data format for final encoding:
    /// - 'before delimiter (if the first block, there is no before delimiter)'
    /// - `Content-Disposition: form-data; name=#{name}; filename=#{filename}`
    /// - `Content-Type: #{mimeType}`
    /// -fileurl Reads Data
    /// - 'after the delimiter (if the last data block, the delimiter is the final delimiter)'
    public func append(_ stream: InputStream.withLength length: UInt64.name: String.fileName: String.mimeType: String) {
        // Wrap the form header
        let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
        // Continue using the following method
        append(stream, withLength: length, headers: headers)
    }
Copy the code
5.InputStream+data length + form header
    // Form data format for final encoding:
    /// - 'before delimiter (if the first block, there is no before delimiter)'
    /// - 'form header'
    /// - 'form data'
    /// - 'after the delimiter (if the last data block, the delimiter is the final delimiter)'
    public func append(_ stream: InputStream.withLength length: UInt64.headers: HTTPHeaders) {
        // Encapsulate as a BodyPart object
        let bodyPart = BodyPart(headers: headers, bodyStream: stream, bodyContentLength: length)
        // Store to array
        bodyParts.append(bodyPart)
    }
Copy the code

codingBodyPartobject

Alamofire provides two public methods to encode the encapsulated BodyPart object as a Data type to be used by the BodyData of URLRequest

  1. Code as Data (all Data in memory, note that too many files will explode memory)
  2. Use IOStream to save to a temporary file. (Since the above wrapped BodyPart holds data of type InputStream, you only need to create an OutputStream object to write to the file.
  • N additional private methods are provided to aid writing
1. Write to memory:

Public method:

    /// memory encoding, encoding as Data, note that large files are prone to memory explosion, large files use the following encoding to fileUrl method.
    public func encode(a) throws -> Data {
        // Check for save errors
        if let bodyPartError = bodyPartError {
            // Throw an exception if there is a save error
            throw bodyPartError
        }
        
        // Prepare to append data
        var encoded = Data(a)// Set the header and tail delimiters
        bodyParts.first?.hasInitialBoundary = true
        bodyParts.last?.hasFinalBoundary = true

        // Iterate over encoded data, then append
        for bodyPart in bodyParts {
            let encodedData = try encode(bodyPart)
            encoded.append(encodedData)
        }

        return encoded
    }
Copy the code

Private methods: Used to encode individual BodyPart data

    // Encode a single BodyPart data
    private func encode(_ bodyPart: BodyPart) throws -> Data {
        // Prepare data to be appended
        var encoded = Data(a)// Encode the delimiter first (either the start delimiter or the middle delimiter)
        let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData()
        encoded.append(initialData)

        // Encode the form header
        let headerData = encodeHeaders(for: bodyPart)
        encoded.append(headerData)

        // Encode form data
        let bodyStreamData = try encodeBodyStream(for: bodyPart)
        encoded.append(bodyStreamData)

        // If it is the last form data, encode the end delimiter
        if bodyPart.hasFinalBoundary {
            encoded.append(finalBoundaryData())
        }

        return encoded
    }

    // Encode the form header
    private func encodeHeaders(for bodyPart: BodyPart) -> Data {
        // Format: 'form header 1 name: form header 1 value \r\n Form header 2 Name: form header 2 value \r\n... \r\n`
        let headerText = bodyPart.headers.map { "\ [$0.name): \ [$0.value)\(EncodingCharacters.crlf)" }
            .joined()
            + EncodingCharacters.crlf
        / / utf8 encoding
        return Data(headerText.utf8)
    }

    // Encode form data
    private func encodeBodyStream(for bodyPart: BodyPart) throws -> Data {
        let inputStream = bodyPart.bodyStream
        / / open the stream
        inputStream.open()
        // Close the stream at the end of the method
        defer { inputStream.close() }

        var encoded = Data(a)// Loop read directly
        while inputStream.hasBytesAvailable {
            // The length of the buffer is 1024 bytes
            var buffer = [UInt8](repeating: 0, count: streamBufferSize)
            // Read one 1024Byte data at a time
            let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize)
            // Error directly throws an error
            if let error = inputStream.streamError {
                throw AFError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: error))
            }
            // Append data when it is read, otherwise break the loop
            if bytesRead > 0 {
                encoded.append(buffer, count: bytesRead)
            } else {
                break}}return encoded
    }
Copy the code
2. Write to file:

Use IOStream to write data to a file, suitable for large file handling public methods:

    /// Use IOStream to write data to files, suitable for large files
    public func writeEncodedData(to fileURL: URL) throws {
        if let bodyPartError = bodyPartError {
            // 1. Any error is thrown directly
            throw bodyPartError
        }
        
        if fileManager.fileExists(atPath: fileURL.path) {
            // 2. The file already has a throw error
            throw AFError.multipartEncodingFailed(reason: .outputStreamFileAlreadyExists(at: fileURL))
        } else if !fileURL.isFileURL {
            // 3. Url is not a file URL throws an error
            throw AFError.multipartEncodingFailed(reason: .outputStreamURLInvalid(url: fileURL))
        }

        guard let outputStream = OutputStream(url: fileURL, append: false) else {
            // 4. Failure to create OutputStream throws an error
            throw AFError.multipartEncodingFailed(reason: .outputStreamCreationFailed(for: fileURL))
        }
        / / open the OutputStream
        outputStream.open()
        // 方法结束关闭OutputStream
        defer { outputStream.close() }
        
        // Set the header and tail delimiter flags
        bodyParts.first?.hasInitialBoundary = true
        bodyParts.last?.hasFinalBoundary = true
        // Traversal writes data using private methods
        for bodyPart in bodyParts {
            try write(bodyPart, to: outputStream)
        }
    }
Copy the code

Intermediate private methods (there are only two methods that actually write data to OStream) :

    // encode a single BodyPart into OStream, sending four sub-methods
    private func write(_ bodyPart: BodyPart.to outputStream: OutputStream) throws {
        // Encode the front delimiter of the data (either a start delimiter or a middle delimiter)
        try writeInitialBoundaryData(for: bodyPart, to: outputStream)
        // Encode the form header
        try writeHeaderData(for: bodyPart, to: outputStream)
        // Encode form data
        try writeBodyStream(for: bodyPart, to: outputStream)
        // End of encoding delimiter (only the last data is encoded)
        try writeFinalBoundaryData(for: bodyPart, to: outputStream)
    }
    /// Encode the front delimiter of the data
    private func writeInitialBoundaryData(for bodyPart: BodyPart.to outputStream: OutputStream) throws {
        // The start delimiter type (which can be a start delimiter or an intermediate delimiter) is encoded as Data
        let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData()
        // Continue to distribute
        return try write(initialData, to: outputStream)
    }
    /// Encode the form header
    private func writeHeaderData(for bodyPart: BodyPart.to outputStream: OutputStream) throws {
        // Encode the form header as Data
        let headerData = encodeHeaders(for: bodyPart)
        // Continue to distribute
        return try write(headerData, to: outputStream)
    }
    /// encode form data
    private func writeBodyStream(for bodyPart: BodyPart.to outputStream: OutputStream) throws {
        let inputStream = bodyPart.bodyStream
        / / open the cost
        inputStream.open()
        // Close IStream at the end of the method
        defer { inputStream.close() }
        // Loop to read Bytes
        while inputStream.hasBytesAvailable {
            // Cache, 1024 bytes in size
            var buffer = [UInt8](repeating: 0, count: streamBufferSize)
            // Read 1024 bytes at a time
            let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize)

            if let streamError = inputStream.streamError {
                // If there is an error, throw it
                throw AFError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: streamError))
            }

            if bytesRead > 0 {
                // If the read data is smaller than the cache, fetch the previous valid data
                // We don't need to do this because we don't need to write to the file
                if buffer.count ! = bytesRead {
                    buffer = Array(buffer[0..<bytesRead])
                }
                // Continue to distribute (write the byte data array to OStream)
                try write(&buffer, to: outputStream)
            } else {
                break}}}// Encodes the final separator
    private func writeFinalBoundaryData(for bodyPart: BodyPart.to outputStream: OutputStream) throws {
        // Only the last form data is encoded with the final delimiter
        if bodyPart.hasFinalBoundary {
            // Encode as Data and continue to distribute
            return try write(finalBoundaryData(), to: outputStream)
        }
    }
Copy the code

Two methods to add data to OStream: intersect, intersect, intersect

    // Write OStream in Data format
    private func write(_ data: Data.to outputStream: OutputStream) throws {
        // Copy to byte array
        var buffer = [UInt8](repeating: 0, count: data.count)
        data.copyBytes(to: &buffer, count: data.count)
        // Continue to distribute
        return try write(&buffer, to: outputStream)
    }
    // Write OStream in byte array format
    private func write(_ buffer: inout [UInt8].to outputStream: OutputStream) throws {
        var bytesToWrite = buffer.count
        // loop to write data to OStream
        while bytesToWrite > 0, outputStream.hasSpaceAvailable {
            // Write data, record the number of bytes written
            let bytesWritten = outputStream.write(buffer, maxLength: bytesToWrite)

            // Error is thrown directly
            if let error = outputStream.streamError {
                throw AFError.multipartEncodingFailed(reason: .outputStreamWriteFailed(error: error))
            }
            // Subtract the data to be written
            bytesToWrite - = bytesWritten
            // buffer removes written data
            if bytesToWrite > 0 {
                buffer = Array(buffer[bytesWritten..<buffer.count])
            }
        }
    }
Copy the code

Auxiliary functions:

Auxiliary processing operations, including:

  • Gets the MIME type string based on the file extension
  • Encapsulate the form name field, filename, and MIME type as HTTPHeaders
  • Encode the three types of delimiters in the Data format
  • Errors that occur while saving appending form data will only be saved for the first error

MultipartUpload inner class

  • Alamofire internal class, used to quickly send upload requests
  • It is used to encode encapsulated MultipartFormData into Data or into a temporary file, which is then returned as a tuple wrapped with the associated URLRequest object
  • UploadConvertible is implemented to create UploadRequest

Properties and initialization

Because it is an internal class, all attributes are intenal and cannot be accessed outside the module

    // Lazy load property result, the first read will call the build method encoding MultipartFormData
    lazy var result = Result { try build() }
    // Whether it is a background task, if so, it encodes the form data into a temporary file
    let isInBackgroundSession: Bool
    // Form data
    let multipartFormData: MultipartFormData
    // Maximum memory overhead. Form data greater than this value is encoded into temporary files
    let encodingMemoryThreshold: UInt64
    // Associated URLRequestConvertible object
    let request: URLRequestConvertible
    // Manipulate temporary files
    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
    }
Copy the code

Core method: build code data

    // Encode data and return the tuple associated with UploadRequest.Uploadable
    func build(a) throws -> (request: URLRequest, uploadable: UploadRequest.Uploadable) {
        / / create the URLRequest
        var urlRequest = try request.asURLRequest()
        // Set the content-type field of the request header to 'multipart/form-data; Boundary ={form delimiter} '
        urlRequest.setValue(multipartFormData.contentType, forHTTPHeaderField: "Content-Type")
        // Uploadable after encoding
        let uploadable: UploadRequest.Uploadable
        if multipartFormData.contentLength < encodingMemoryThreshold && !isInBackgroundSession {
            // Form Data is less than the set memory overhead and cannot be directly encoded as Data for a background Session
            let data = try multipartFormData.encode()

            uploadable = .data(data)
        } else {
            // System cache directory
            let tempDirectoryURL = fileManager.temporaryDirectory
            // The directory to save the temporary form file
            let directoryURL = tempDirectoryURL.appendingPathComponent("org.alamofire.manager/multipart.form.data")
            // Temporary file name
            let fileName = UUID().uuidString
            // Temporary file URL
            let fileURL = directoryURL.appendingPathComponent(fileName)

            // Create a temporary form file directory
            try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)

            do {
                // Encode the form data into a temporary file
                try multipartFormData.writeEncodedData(to: fileURL)
            } catch {
                // If encoding fails, delete temporary files and throw an exception
                try? fileManager.removeItem(at: fileURL)
                throw error
            }
            / / return UploadRequest Uploadable, and set up after the completion of the need to delete temporary files
            uploadable = .file(fileURL, shouldRemove: true)}// Returns the tuple associated with UploadRequest.Uploadable
        return (request: urlRequest, uploadable: uploadable)
    }
Copy the code

The extension implementationUploadConvertibleThe protocol is used to createUploadRequest

extension MultipartUpload: UploadConvertible {
    func asURLRequest(a) throws -> URLRequest {
        try result.get().request
    }

    func createUploadable(a) throws -> UploadRequest.Uploadable {
        try result.get().uploadable
    }
}
Copy the code

The above is purely personal understanding, unavoidably wrong, if found there is a mistake, welcome to comment pointed out, will be the first time to modify, very grateful ~