阅读 265

Alamofire源码学习(十): URLEncodedFormEncoder--自定义的表单参数编码器

往期导航:

Alamofire源码学习目录合集

简介:

在上一篇Alamofire源码学习(九): ParameterEncoding与ParameterEncoder中有提到, ParameterEncoder协议用来把任何遵循Encodable协议的参数编码添加到URLRequest当中,在默认实现URLEncodedFormParameterEncoder类中,编码参数用的就是这个自定义实现的URLEncodedFormEncoder编码器,用来把Encodable协议的参数编码为url query string,参数类型可以是基本数据类型(Int,Double等)也可以是其他高级类型(Data,Date,Decimal等),也可以是实现了Encodable协议的自定义类型。

URLEncodedFormEncoder--假装是编码器,实际上只是用来定义类型的容器类

该类被修饰为final,不允许继承,只允许使用初始化参数控制编码逻辑,其实URLEncodedFormEncoder类本身并没有实现编码方法,只是定义了N多编码时行为的定义,真正用来编码的是内部类 _URLEncodedFormEncoder ,所有对参数的编码处理都在该内部类中完成,编码后的数据保存在URLEncodedFormComponent中,传递给上层URLEncodedFormParameterEncoder时,使用URLEncodedFormSerializer将编码后的数据序列化为url query string。

部分数据类型的编码格式

URLEncodedFormEncoder定义了4中数据类型的编码格式,编码时可自由选择:

1.数组编码方式:
    /// 数组编码方式
    public enum ArrayEncoding {
        case brackets
        case noBrackets
        func encode(_ key: String) -> String {
            switch self {
            case .brackets: return "\(key)[]"
            case .noBrackets: return key
            }
        }
    }
复制代码
2.Bool编码方式:
    /// Bool编码方式
    public enum BoolEncoding {
        case numeric
        case literal
        func encode(_ value: Bool) -> String {
            switch self {
            case .numeric: return value ? "1" : "0"
            case .literal: return value ? "true" : "false"
            }
        }
    }
复制代码
3.Data编码方式:

Data的延迟编码方式为:在自定义编码时,若对Data的编码方式是deferredToData类型,会创建一个子编码器对Data进行编码,会使用Data默认的编码格式(UInt8数组)

    /// Data编码方式
    public enum DataEncoding {
        /// 延迟编码成Data
        case deferredToData
        /// base64字符串编码
        case base64
        /// 使用闭包来编码成自定义格式的字符串
        case custom((Data) throws -> String)

        /// 编码data
        func encode(_ data: Data) throws -> String? {
            switch self {
            case .deferredToData: return nil
            case .base64: return data.base64EncodedString()
            case let .custom(encoding): return try encoding(data)
            }
        }
    }
复制代码
4.Date编码方式

Date的延迟编码方式类似Data的,不过默认编码格式是会编码为Double类型的距离1970.1.1的秒.毫秒

    public enum DateEncoding {
        /// 用来把Data转换成ISO8601字符串的Formatter
        private static let iso8601Formatter: ISO8601DateFormatter = {
            let formatter = ISO8601DateFormatter()
            formatter.formatOptions = .withInternetDateTime
            return formatter
        }()

        /// 延迟编码成Date
        case deferredToDate
        /// 编码成从1910.1.1开始的秒字符串
        case secondsSince1970
        /// 编码成从1910.1.1开始的毫秒秒字符串
        case millisecondsSince1970
        /// 编码成ISO8601标准字符串
        case iso8601
        /// 使用自定义的格式器编码
        case formatted(DateFormatter)
        /// 用闭包来自定义编码Date
        case custom((Date) throws -> String)

        func encode(_ date: Date) throws -> String? {
            switch self {
            case .deferredToDate:
                return nil
            case .secondsSince1970:
                return String(date.timeIntervalSince1970)
            case .millisecondsSince1970:
                return String(date.timeIntervalSince1970 * 1000.0)
            case .iso8601:
                return DateEncoding.iso8601Formatter.string(from: date)
            case let .formatted(formatter):
                return formatter.string(from: date)
            case let .custom(closure):
                return try closure(date)
            }
        }
    }
复制代码

Key的编码格式

