Alamofire源碼學習(九): ParameterEncoding與ParameterEncoder

往期導航:

Alamofire源碼學習目錄合集linux

簡介

這倆都是用來在建立請求時,對參數進行編碼用的,傳入的參數ios

相同點

  • 都是在Session中建立Request時使用
  • 都是用來把把參數編碼進URLRequest中
  • 均可以決定參數的編碼位置(url query string、body表單、bodyjson)
  • UploadRequest由於不帶參數,因此不會使用這倆

不一樣點

  • 初始化參數不一樣
  • ParameterEncoding只能編碼字典數據, ParameterEncoder用來編碼任意實現Encodable協議的數據類型
  • ParameterEncoding編碼實現簡單,由於都是字典數據,body表單編碼時,只須要先編碼成query string,而後utf8轉成data丟入body就行,ParameterEncoder使用的是一個本身Alamofire本身實現的URLEncodedFormEncoder來進行表單數據編碼,能夠編碼Date,Data等特殊數據
  • ParameterEncoding只有在建立DataRequest跟DownloadRequest時使用,DataStreamRequest沒法使用,而ParameterEncoder這三個Request子類都能用來初始化

ParameterEncoding

首先定義了Parameters別名爲[String: Any], 只能用來編碼字典參數, 協議很簡單,只有一個方法用來把參數編碼到URLRequest中,並返回新的URLRequest:json

/// A dictionary of parameters to apply to a `URLRequest`.
public typealias Parameters = [String: Any]

/// A type used to define how a set of parameters are applied to a `URLRequest`.
public protocol ParameterEncoding {
    /// 使用URLRequestConvertible建立URLRequest, 而後把字典參數編碼進URLRequest中, 能夠拋出異常, 拋出異常時會返回AFError.parameterEncodingFailed錯誤
    func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest
}
複製代碼

Alamofire提供了默認實現,分別用來編碼url query string跟jsonswift

1.URLEncoding默認實現,用來編碼url query string

  • 根據參數編碼的位置分爲: querystring與form表單兩種, 種類由Destination枚舉控制
  • 如果表單編碼, 請求頭的Content-Type會被設置爲application/x-www-form-urlencoded; charset=utf-8
  • 數組與字典經過遞歸來所有編碼
  • 由於沒有統一規範規定如何編碼集合參數, 所以數組參數編碼有兩個選擇, 由ArrayEncoding枚舉控制:, 默認帶方括號

    例子: key: [value1, value2]
    1.使用key後面跟方括號而後跟等號跟值,例如: key[]=value1&key[]=value2
    2.key後面不跟括號, 例如: key=value1&key=value2數組

  • 字典參數編碼使用key跟方括號subkey跟等號跟值

    key[subkey1]=value1&key[subkey2]=value2markdown

  • Bool值編碼能夠選擇使用數值0,1仍是使用字符串true,false, 由BoolEncoding枚舉控制, 默認爲數值

代碼註釋:閉包

public struct URLEncoding: ParameterEncoding {
    // MARK: 輔助數據類型

    /// 定義參數被編碼到url query中仍是body中
    public enum Destination {
        /// 有method決定(get, head, delete爲urlquery, 其餘爲body)
        case methodDependent
        /// url query
        case queryString
        /// body
        case httpBody
        /// 返回是否要把參數編入到url query中
        func encodesParametersInURL(for method: HTTPMethod) -> Bool {
            switch self {
            case .methodDependent: return [.get, .head, .delete].contains(method)
            case .queryString: return true
            case .httpBody: return false
            }
        }
    }

    /// 決定如何編碼Array
    public enum ArrayEncoding {
        /// key後跟括號編碼
        case brackets
        /// key後不跟括號編碼
        case noBrackets
        /// 對key進行編碼
        func encode(key: String) -> String {
            switch self {
            case .brackets:
                return "\(key)[]"
            case .noBrackets:
                return key
            }
        }
    }

    ///決定如何編碼Bool
    public enum BoolEncoding {
        /// 數字: 1, 0
        case numeric
        /// string: true, false
        case literal
        /// 對值進行編碼
        func encode(value: Bool) -> String {
            switch self {
            case .numeric:
                return value ? "1" : "0"
            case .literal:
                return value ? "true" : "false"
            }
        }
    }

    // MARK: 快速初始化的三個靜態計算屬性

    /// 默認使用method決定編碼位置, 數組使用帶括號, bool使用數字
    public static var `default`: URLEncoding { URLEncoding() }

    /// url query 編碼, 數組使用帶括號, bool使用數字
    public static var queryString: URLEncoding { URLEncoding(destination: .queryString) }

    /// form 表單編碼到body, 數組使用帶括號, bool使用數字
    public static var httpBody: URLEncoding { URLEncoding(destination: .httpBody) }

