Alamofire從源碼淺析2種不符合實際需求的參數格式總結

目前公司的Swift項目網絡請求使用的是第三方開源庫Alamofire,在使用的過程當中有遇到過2種參數格式沒法正確傳遞到後端的狀況;git

1)參數包含空數組github

直接會被過濾刪除掉json

2)參數包含二維數組swift

會把二維數組轉換爲一維數組後端

下面將結合Alamofire參數編碼部分的源碼來一步一步的分析爲啥不知足這2種參數格式。數組

一、首先看下實際結果

這裏有必要先說下測試接口使用 httpbin.org來進行測試的好處;由於它在被調用後能夠返回服務端所接收到的全部參數;在咱們這裏僅僅調試參數,因此比使用抓包工具要更方便一些,能夠直接查看print的結果。緩存

1)發起一個網絡請求

// GET請求markdown

AF.request("http://httpbin.org/get", method: .get, parameters: ["name": "kang", "score": 90, "nulllArr": [], "twoDArr": [["1","2"],["3","4"]]]).responseJSON { (response) in
    switch response.result {
    case .success(let value):
        print(value)
    case .failure(let error):
        print(error)
    }
}
複製代碼

輸出結果:網絡

{
    args =     {
        name = kang;
        score = 90;
        "twoDArr[][]" =         (
            1,
            2,
            3,
            4
        );
    };
    headers =     {
        Accept = "*/*";
        "Accept-Encoding" = "br;q=1.0, gzip;q=0.9, deflate;q=0.8";
        "Accept-Language" = "en;q=1.0";
        Host = "httpbin.org";
        "User-Agent" = "Test/1.0 ( build:1; iOS 14.0.0) Alamofire/5.2.2";
        "X-Amzn-Trace-Id" = "Root=1-5f8d57cd-039301d94fe8ad1d6311287c";
    };
    origin = "";
    url = "http://httpbin.org/get?name=kang&score=90&twoDArr[][]=1&twoDArr[][]=2&twoDArr[][]=3&twoDArr[][]=4";
}
複製代碼

// POST請求app

AF.request("http://httpbin.org/post", method: .post, parameters: ["name": "kang", "score": 90, "nulllArr": [], "twoDArr": [["1","2"],["3","4"]]]).responseJSON { (response) in
    switch response.result {
    case .success(let value):
        print(value)
    case .failure(let error):
        print(error)
    }
}
複製代碼

輸出結果:

{
    args =     {
    };
    data = "";
    files =     {
    };
    form =     {
        name = kang;
        score = 90;
        "twoDArr[][]" =         (
            1,
            2,
            3,
            4
        );
    };
    headers =     {
        Accept = "*/*";
        "Accept-Encoding" = "br;q=1.0, gzip;q=0.9, deflate;q=0.8";
        "Accept-Language" = "en;q=1.0";
        "Content-Length" = 106;
        "Content-Type" = "application/x-www-form-urlencoded; charset=utf-8";
        Host = "httpbin.org";
        "User-Agent" = "Test/1.0 (; build:1; iOS 14.0.0) Alamofire/5.2.2";
        "X-Amzn-Trace-Id" = "Root=1-5f8e5cd1-5aa70b642cee70930c0eedd7";
    };
    json = "<null>";
    origin = "";
    url = "https://httpbin.org/post";
}
複製代碼

能夠看到不管是GET仍是POST,空數組被過濾掉了,以及二維數組變成了一維數組

2)經過抓包工具Charles驗證

// GET請求抓包

// POST請求抓包

能夠看到請求的參數以及接口返回的參數和上面print的結果是同樣的:空數組nullArr不見了,二維數組twoDArr被轉換成了一維數組。這和咱們定義參數格式初衷不同了,須要和後臺的同事從新溝通定義參數格式。

二、源碼調試分析

宿主工程目前是經過Pod管理第三方依賴庫,而且Xcode12已經支持在同一個workspace下不一樣project之間進行斷點調試。

前面的func調用不用看,咱們直接看參數處理的部分:

ParameterEncoding.swift -> URLEncoding
// MARK: Encoding

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

    guard let parameters = parameters else { return urlRequest }
    
    // 根據請求方法不一樣判斷處理參數是放在url後面仍是body
    if let method = urlRequest.method, destination.encodesParametersInURL(for: method) {
        guard let url = urlRequest.url else {
            throw AFError.parameterEncodingFailed(reason: .missingURL)
        }

        if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), !parameters.isEmpty {
            let percentEncodedQuery = (urlComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(parameters)
            urlComponents.percentEncodedQuery = percentEncodedQuery
            urlRequest.url = urlComponents.url
        }
    } else {
        if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
            urlRequest.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type")
        }

        urlRequest.httpBody = Data(query(parameters).utf8)
    }

    return urlRequest
}
複製代碼