Key的编码方式是从系统的JSONEncoder.KeyEncodingStrategyXMLEncoder.KeyEncodingStrategy共同派生出来的编码方式主要是针对Key的字符串表现形式进行了定义:

    public enum KeyEncoding {
        /// 默认格式,不编码key
        case useDefaultKeys
        /// 驼峰转下划线蛇形: oneTwoThree -> one_two_three
        case convertToSnakeCase
        /// 驼峰转串形: ontTwoThree -> one-two-three
        case convertToKebabCase
        /// 首字母大写: oneTwoThree -> OneTwoThree
        case capitalized
        /// 全部转为大写: oneTwoThree -> ONETWOTHREE
        case uppercased
        /// 全部转为小写: oneTwoThree -> onetwothree
        case lowercased
        /// 使用闭包来自定义编码规则
        case custom((String) -> String)

        /// 编码key, 上面枚举不太理解的话, 可以看各个枚举对应的方法实现就可以理解了
        func encode(_ key: String) -> String {
            switch self {
            case .useDefaultKeys: return key//不处理
            case .convertToSnakeCase: return convertToSnakeCase(key)
            case .convertToKebabCase: return convertToKebabCase(key)
            case .capitalized: return String(key.prefix(1).uppercased() + key.dropFirst())//首字母大写然后加上剩余字符串
            case .uppercased: return key.uppercased()//全部大写
            case .lowercased: return key.lowercased()//全部小写
            case let .custom(encoding): return encoding(key)
            }
        }
        //蛇形
        private func convertToSnakeCase(_ key: String) -> String {
            convert(key, usingSeparator: "_")
        }
        //串形
        private func convertToKebabCase(_ key: String) -> String {
            convert(key, usingSeparator: "-")
        }
        //把驼峰写法的key转为使用separator分割的新key
        // 算法: 从开始查找字符串大小写部分, 假定字符串开始为小写, 碰到第一个大写字母:
        // 1.若只有一个大写字母, 就认为该大写字母到下一个大写字母前的字符串为一个单词
        // 2.否则, 认为该大写字母到小写字母前的倒数第二个字母为一个单词
        // 反复查找, 把字符串分为多个子字符串, 全部转为小写, 使用separator连接
        // 例如:myProperty -> my_property, myURLProperty -> my_url_property
        // 注意: 因为会便利ztring, 所以会有明显的性能影响
        private func convert(_ key: String, usingSeparator separator: String) -> String {
            guard !key.isEmpty else { return key }

            // 存放分割字符串的range
            var words: [Range<String.Index>] = []
            
            // 开始查找的index
            var wordStart = key.startIndex
            // 查找字符串的range
            var searchRange = key.index(after: wordStart)..<key.endIndex

            // 开始遍历字符串查找
            while let upperCaseRange = key.rangeOfCharacter(from: CharacterSet.uppercaseLetters, options: [], range: searchRange) {
                // 大写字母前的range(第一个小写字符串)
                let untilUpperCase = wordStart..<upperCaseRange.lowerBound
                // 加入words
                words.append(untilUpperCase)

                // 从大写字符串后找小写字符串的range
                searchRange = upperCaseRange.lowerBound..<searchRange.upperBound
                guard let lowerCaseRange = key.rangeOfCharacter(from: CharacterSet.lowercaseLetters, options: [], range: searchRange) else {
                    // There are no more lower case letters. Just end here.
                    // 若没有小写字符串了, 跳出循环
                    wordStart = searchRange.lowerBound
                    break
                }

                // 如果大写字符串长度大于1, 就把大写字符串认为是一个word
                let nextCharacterAfterCapital = key.index(after: upperCaseRange.lowerBound)//大写字符串range的startIndex的后一位
                if lowerCaseRange.lowerBound == nextCharacterAfterCapital {
                    // 是否与小写字符串的startIndex相等, 相等表示大写字符串只有一个字符, 就把这个字符跟后面的小写字符串一起当做一个word
                    wordStart = upperCaseRange.lowerBound
                } else {
                    // 否则把大写字符串开始到小写字符串的startIndex的前一位当做一个word
                    // 例如: URLProperty搜索出来大写字符串为URLP, 就把URL当做一个word, Property当做后一个word
                    let beforeLowerIndex = key.index(before: lowerCaseRange.lowerBound)
                    // 加入words
                    words.append(upperCaseRange.lowerBound..<beforeLowerIndex)

                    // 设置wordStart, 下次查找到字符串后取word用
                    wordStart = beforeLowerIndex
                }
                // 下次搜索从小写字符串range的尾部直到搜索range的尾部
                searchRange = lowerCaseRange.upperBound..<searchRange.upperBound
            }
            // 循环完成, 加入结尾range
            words.append(wordStart..<searchRange.upperBound)
            // 全部变成小写, 使用separator连接
            let result = words.map { range in
                key[range].lowercased()
            }.joined(separator: separator)

            return result
        }
    }
复制代码

空格的编码格式

空格的编码有两个选择:

    public enum SpaceEncoding {
        /// 转为%20
        case percentEscaped
        /// 转为+
        case plusReplaced

        func encode(_ string: String) -> String {
            switch self {
            case .percentEscaped: return string.replacingOccurrences(of: " ", with: "%20")
            case .plusReplaced: return string.replacingOccurrences(of: " ", with: "+")
            }
        }
    }
复制代码

编码错误时的错误定义

定义了Error枚举来在编码出错时抛出异常,只有一个错误:invalidRootObjecturl query string编码要求参数根必须是key-value类型的

    /// URL编码错误
    public enum Error: Swift.Error {
        /// root节点必须是key-value数据
        case invalidRootObject(String)

        var localizedDescription: String {
            switch self {
            case let .invalidRootObject(object):
                return "URLEncodedFormEncoder requires keyed root object. Received \(object) instead."
            }
        }
    }
复制代码

常量属性与初始化:

