Alamofire源码学习(九): ParameterEncoding与ParameterEncoder

2,133 阅读9分钟

往期导航:

Alamofire源码学习目录合集

简介

这俩都是用来在创建请求时,对参数进行编码用的,传入的参数

相同点

  • 都是在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:

/// 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跟json

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]=value2

  • 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/json

代码注释:

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自定义编码器,有点复杂,会放在下一篇中详解

以上纯属个人理解,难免有误,如发现有错误的地方,欢迎评论指出,将第一时间修改,非常感谢~