從上面源碼能夠看到,不管哪一種請求方式都有調用到 query(parameters) 處理參數

處理參數的核心代碼就是如下3個方法

private func query(_ parameters: [String: Any]) -> String {
    // 建立一個數組,參數類型是2個字符串的元組類型,用於分別存儲key、value
    var components: [(String, String)] = []

    // 將參數key按照字母正序的方式排序,而後遍歷每一個key
    for key in parameters.keys.sorted(by: <) {
        let value = parameters[key]!
        // 根據value類型具體處理參數
        components += queryComponents(fromKey: key, value: value)
    }
    // 按照key1=value1&key2=value2的格式拼接返回參數字符串
    return components.map { "\($0)=\($1)" }.joined(separator: "&")
}
複製代碼
// 遞歸遍歷參數的每一層,將其展開爲一維
public func queryComponents(fromKey key: String, value: Any) -> [(String, String)] {
    // 建立一個數組,參數類型是2個字符串的元組類型,用於分別存儲key、value
    var components: [(String, String)] = []
    // 根據參數字典中的value類型是字典、數組、整型、布爾分別處理
    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 {
            components += queryComponents(fromKey: arrayEncoding.encode(key: key), value: value)
        }
    case let number as NSNumber:
        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:
        components.append((escape(key), escape(boolEncoding.encode(value: bool))))
    default:
        components.append((escape(key), escape("\(value)")))
    }
    return components
}
複製代碼
// 對特殊字符進行URL編碼
public func escape(_ string: String) -> String {
    string.addingPercentEncoding(withAllowedCharacters: .afURLQueryAllowed) ?? string
}
複製代碼
1)對於咱們測試參數,空數組"nulllArr": []

因爲是一個空數組,裏面沒有任何參數,因此在方法queryComponents遍歷後就直接返回一個空數組var components: [(String, String)] = [] 給 components += queryComponents(fromKey: key, value: value),而這裏數組的拼接必須是有元素才能夠的,空數組是不會加過去的。

這裏咱們能夠看看數組Array定義的 += 方法:

@frozen public struct Array<Element> {
   @inlinable public static func += (lhs: inout [Element], rhs: [Element])
}
複製代碼

swift.org/ 查看Swift源碼stdlib--public--core--Array.swift能夠看到

extension Array {
  @inlinable
  public static func += (lhs: inout Array, rhs: Array) {
    lhs.append(contentsOf: rhs)
  }
}

@inlinable
  @_semantics("array.append_element")
  public mutating func append(_ newElement: __owned Element) {
    _makeUniqueAndReserveCapacityIfNotUnique()
    let oldCount = _getCount()
    _reserveCapacityAssumingUniqueBuffer(oldCount: oldCount)
    _appendElementAssumeUniqueAndCapacity(oldCount, newElement: newElement)
  }
複製代碼

當rhs右邊的數組有元素的時候數組buffer緩存纔會加;這裏說得有點偏了,若是感興趣能夠去仔細看看這個方法的具體實現。

2)對於二維數組:"twoDArr": [["1","2"],["3","4"]]]

因爲是二維,因此在執行方法queryComponents的時候會遞歸調用,將裏面的元素所有展開變成了一維數組,"twoDArr [] [] ": ["1","2","3","4"],對於key後面有2箇中括號,這個是能夠經過Alamofire下面代碼進行控制的,默認狀況下是帶中括號的

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

public enum ArrayEncoding {
    case brackets
    case noBrackets

    func encode(key: String) -> String {
        switch self {
        case .brackets:
            return "\(key)[]"
        case .noBrackets:
            return key
        }
    }
}
複製代碼

因爲咱們這裏是二維數組,因此方法queryComponents遞歸調用了2次,因此有2箇中括號,也間接說明參數是一個二維數組參數。

總結

從實際參數請求結果到一步一步分析第三方庫源碼,咱們找到了致使這個結果的具體緣由。Alamofire參數處理有字典、數組、整型、布爾等,在本文中只是對空數組、二維數組進行了驗證,其它類型原理是同樣的,能夠得出使用Alamofire發起網絡請求:

1)參數中若是有空字典、空數組都會被過濾掉;
2)若是有字典、數組嵌套的狀況,都會被一層層展開成一維;
3)參數key是按照字母正序進行排序的,若是某些接口對參數的順序也有要求能夠在key以前加入正序的字母。

在github上面,Alamofire/issues也有人遇到過相似本文參數的問題,參考官方評論使用Encodable類型而不用 Parameters 字典,通過驗證仍是存在這種參數格式問題,具體能夠參看源碼ParameterEncoder.swift去進行調試。

相關文章
相關標籤/搜索