初始化时使用了8个参数控制编码行为,其中alphabetizeKeyValuePairs参数可以使得编码出来的key-value数据使用key排序,不过该api仅限iOS13以上使用。

    /// 编码后的键值对是否根据key排序, 默认为true, 相同的params编码出来的字典数据是相同的, 如果设置了false, 因为字典的无序性, 会导致相同params编码出来的字典顺序不同
    public let alphabetizeKeyValuePairs: Bool
    /// The `ArrayEncoding` to use.
    public let arrayEncoding: ArrayEncoding
    /// The `BoolEncoding` to use.
    public let boolEncoding: BoolEncoding
    /// THe `DataEncoding` to use.
    public let dataEncoding: DataEncoding
    /// The `DateEncoding` to use.
    public let dateEncoding: DateEncoding
    /// The `KeyEncoding` to use.
    public let keyEncoding: KeyEncoding
    /// The `SpaceEncoding` to use.
    public let spaceEncoding: SpaceEncoding
    /// The `CharacterSet` of allowed (non-escaped) characters.
    public var allowedCharacters: CharacterSet

    // 初始化, 全部属性都有默认值
    public init(alphabetizeKeyValuePairs: Bool = true,
                arrayEncoding: ArrayEncoding = .brackets,
                boolEncoding: BoolEncoding = .numeric,
                dataEncoding: DataEncoding = .base64,
                dateEncoding: DateEncoding = .deferredToDate,
                keyEncoding: KeyEncoding = .useDefaultKeys,
                spaceEncoding: SpaceEncoding = .percentEscaped,
                allowedCharacters: CharacterSet = .afURLQueryAllowed) {
        self.alphabetizeKeyValuePairs = alphabetizeKeyValuePairs
        self.arrayEncoding = arrayEncoding
        self.boolEncoding = boolEncoding
        self.dataEncoding = dataEncoding
        self.dateEncoding = dateEncoding
        self.keyEncoding = keyEncoding
        self.spaceEncoding = spaceEncoding
        self.allowedCharacters = allowedCharacters
    }
复制代码

一个internal编码方法,两个public编码方法

内部编码方法把参数编码为URLEncodedFormComponent类型 两个公开编码方法会先调用内部编码方法,再使用URLEncodedFormSerializer解析为String或者Data类型返回。

    /// 核心编码方法, 把value编码成自定义的URLEncodedFormComponent数据(默认会编码成字典类型),
    /// 另外两个编码方法都会先调用该方法, 在对数据进行处理
    func encode(_ value: Encodable) throws -> URLEncodedFormComponent {
        // 表单数据的格式, 默认为字典类型
        let context = URLEncodedFormContext(.object([]))
        // 编码器
        let encoder = _URLEncodedFormEncoder(context: context,
                                             boolEncoding: boolEncoding,
                                             dataEncoding: dataEncoding,
                                             dateEncoding: dateEncoding)
        try value.encode(to: encoder)

        return context.component
    }

    public func encode(_ value: Encodable) throws -> String {
        // 先编码成URLEncodedFormComponent
        let component: URLEncodedFormComponent = try encode(value)

        // 转成字典类型数据这里object的类型是一个包含key,value元组
        // 不是直接的字典, 因为字典无序, 使用元组数组可以保证keyvalue的顺序
        guard case let .object(object) = component else {
            throw Error.invalidRootObject("\(component)")
        }
        // 序列化
        let serializer = URLEncodedFormSerializer(alphabetizeKeyValuePairs: alphabetizeKeyValuePairs,
                                                  arrayEncoding: arrayEncoding,
                                                  keyEncoding: keyEncoding,
                                                  spaceEncoding: spaceEncoding,
                                                  allowedCharacters: allowedCharacters)
        // 序列化成query string
        let query = serializer.serialize(object)

        return query
    }

    public func encode(_ value: Encodable) throws -> Data {
        // 先转成query string
        let string: String = try encode(value)
        // 再utf8编码
        return Data(string.utf8)
    }
复制代码

URLEncodedFormComponent枚举--递归保存数据

  • 这个枚举就是编码后的数据类型,有三个带参case,分别对应:字符串,数组,有序字典
  • 因为字典本身无序,因此定义了保存key,value元组的数组来表示有序字典
  • 可以把URLEncodedFormComponent类型的数据根据path数组追加到自己当前数据中,使用递归来根据key一个个找下去保存,最终能编码出一个数据树。
  • 因为是往自己里面塞数据,因此setvalue的方法声明成了mutating,会改变自身值
//MARK: URLEncodedFormComponent,保存编码的数据
enum URLEncodedFormComponent {
    //对应key-value数据对
    typealias Object = [(key: String, value: URLEncodedFormComponent)]

    case string(String)//字符串
    case array([URLEncodedFormComponent])//数组
    case object(Object)//有序字典

    /// 快速获取数组数据, 字符串与字典会返回nil
    var array: [URLEncodedFormComponent]? {
        switch self {
        case let .array(array): return array
        default: return nil
        }
    }

    /// 快速获取字典数据, 字符串与数组会返回nil
    var object: Object? {
        switch self {
        case let .object(object): return object
        default: return nil
        }
    }

    /// 把值根据keypaths设置进来
    /// 参数value是要设置的值, path是keypath数组, 有三种情况:
    /// 1.path为空数组, 表示直接把value设置成自身 例: data.set(to: "hello", at: [])
    /// 2.path为int类型, 表示需要使用数组保存 例: data.set(to: "hello", at: ["1"])
    /// 3.path为string类型, 表示需要使用字典保存 例: data.set(to: "hello", at: ["path", "to", "value"])
    /// 保存方式为从第一个path开始递归到最后一个path, 根据path从当前自身节点开始查找创建一个个节点, 把值在最后一个节点, 然后倒腾回来根据path类型设置一个个层级的数据类型, 最后完成整个数据树
    public mutating func set(to value: URLEncodedFormComponent, at path: [CodingKey]) {
        set(&self, to: value, at: path)
    }