    //MARK: 屬性與初始化
    /// 參數編碼位置
    public let destination: Destination

    /// 數組編碼格式
    public let arrayEncoding: ArrayEncoding

    /// Bool編碼格式
    public let boolEncoding: BoolEncoding

    public init(destination: Destination = .methodDependent, arrayEncoding: ArrayEncoding = .brackets, boolEncoding: BoolEncoding = .numeric) {
        self.destination = destination
        self.arrayEncoding = arrayEncoding
        self.boolEncoding = boolEncoding
    }

    // MARK: 實現協議的編碼方法

    public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
        //先拿到URLRequest
        var urlRequest = try urlRequest.asURLRequest()
        //沒參數的話直接返回
        guard let parameters = parameters else { return urlRequest }
        //先拿到method, 而後使用method判斷下往哪裏編碼參數
        //不夠嚴謹, 若是method爲空, 應該拋出異常的. ParameterEncoder中有處理
        if let method = urlRequest.method, destination.encodesParametersInURL(for: method) {
            //url query編碼
            guard let url = urlRequest.url else {
                // url爲空直接拋出異常
                throw AFError.parameterEncodingFailed(reason: .missingURL)
            }
            if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), !parameters.isEmpty {
                //先獲取到已有的query string, 存在的話就加上個&, 而後拼接上新的query string
                let percentEncodedQuery = (urlComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(parameters)
                urlComponents.percentEncodedQuery = percentEncodedQuery
                urlRequest.url = urlComponents.url
            }
        } else {
            //body編碼
            if urlRequest.headers["Content-Type"] == nil {
                //設置Content-Type
                urlRequest.headers.update(.contentType("application/x-www-form-urlencoded; charset=utf-8"))
            }
            //把query string轉成utf8編碼丟入body中
            urlRequest.httpBody = Data(query(parameters).utf8)
        }

        return urlRequest
    }
    
    /// 對key-value對進行編碼, value主要處理字典,數組,nsnumber類型的bool,bool以及其餘值
    public func queryComponents(fromKey key: String, value: Any) -> [(String, String)] {
        var components: [(String, String)] = []
        switch value {
        case let dictionary as [String: Any]:
            //字典處理, 遍歷字典遞歸調用
            for (nestedKey, value) in dictionary {
                components += queryComponents(fromKey: "\(key)[\(nestedKey)]", value: value)
            }
        case let array as [Any]:
            for value in array {
                //數組處理, 根據數組key編碼的類型遍歷遞歸調用
                components += queryComponents(fromKey: arrayEncoding.encode(key: key), value: value)
            }
        case let number as NSNumber:
            //nsnumber使用objCType類判斷是不是bool
            if number.isBool {
                components.append((escape(key), escape(boolEncoding.encode(value: number.boolValue))))
            } else {
                components.append((escape(key), escape("\(number)")))
            }
        case let bool as Bool:
            //bool處理, 根據編碼類型來處理
            components.append((escape(key), escape(boolEncoding.encode(value: bool))))
        default:
            //其餘的,直接轉成string
            components.append((escape(key), escape("\(value)")))
        }
        return components
    }

    /// url轉義, 轉成百分號格式的
    /// 會忽略 :#[]@!$&'()*+,;=
    public func escape(_ string: String) -> String {
        string.addingPercentEncoding(withAllowedCharacters: .afURLQueryAllowed) ?? string
    }
    /// 把參數字典轉成query string
    private func query(_ parameters: [String: Any]) -> String {
        //存放key,value元組
        var components: [(String, String)] = []
        
        for key in parameters.keys.sorted(by: <) {
            let value = parameters[key]!//直接強制解包
            //對每一個key-value對進行編碼
            components += queryComponents(fromKey: key, value: value)
        }
        //拼接成query string返回
        return components.map { "\($0)=\($1)" }.joined(separator: "&")
    }
}
複製代碼

2.JSONEncoding默認實現,用來把參數編碼成json丟入body中

使用JSONSerialization來把參數字典編碼爲json, 必定會被編碼到body中, 而且會設置Content-Type爲application/jsonapp

代碼註釋:post

public struct JSONEncoding: ParameterEncoding {
    // MARK: 用來快速初始化的靜態計算變量

    //默認類型, 壓縮json格式
    public static var `default`: JSONEncoding { JSONEncoding() }

    //標準json格式
    public static var prettyPrinted: JSONEncoding { JSONEncoding(options: .prettyPrinted) }

    // MARK: 屬性與初始化
    
    //保存JSONSerialization.WritingOptions
    public let options: JSONSerialization.WritingOptions

    public init(options: JSONSerialization.WritingOptions = []) {
        self.options = options
    }

    // MARK: 實現協議的編碼方法

    public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
        //拿到Request
        var urlRequest = try urlRequest.asURLRequest()

