Alamofire源碼學習(十): URLEncodedFormEncoder--自定義的表單參數編碼器

往期導航:

Alamofire源碼學習目錄合集git

簡介:

在上一篇Alamofire源碼學習(九): ParameterEncoding與ParameterEncoder中有提到, ParameterEncoder協議用來把任何遵循Encodable協議的參數編碼添加到URLRequest當中,在默認實現URLEncodedFormParameterEncoder類中,編碼參數用的就是這個自定義實現的URLEncodedFormEncoder編碼器,用來把Encodable協議的參數編碼爲url query string,參數類型能夠是基本數據類型(Int,Double等)也能夠是其餘高級類型(Data,Date,Decimal等),也能夠是實現了Encodable協議的自定義類型。github

URLEncodedFormEncoder--僞裝是編碼器,實際上只是用來定義類型的容器類

該類被修飾爲final,不容許繼承,只容許使用初始化參數控制編碼邏輯,其實URLEncodedFormEncoder類自己並無實現編碼方法,只是定義了N多編碼時行爲的定義,真正用來編碼的是內部類 _URLEncodedFormEncoder ,全部對參數的編碼處理都在該內部類中完成,編碼後的數據保存在URLEncodedFormComponent中,傳遞給上層URLEncodedFormParameterEncoder時,使用URLEncodedFormSerializer將編碼後的數據序列化爲url query string。算法

部分數據類型的編碼格式

URLEncodedFormEncoder定義了4中數據類型的編碼格式,編碼時可自由選擇:swift

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數組)api

/// 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的字符串表現形式進行了定義:markdown

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類型的app

/// 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)
    }()
}
複製代碼
相關文章
相關標籤/搜索