    /// 递归设置key-value
    /// 参数context: 递归的当前节点, value: 需要保存的值, path: 保存的keypaths
    /// 最初调用时, context时self节点, 随着每一次递归, 会根据path的顺序一层层往下传, context也会一层层节点的往下查找创建, 最后完成整个数据树
    private func set(_ context: inout URLEncodedFormComponent, to value: URLEncodedFormComponent, at path: [CodingKey]) {
        guard path.count >= 1 else {
            //如果path为空数组, 直接把value设置给当前节点, return
            context = value
            return
        }

        //第一个path
        let end = path[0]
        //子节点, 需要根据path去判断子节点的类型
        var child: URLEncodedFormComponent
        switch path.count {
        case 1:
            //path只有一个, 就保存child就行
            child = value
        case 2...:
            //paht有多个, 需要递归
            if let index = end.intValue {
                //第一个path是int, 需要用数组保存
                //获取当前节点的array类型
                let array = context.array ?? []
                if array.count > index {
                    // array数据大于index表示更新, 取出需要更新的节点作为子节点
                    child = array[index]
                } else {
                    //否则是新增, 创建子节点
                    child = .array([])
                }
                //开始递归
                set(&child, to: value, at: Array(path[1...]))
            } else {
                //用字典保存
                //根据第一个path, 找到子节点, 找得到就是更新数据, 找不到就是新增需要创建子节点
                child = context.object?.first { $0.key == end.stringValue }?.value ?? .object(.init())
                //递归
                set(&child, to: value, at: Array(path[1...]))
            }
        default: fatalError("Unreachable")
        }
        //递归回来, 这时候子节点自身已经处理完毕, 需要把子节点(child)插入到当前节点(context)中
        
        if let index = end.intValue {
            //第一个path是数组
            if var array = context.array {
                //如果当前节点为数组节点, 直接把child插入或者更新到数组中
                if array.count > index {
                    //更新
                    array[index] = child
                } else {
                    //插入
                    array.append(child)
                }
                //更新当前节点
                context = .array(array)
            } else {
                //否则, 直接把当前节点设置为数组节点
                context = .array([child])
            }
        } else {
            //第一个path是字典
            if var object = context.object {
                //如果当前节点为字典节点, 把child插入或更新进去
                if let index = object.firstIndex(where: { $0.key == end.stringValue }) {
                    //更新
                    object[index] = (key: end.stringValue, value: child)
                } else {
                    //插入
                    object.append((key: end.stringValue, value: child))
                }
                //更新当前节点
                context = .object(object)
            } else {
                //否则, 把当前节点设置为字典节点
                context = .object([(key: end.stringValue, value: child)])
            }
        }
    }
}
复制代码

URLEncodedFormContext--编码时传递的上下文

只是持有着一个URLEncodedFormComponent枚举属性,用来编码时上下传递,逐个往里面塞入新的编码数据。最终编码完成返回的结果就是持有的属性

//MARK: URLEncodedFormContext编码中递归传递的上下文, 持有保存的数据对象
final class URLEncodedFormContext {
    var component: URLEncodedFormComponent

    init(_ component: URLEncodedFormComponent) {
        self.component = component
    }
}
复制代码

AnyCodingKey--实现了CodeKey,Hashable协议的key类型

可以保存字典的key(String类型),可以保存数组的index(Int类型)

// 把int或者string转换成CodingKey的容器
struct AnyCodingKey: CodingKey, Hashable {
    let stringValue: String
    let intValue: Int?

    init?(stringValue: String) {
        self.stringValue = stringValue
        intValue = nil
    }

    init?(intValue: Int) {
        stringValue = "\(intValue)"
        self.intValue = intValue
    }

    init<Key>(_ base: Key) where Key: CodingKey {
        if let intValue = base.intValue {
            self.init(intValue: intValue)!
        } else {
            self.init(stringValue: base.stringValue)!
        }
    }
}
复制代码

_URLEncodedFormEncoder--真正干活的类

  • 实现了Encoder协议,真正用来编码数据的类
  • 持有一个封装URLEncodedFormContext上下文属性,用来保存编码的数据,同时在递归编码数据时传递使用。
  • 可以设置是那种数据类型的编码方式
  • 因为实现了Encoder协议,所以持有[CodingKey]类型的codingPath数组
// 用来把数据编码成URLEncodedFormComponent表单数据的编码器
final class _URLEncodedFormEncoder {
    // Encoder协议属性, 用来编码key-value数据
    var codingPath: [CodingKey]
    // userinfo, 该编码器不支持userinfo, 所以直接返回空数据
    var userInfo: [CodingUserInfoKey: Any] { [:] }
    // 编码时递归传递的上下文, 包裹着URLEncodedFormComponent最终数据
    let context: URLEncodedFormContext
    //三种特殊类型的编码方式
    private let boolEncoding: URLEncodedFormEncoder.BoolEncoding
    private let dataEncoding: URLEncodedFormEncoder.DataEncoding
    private let dateEncoding: URLEncodedFormEncoder.DateEncoding