        guard let parameters = parameters else { return urlRequest }

        do {
            //編碼成data
            let data = try JSONSerialization.data(withJSONObject: parameters, options: options)
            //設置Content-Type
            if urlRequest.headers["Content-Type"] == nil {
                urlRequest.headers.update(.contentType("application/json"))
            }
            //丟入body
            urlRequest.httpBody = data
        } catch {
            //解析json出錯就拋出錯誤
            throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
        }

        return urlRequest
    }
    
    //把json對象編碼進body中, 其實上面的編碼方法能夠直接掉這個方法, 兩個方法實現一毛同樣
    public func encode(_ urlRequest: URLRequestConvertible, withJSONObject jsonObject: Any? = nil) throws -> URLRequest {
        var urlRequest = try urlRequest.asURLRequest()

        guard let jsonObject = jsonObject else { return urlRequest }

        do {
            let data = try JSONSerialization.data(withJSONObject: jsonObject, options: options)

            if urlRequest.headers["Content-Type"] == nil {
                urlRequest.headers.update(.contentType("application/json"))
            }

            urlRequest.httpBody = data
        } catch {
            throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
        }

        return urlRequest
    }
}
複製代碼

最後還擴展了下OC的NSNumber類,添加了檢測是否爲Bool類型的方法:

extension NSNumber {
    fileprivate var isBool: Bool {
        // Use Obj-C type encoding to check whether the underlying type is a `Bool`, as it's guaranteed as part of
        // swift-corelibs-foundation, per [this discussion on the Swift forums](https://forums.swift.org/t/alamofire-on-linux-possible-but-not-release-ready/34553/22).
        String(cString: objCType) == "c"
    }
}
複製代碼

ParameterEncoder

協議很簡單,也是隻有一個方法,把Parameters類型的參數編碼進URLRequest中,可是要求Parameters類型必須符合Encodable協議。學習

public protocol ParameterEncoder {
    
    func encode<Parameters: Encodable>(_ parameters: Parameters?, into request: URLRequest) throws -> URLRequest
}
複製代碼

其實有不少地方相似ParameterEncoding,也是把參數編碼編碼進Request,編碼位置也是能夠控制,可是對參數要求不一樣:

ParameterEncoding要求參數是字典類型,字典的value是Any的,編碼爲url query string時會直接強制轉成String,所以對於標準類型之外的數據,編碼出來的值就會錯誤。編碼爲JSON時,標準類型之外的數據,會致使編碼錯誤,拋出異常
ParameterEncoder要求參數符合Encodable協議,編碼時使用的是Encoder協議對象,編碼爲json時,用的是JSONEncoder,編碼爲url query string時,用的是本身實現的URLEncodedFormEncoder編碼器

所以,若編碼的參數爲符合Encodable類型的字典時,使用兩種編碼方式都ok。好比parameter = ["a": 1, "b": 2]這樣的參數。

也有兩個默認實現,分別用來進行json編碼與url query string編碼:

1.JSONParameterEncoder編碼json數據

使用系統的JSONEncoder來編碼數據,能夠控制json的格式,ios11以上還支持根據key來排序(json字典爲無序),實現方法也是比較簡單:

open class JSONParameterEncoder: ParameterEncoder {
    //MARK: 用來快速建立對象的靜態計算屬性
    
    /// 默認類型, 使用默認的JSONEncoder初始化, 會壓縮json格式
    public static var `default`: JSONParameterEncoder { JSONParameterEncoder() }

    /// 使用標準json格式輸出
    public static var prettyPrinted: JSONParameterEncoder {
        let encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted

        return JSONParameterEncoder(encoder: encoder)
    }

    /// ios11以上支持輸出的json根據key排序
    @available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *)
    public static var sortedKeys: JSONParameterEncoder {
        let encoder = JSONEncoder()
        encoder.outputFormatting = .sortedKeys

        return JSONParameterEncoder(encoder: encoder)
    }
    
    // MARK: 屬性與初始化

    /// 用來編碼參數的JSONEncoder
    public let encoder: JSONEncoder

    public init(encoder: JSONEncoder = JSONEncoder()) {
        self.encoder = encoder
    }
    
    /// 實現協議的編碼方法:
    open func encode<Parameters: Encodable>(_ parameters: Parameters?, into request: URLRequest) throws -> URLRequest {
        //獲取參數
        guard let parameters = parameters else { return request }

        var request = request

        do {
            //把參數編碼成json data
            let data = try encoder.encode(parameters)
            //丟入body
            request.httpBody = data
            //設置Content-Type
            if request.headers["Content-Type"] == nil {
                request.headers.update(.contentType("application/json"))
            }
        } catch {
            //解析json異常就拋出錯誤
            throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
        }

        return request
    }
}
複製代碼

