目前公司的Swift項目網絡請求使用的是第三方開源庫Alamofire,在使用的過程當中有遇到過2種參數格式沒法正確傳遞到後端的狀況;git
1)參數包含空數組github
直接會被過濾刪除掉json
2)參數包含二維數組swift
會把二維數組轉換爲一維數組後端
下面將結合Alamofire參數編碼部分的源碼來一步一步的分析爲啥不知足這2種參數格式。數組
這裏有必要先說下測試接口使用 httpbin.org來進行測試的好處;由於它在被調用後能夠返回服務端所接收到的全部參數;在咱們這裏僅僅調試參數,因此比使用抓包工具要更方便一些,能夠直接查看print的結果。緩存
// 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,空數組被過濾掉了,以及二維數組變成了一維數組
// GET請求抓包
// POST請求抓包
能夠看到請求的參數以及接口返回的參數和上面print的結果是同樣的:空數組nullArr不見了,二維數組twoDArr被轉換成了一維數組。這和咱們定義參數格式初衷不同了,須要和後臺的同事從新溝通定義參數格式。
宿主工程目前是經過Pod管理第三方依賴庫,而且Xcode12已經支持在同一個workspace下不一樣project之間進行斷點調試。
前面的func調用不用看,咱們直接看參數處理的部分:
// 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
}
複製代碼
因爲是一個空數組,裏面沒有任何參數,因此在方法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緩存纔會加;這裏說得有點偏了,若是感興趣能夠去仔細看看這個方法的具體實現。
因爲是二維,因此在執行方法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發起網絡請求:
在github上面,Alamofire/issues也有人遇到過相似本文參數的問題,參考官方評論使用Encodable
類型而不用 Parameters
字典,通過驗證仍是存在這種參數格式問題,具體能夠參看源碼ParameterEncoder.swift去進行調試。