    init(context: URLEncodedFormContext,
         codingPath: [CodingKey] = [],
         boolEncoding: URLEncodedFormEncoder.BoolEncoding,
         dataEncoding: URLEncodedFormEncoder.DataEncoding,
         dateEncoding: URLEncodedFormEncoder.DateEncoding) {
        self.context = context
        self.codingPath = codingPath
        self.boolEncoding = boolEncoding
        self.dataEncoding = dataEncoding
        self.dateEncoding = dateEncoding
    }
}
复制代码

Encoder协议的实现使用扩展封装

主要是需要返回三种数据编码后的储存容器。三种容器均使用内部类的形式写在下面的扩展中

//MARK: 扩展_URLEncodedFormEncoder实现Encoder协议, 用来编码数据
extension _URLEncodedFormEncoder: Encoder {
    // 保存key-value数据的容器
    func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key: CodingKey {
        //返回_URLEncodedFormEncoder.KeyedContainer, 数据会存在context中
        let container = _URLEncodedFormEncoder.KeyedContainer<Key>(context: context,
                                                                   codingPath: codingPath,
                                                                   boolEncoding: boolEncoding,
                                                                   dataEncoding: dataEncoding,
                                                                   dateEncoding: dateEncoding)
        return KeyedEncodingContainer(container)
    }
    //保存数组数据的容器
    func unkeyedContainer() -> UnkeyedEncodingContainer {
        _URLEncodedFormEncoder.UnkeyedContainer(context: context,
                                                codingPath: codingPath,
                                                boolEncoding: boolEncoding,
                                                dataEncoding: dataEncoding,
                                                dateEncoding: dateEncoding)
    }
    //保存单个值的容器
    func singleValueContainer() -> SingleValueEncodingContainer {
        _URLEncodedFormEncoder.SingleValueContainer(context: context,
                                                    codingPath: codingPath,
                                                    boolEncoding: boolEncoding,
                                                    dataEncoding: dataEncoding,
                                                    dateEncoding: dateEncoding)
    }
}
复制代码

1.KeyedContainer内部类,用来保存key-value数据:

主要作用是用来编码字典数据,本身类声明中只是保存一些属性与定义了一个追加keypath的方法:

extension _URLEncodedFormEncoder {
    
    final class KeyedContainer<Key> where Key: CodingKey {
        var codingPath: [CodingKey]

        private let context: URLEncodedFormContext
        private let boolEncoding: URLEncodedFormEncoder.BoolEncoding
        private let dataEncoding: URLEncodedFormEncoder.DataEncoding
        private let dateEncoding: URLEncodedFormEncoder.DateEncoding

        init(context: URLEncodedFormContext,
             codingPath: [CodingKey],
             boolEncoding: URLEncodedFormEncoder.BoolEncoding,
             dataEncoding: URLEncodedFormEncoder.DataEncoding,
             dateEncoding: URLEncodedFormEncoder.DateEncoding) {
            self.context = context
            self.codingPath = codingPath
            self.boolEncoding = boolEncoding
            self.dataEncoding = dataEncoding
            self.dateEncoding = dateEncoding
        }
        
        //嵌套追加key, 在现有keypaths上继续追加
        private func nestedCodingPath(for key: CodingKey) -> [CodingKey] {
            codingPath + [key]
        }
    }
}
复制代码
扩展实现KeyedEncodingContainerProtocol协议:

用来编码数据,主要是追加keypath然后根据value的类型把编码任务派发下去,派发出去的子类型也是三种:


extension _URLEncodedFormEncoder.KeyedContainer: KeyedEncodingContainerProtocol {
    // 不支持编码nil数据, 所以直接抛出异常
    func encodeNil(forKey key: Key) throws {
        let context = EncodingError.Context(codingPath: codingPath,
                                            debugDescription: "URLEncodedFormEncoder cannot encode nil values.")
        throw EncodingError.invalidValue("\(key): nil", context)
    }
    // 编码单个数据
    func encode<T>(_ value: T, forKey key: Key) throws where T: Encodable {
        // 创建一个嵌套值编码器来编码
        var container = nestedSingleValueEncoder(for: key)
        try container.encode(value)
    }
    
    //创建嵌套单个数据编码容器
    func nestedSingleValueEncoder(for key: Key) -> SingleValueEncodingContainer {
        let container = _URLEncodedFormEncoder.SingleValueContainer(context: context,
                                                                    codingPath: nestedCodingPath(for: key),
                                                                    boolEncoding: boolEncoding,
                                                                    dataEncoding: dataEncoding,
                                                                    dateEncoding: dateEncoding)

        return container
    }
    //嵌套数组编码容器
    func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
        let container = _URLEncodedFormEncoder.UnkeyedContainer(context: context,
                                                                codingPath: nestedCodingPath(for: key),
                                                                boolEncoding: boolEncoding,
                                                                dataEncoding: dataEncoding,
                                                                dateEncoding: dateEncoding)