2.URLEncodedFormParameterEncoder編碼url query string數據

url編碼, 使用Destination來判斷編碼到url query仍是body中, 編碼數據使用的是URLEncodedFormEncoder

open class URLEncodedFormParameterEncoder: ParameterEncoder {
    /// 參數編碼位置,與ParameterEncoding同樣
    public enum Destination {
        case methodDependent
        case queryString
        case httpBody

        func encodesParametersInURL(for method: HTTPMethod) -> Bool {
            switch self {
            case .methodDependent: return [.get, .head, .delete].contains(method)
            case .queryString: return true
            case .httpBody: return false
            }
        }
    }

    // MARK: 默認初始化對象, 使用URLEncodedFormEncoder默認參數, 編碼位置由method決定
    public static var `default`: URLEncodedFormParameterEncoder { URLEncodedFormParameterEncoder() }

    /// 用來編碼數據的URLEncodedFormEncoder對象
    public let encoder: URLEncodedFormEncoder

    /// 編碼位置
    public let destination: Destination

    public init(encoder: URLEncodedFormEncoder = URLEncodedFormEncoder(), destination: Destination = .methodDependent) {
        self.encoder = encoder
        self.destination = destination
    }
    
    // 實現協議的編碼參數方法
    open func encode<Parameters: Encodable>(_ parameters: Parameters?, into request: URLRequest) throws -> URLRequest {
        //獲取參數
        guard let parameters = parameters else { return request }

        var request = request
        
        //首先要保證url存在
        guard let url = request.url else {
            throw AFError.parameterEncoderFailed(reason: .missingRequiredComponent(.url))
        }
        //而後保證method存在
        guard let method = request.method else {
            let rawValue = request.method?.rawValue ?? "nil"
            throw AFError.parameterEncoderFailed(reason: .missingRequiredComponent(.httpMethod(rawValue: rawValue)))
        }
        //根據編碼位置, 進行編碼操做
        if destination.encodesParametersInURL(for: method),
           var components = URLComponents(url: url, resolvingAgainstBaseURL: false) {
            //url query
            //這裏格式化了下寫法寫下詳細註釋(吹爆swift!)
            let query: String = try Result<String, Error> {//初始化Request(參數爲能夠拋出異常的閉包)
                try encoder.encode(parameters)//編碼參數
            }
            .mapError {//編碼出錯轉換爲AFError
                AFError.parameterEncoderFailed(reason: .encoderFailed(error: $0))
            }
            .get()//get能夠獲取成功數據, 若爲error, 會拋出異常
            //這裏寫法也很騷, 把原querystring與新的querystring組合成一個[String?]數組, 而後compactMap去掉nil, 再用&組合起來
            let newQueryString = [components.percentEncodedQuery, query].compactMap { $0 }.joinedWithAmpersands()
            components.percentEncodedQuery = newQueryString.isEmpty ? nil : newQueryString
            //url不能爲空
            guard let newURL = components.url else {
                throw AFError.parameterEncoderFailed(reason: .missingRequiredComponent(.url))
            }

            request.url = newURL
        } else {
            //設置Content-Type
            if request.headers["Content-Type"] == nil {
                request.headers.update(.contentType("application/x-www-form-urlencoded; charset=utf-8"))
            }
            //編碼, 而後丟入body 吐槽:尾隨閉包+寫一行讀起來太難受了
            request.httpBody = try Result<Data, Error> {
                try encoder.encode(parameters)
            }
            .mapError {
                AFError.parameterEncoderFailed(reason: .encoderFailed(error: $0))
            }
            .get()
        }

        return request
    }
}
複製代碼

URLEncodedFormEncoder用來把數據編碼成url query string的核心類

  • 該類被聲明爲final, 不容許繼承, 只容許在初始化時經過參數控制行爲.
  • 該類定義了不少控制編碼行爲的數據類型,並在初始化時設置這些類型
  • 編碼時使用自定義的實現了Encoder協議的 _URLEncodedFormEncoder 內部類型的屬性。用來編碼數據
  • 編碼的數據儲存對象爲URLEncodedFormComponent枚舉,能夠保存string,array以及使用元組數組表明的object三種類型,使用元組數組來表明object類型可使參數保持順序。
  • 持有一個URLEncodedFormContext上下文屬性,該屬性持有URLEncodedFormComponent來保存數據,做爲上下文遞歸編碼時傳遞使用
  • 最後實現了一個URLEncodedFormSerializer序列器,用來把編碼完成的URLEncodedFormComponent數據序列化成query string給上層

這個URLEncodedFormEncoder自定義編碼器,有點複雜,會放在下一篇中詳解

以上純屬我的理解,不免有誤,如發現有錯誤的地方,歡迎評論指出,將第一時間修改,很是感謝~

相關文章
相關標籤/搜索