        return container
    }
    //嵌套key-value编码容器
    func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> where NestedKey: CodingKey {
        let container = _URLEncodedFormEncoder.KeyedContainer<NestedKey>(context: context,
                                                                         codingPath: nestedCodingPath(for: key),
                                                                         boolEncoding: boolEncoding,
                                                                         dataEncoding: dataEncoding,
                                                                         dateEncoding: dateEncoding)

        return KeyedEncodingContainer(container)
    }
    //父编码器
    func superEncoder() -> Encoder {
        _URLEncodedFormEncoder(context: context,
                               codingPath: codingPath,
                               boolEncoding: boolEncoding,
                               dataEncoding: dataEncoding,
                               dateEncoding: dateEncoding)
    }
    //父编码器
    func superEncoder(forKey key: Key) -> Encoder {
        _URLEncodedFormEncoder(context: context,
                               codingPath: nestedCodingPath(for: key),
                               boolEncoding: boolEncoding,
                               dataEncoding: dataEncoding,
                               dateEncoding: dateEncoding)
    }
}
复制代码

2.UnkeyedContainer内部类,用来编码数组数据

类的声明中也只是定义了一些属性,与上面KeyedEncodingContainer不同的是持有一个count属性用来记录数据个数作为index keypath使用

extension _URLEncodedFormEncoder {
    final class UnkeyedContainer {
        var codingPath: [CodingKey]

        var count = 0//记录数组index, 每新增一个值就会+1
        var nestedCodingPath: [CodingKey] {
            codingPath + [AnyCodingKey(intValue: count)!]
        }

        private let context: URLEncodedFormContext
        private let boolEncoding: URLEncodedFormEncoder.BoolEncoding
        private let dataEncoding: URLEncodedFormEncoder.DataEncoding
        private let dateEncoding: URLEncodedFormEncoder.DateEncoding

        init(context: URLEncodedFormContext,
             codingPath: [CodingKey],
             boolEncoding: URLEncodedFormEncoder.BoolEncoding,
             dataEncoding: URLEncodedFormEncoder.DataEncoding,
             dateEncoding: URLEncodedFormEncoder.DateEncoding) {
            self.context = context
            self.codingPath = codingPath
            self.boolEncoding = boolEncoding
            self.dataEncoding = dataEncoding
            self.dateEncoding = dateEncoding
        }
    }
}
复制代码
扩展实现UnkeyedEncodingContainer协议

用来编码数据,类似KeyedContainer,因为value是容器,因此编码操作也只是追加keypath,然后把编码任务派发下去,派发出去的也是三种:

extension _URLEncodedFormEncoder.UnkeyedContainer: UnkeyedEncodingContainer {
    //也是不支持编码nil
    func encodeNil() throws {
        let context = EncodingError.Context(codingPath: codingPath,
                                            debugDescription: "URLEncodedFormEncoder cannot encode nil values.")
        throw EncodingError.invalidValue("nil", context)
    }
    //编码单个值
    func encode<T>(_ value: T) throws where T: Encodable {
        //使用单数据编码容器编码
        var container = nestedSingleValueContainer()
        try container.encode(value)
    }
    //单个数据编码容器
    func nestedSingleValueContainer() -> SingleValueEncodingContainer {
        //编码完成,个数+1
        defer { count += 1 }

        return _URLEncodedFormEncoder.SingleValueContainer(context: context,
                                                           codingPath: nestedCodingPath,
                                                           boolEncoding: boolEncoding,
                                                           dataEncoding: dataEncoding,
                                                           dateEncoding: dateEncoding)
    }
    //key-value编码容器
    func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> where NestedKey: CodingKey {
        defer { count += 1 }
        let container = _URLEncodedFormEncoder.KeyedContainer<NestedKey>(context: context,
                                                                         codingPath: nestedCodingPath,
                                                                         boolEncoding: boolEncoding,
                                                                         dataEncoding: dataEncoding,
                                                                         dateEncoding: dateEncoding)

        return KeyedEncodingContainer(container)
    }
    //数组编码容器
    func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
        defer { count += 1 }

        return _URLEncodedFormEncoder.UnkeyedContainer(context: context,
                                                       codingPath: nestedCodingPath,
                                                       boolEncoding: boolEncoding,
                                                       dataEncoding: dataEncoding,
                                                       dateEncoding: dateEncoding)
    }
    //父编码器
    func superEncoder() -> Encoder {
        defer { count += 1 }

        return _URLEncodedFormEncoder(context: context,
                                      codingPath: codingPath,
                                      boolEncoding: boolEncoding,
                                      dataEncoding: dataEncoding,
                                      dateEncoding: dateEncoding)
    }
}
复制代码

3.SingleValueContainer内部类,真正编码值的类

  • 前面两个容器类都是记录下keypath然后派发出去递归编码,只有这个类是用来编码具体值
  • 声明中除了基本属性外,还有一个canEncodeNewValue属性,用来标记是否已经编码过值了,如果已经编码的key重复,会抛出错误
  • 扩展中定义了一大堆用来编码数据的方法,包括基本数据类型与特殊类型,使用泛型统一处理
extension _URLEncodedFormEncoder {
    final class SingleValueContainer {
        var codingPath: [CodingKey]

        private var canEncodeNewValue = true

        private let context: URLEncodedFormContext
        private let boolEncoding: URLEncodedFormEncoder.BoolEncoding
        private let dataEncoding: URLEncodedFormEncoder.DataEncoding
        private let dateEncoding: URLEncodedFormEncoder.DateEncoding

        init(context: URLEncodedFormContext,
             codingPath: [CodingKey],
             boolEncoding: URLEncodedFormEncoder.BoolEncoding,
             dataEncoding: URLEncodedFormEncoder.DataEncoding,
             dateEncoding: URLEncodedFormEncoder.DateEncoding) {
            self.context = context
            self.codingPath = codingPath
            self.boolEncoding = boolEncoding
            self.dataEncoding = dataEncoding
            self.dateEncoding = dateEncoding
        }
        //检测是否能编码新数据(编码一个值后canEncodeNewValue就会被设置为false, 就不允许在编码新值了)
        private func checkCanEncode(value: Any?) throws {
            guard canEncodeNewValue else {
                let context = EncodingError.Context(codingPath: codingPath,
                                                    debugDescription: "Attempt to encode value through single value container when previously value already encoded.")
                throw EncodingError.invalidValue(value as Any, context)
            }
        }
    }
}
复制代码
扩展实现SingleValueEncodingContainer协议
  • 对于基本类型,均使用泛型encode(value:as:)方法来编码成String格式
  • 对于其他类型,使用泛型的encode方法来编码先试图根据用户设置的编码格式来处理,碰到延迟编码或者没有自定义编码格式,就使用新的编码器编码成默认格式
  • 每次编码前均检查是否编码过相同keypath的数据,重复的keypath直接抛出异常

extension _URLEncodedFormEncoder.SingleValueContainer: SingleValueEncodingContainer {
    //不支持编码nil
    func encodeNil() throws {
        try checkCanEncode(value: nil)
        defer { canEncodeNewValue = false }

        let context = EncodingError.Context(codingPath: codingPath,
                                            debugDescription: "URLEncodedFormEncoder cannot encode nil values.")
        throw EncodingError.invalidValue("nil", context)
    }
    
    //MARK: 一大堆编码值得方法, 最终都是调用一个私有方法来编码
    
    func encode(_ value: Bool) throws {
        try encode(value, as: String(boolEncoding.encode(value)))
    }

    func encode(_ value: String) throws {
        try encode(value, as: value)
    }

    func encode(_ value: Double) throws {
        try encode(value, as: String(value))
    }

    func encode(_ value: Float) throws {
        try encode(value, as: String(value))
    }

    func encode(_ value: Int) throws {
        try encode(value, as: String(value))
    }

    func encode(_ value: Int8) throws {
        try encode(value, as: String(value))
    }

    func encode(_ value: Int16) throws {
        try encode(value, as: String(value))
    }

    func encode(_ value: Int32) throws {
        try encode(value, as: String(value))
    }

    func encode(_ value: Int64) throws {
        try encode(value, as: String(value))
    }

    func encode(_ value: UInt) throws {
        try encode(value, as: String(value))
    }

    func encode(_ value: UInt8) throws {
        try encode(value, as: String(value))
    }

    func encode(_ value: UInt16) throws {
        try encode(value, as: String(value))
    }

    func encode(_ value: UInt32) throws {
        try encode(value, as: String(value))
    }

    func encode(_ value: UInt64) throws {
        try encode(value, as: String(value))
    }

    //私有的泛型编码数据方法
    private func encode<T>(_ value: T, as string: String) throws where T: Encodable {
        //先检查是否能编码新值
        try checkCanEncode(value: value)
        //作用于结束后(编码完成),把开关设置为不允许编码新值
        defer { canEncodeNewValue = false }
        //把值使用string存进context
        context.component.set(to: .string(string), at: codingPath)
    }

    // 编码非标准类型的泛型数据
    // 除了上面的标准类型外, 其他数据类型的编码会调用该泛型方法
    // 这里对Date, Data, Decimal先进行了判断处理, 会先试图以原数据类型进行编码, 如果没有规定编码方法, 就使用_URLEncodedFormEncoder对value再次进行编码, 系统会使用value的底层数据类型进行再次编码(Date会使用Double, Data会使用[UInt8]数组)
    func encode<T>(_ value: T) throws where T: Encodable {
        //
        switch value {
        case let date as Date:
            //Date判断下是否使用Date默认类型进行延迟编码
            guard let string = try dateEncoding.encode(date) else {
                //如果是用默认类型进行延迟编码, 就使用_URLEncodedFormEncoder再次编码
                try attemptToEncode(value)
                return
            }
            //否则使用string编码
            try encode(value, as: string)
        case let data as Data:
            //Data的处理类似上面Date处理
            guard let string = try dataEncoding.encode(data) else {
                try attemptToEncode(value)
                return
            }

            try encode(value, as: string)
        case let decimal as Decimal:
            // Decimal默认的编码数据类型是对象, 所以这里拦截下, 转成String格式
            try encode(value, as: String(describing: decimal))
        default:
            // 其他非标准类型全部使用默认类型编码
            try attemptToEncode(value)
        }
    }
    // 编码时二次调用, 使用value的原类型的默认编码格式来编码处理
    private func attemptToEncode<T>(_ value: T) throws where T: Encodable {
        try checkCanEncode(value: value)
        defer { canEncodeNewValue = false }

        let encoder = _URLEncodedFormEncoder(context: context,
                                             codingPath: codingPath,
                                             boolEncoding: boolEncoding,
                                             dataEncoding: dataEncoding,
                                             dateEncoding: dateEncoding)
        try value.encode(to: encoder)
    }
}
复制代码

URLEncodedFormSerializer--序列化编码的结果数据

编码完成后保存数据的是URLEncodedFormComponent枚举类型,这是个数据树,需要把数据树转换成String或者Data返回给上层。因此定义了该解析器,用来把结果序列化成String

  • 定义了5个属性控制编码行为
  • 解析从根字典开始,递归调用解析方法解析子字典、数组
  • 最终的叶子节点一定是String格式
  • key-value排序,url转义,使用&拼接成query string都是在该类中完成
final class URLEncodedFormSerializer {
    //是否把key-value数据排序
    private let alphabetizeKeyValuePairs: Bool
    private let arrayEncoding: URLEncodedFormEncoder.ArrayEncoding
    private let keyEncoding: URLEncodedFormEncoder.KeyEncoding
    private let spaceEncoding: URLEncodedFormEncoder.SpaceEncoding
    private let allowedCharacters: CharacterSet

    init(alphabetizeKeyValuePairs: Bool,
         arrayEncoding: URLEncodedFormEncoder.ArrayEncoding,
         keyEncoding: URLEncodedFormEncoder.KeyEncoding,
         spaceEncoding: URLEncodedFormEncoder.SpaceEncoding,
         allowedCharacters: CharacterSet) {
        self.alphabetizeKeyValuePairs = alphabetizeKeyValuePairs
        self.arrayEncoding = arrayEncoding
        self.keyEncoding = keyEncoding
        self.spaceEncoding = spaceEncoding
        self.allowedCharacters = allowedCharacters
    }
    
    //MARK: 四个解析方法, 嵌套调用
    
    //解析根字典对象
    func serialize(_ object: URLEncodedFormComponent.Object) -> String {
        var output: [String] = []
        for (key, component) in object {
            //便利字典, 把每对数据解析为string
            let value = serialize(component, forKey: key)
            output.append(value)
        }
        //排序
        output = alphabetizeKeyValuePairs ? output.sorted() : output
        //使用&拼接string返回
        return output.joinedWithAmpersands()
    }

    //解析字典中的对象, 格式为: key=value
    func serialize(_ component: URLEncodedFormComponent, forKey key: String) -> String {
        switch component {
        //string直接对key进行编码,然后拼接成字符串
        case let .string(string): return "\(escape(keyEncoding.encode(key)))=\(escape(string))"
        //数组字典调下面两个解析方法
        case let .array(array): return serialize(array, forKey: key)
        case let .object(object): return serialize(object, forKey: key)
        }
    }
    //字典中的字典对象, 格式为: key[subKey]=value
    func serialize(_ object: URLEncodedFormComponent.Object, forKey key: String) -> String {
        var segments: [String] = object.map { subKey, value in
            let keyPath = "[\(subKey)]"
            return serialize(value, forKey: key + keyPath)
        }
        segments = alphabetizeKeyValuePairs ? segments.sorted() : segments

        return segments.joinedWithAmpersands()
    }
    //字典中的数组对象, 格式为: key[]=value或者key=value
    func serialize(_ array: [URLEncodedFormComponent], forKey key: String) -> String {
        var segments: [String] = array.map { component in
            let keyPath = arrayEncoding.encode(key)
            return serialize(component, forKey: keyPath)
        }
        segments = alphabetizeKeyValuePairs ? segments.sorted() : segments

        return segments.joinedWithAmpersands()
    }
    //url转义
    func escape(_ query: String) -> String {
        var allowedCharactersWithSpace = allowedCharacters
        allowedCharactersWithSpace.insert(charactersIn: " ")
        let escapedQuery = query.addingPercentEncoding(withAllowedCharacters: allowedCharactersWithSpace) ?? query
        let spaceEncodedQuery = spaceEncoding.encode(escapedQuery)

        return spaceEncodedQuery
    }
}
复制代码

两个工具扩展

  • 扩展Array用来使用&拼接最终的query string
  • 扩展CharacterSet用来定义不需要url转义的字符
//使用&连接
extension Array where Element == String {
    func joinedWithAmpersands() -> String {
        joined(separator: "&")
    }
}

// 需要转义的字符
extension CharacterSet {
    /// Creates a CharacterSet from RFC 3986 allowed characters.
    ///
    /// RFC 3986 states that the following characters are "reserved" characters.
    ///
    /// - General Delimiters: ":", "#", "[", "]", "@", "?", "/"
    /// - Sub-Delimiters: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "="
    ///
    /// In RFC 3986 - Section 3.4, it states that the "?" and "/" characters should not be escaped to allow
    /// query strings to include a URL. Therefore, all "reserved" characters with the exception of "?" and "/"
    /// should be percent-escaped in the query string.
    public static let afURLQueryAllowed: CharacterSet = {
        let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4
        let subDelimitersToEncode = "!$&'()*+,;="
        let encodableDelimiters = CharacterSet(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")

        return CharacterSet.urlQueryAllowed.subtracting(encodableDelimiters)
    }()
}
复制代码