Alamofire-从一个简单的请求深入源代码(1)
今天我们从一个简单的例子中, 来一步一步剖析 Alamofire 的源代码.
简单的例子
例子很简单, 就是请求一个api, 然后打印输出结果而已.
Alamofire.request("https://httpbin.org/get").responseString { (response) in
if let string = response.result.value {
print(string)
}
}
Alamofire.request
你如果自己写这段代码就知道, 你在写Alamofire
的时候, 是没有代码提示的, 这并不是一个 bug, 因为 Alamofire
并不是一个类型, 而是一个模块名, 在这里只是起到一个类似命名空间的作用. 所以你大可将例子改为request(xxx)...
request 函数签名
request
函数其实并不只有一个参数, 我们这里其他的参数都是使用的默认值而已.
完成的函数签名
func request(
_ url: URLConvertible,
method: HTTPMethod = .get,
parameters: Parameters? = nil,
encoding: ParameterEncoding = URLEncoding.default,
headers: HTTPHeaders? = nil)
-> DataRequest
这里一下出现了很多未知的类型, 我们慢慢来.
URLConvertible
我们在示例中明明是传入的一个字符串, 为何这里是一个URLConvertible
类型? 想要回答这个问题, 我们不妨看一下定义
public protocol URLConvertible {
func asURL() throws -> URL
}
URLConvertible
只是一个协议, 这个协议也只有一个方法. 方法的目的也很简单--想办法变成一个URL.
同时, String
类型也是实现了这个协议的(为了方便阅读, 稍微调整了一下代码缩进)
extension String: URLConvertible {
public func asURL() throws -> URL {
guard let url = URL(string: self) else {
throw AFError.invalidURL(url: self)
}
return url
}
}
几行代码, 一目了然, 无需多言, 错误处理也很到位.
与 String
类似, URL
, URLComponents
也是实现了这个协议的. 同时, 如果你愿意, 你也可以自己实现, 例如你想把一个枚举转换成 url, 这样请求 api, 就可以直接填写枚举, 岂不美哉?
HTTPMethod
这个就不难理解, 请求方法而已, 这里默认的方法是GET
, 除此之外, 还有"POST", "PUT" 等等. 如果你想对 http 请求方法有更深入了解, 可以查阅 HTTP 权威指南
Parameters
这个类型只是一个别名, 定义如下
public typealias Parameters = [String: Any]
你可以在这里填入你要请求的参数.
ParameterEncoding
参数的编码方式.
常用的编码方式为URLEncoding
, 这种编码方式, 会将参数编码为 param1=value1¶m2=value2
类似的结构.
初次之外, 还有编码为 JSON, PLIST 方式.
ParameterEncoding
本身是一个枚举, 定义如下
public protocol ParameterEncoding {
func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest
}
可以看出来, 目的很简单, 就是想要根据参数和请求(URLRequestConvertible
, 这个我们后面来讲, 这里你就认为他是一个URLRequest
吧)生成一个含有参数的 URLRequest
上面说到, Alamofire 为我们已经预定义了三种编码方式, 我们先来看 URLEncoding
.
URLEncoding
编码的最终格式如上面所示, 而编码后的数据放在那里, 则有两种情况, 分别是直接放在 URL 上和放在请求体中( http body).
而具体放在那里, Alamofire 给我们两种方案, 一种是自动根据请求方法选择, 会根据 HTTP 请求方法来确定, 例如针对 GET 请求, 通常是直接放在 URL 中.第二种是手动制定
为此, Alamofire 在 URLEncoding
类型内部, 定义了一个结构体, 同时, 定了了几个便捷属性, 方便外部使用.
public struct URLEncoding: ParameterEncoding {
public enum Destination {
case methodDependent, queryString, httpBody
}
/// 依赖方法
public static var `default`: URLEncoding { return URLEncoding() }
/// 与 default 相同
public static var methodDependent: URLEncoding { return URLEncoding() }
/// 编码时, 参数放到 url 中
public static var queryString: URLEncoding { return URLEncoding(destination: .queryString) }
/// 编码时, 参数放到请求体中
public static var httpBody: URLEncoding { return URLEncoding(destination: .httpBody) }
///编码后的参数位置
public let destination: Destination
/// 构造函数
public init(destination: Destination = .methodDependent) {
self.destination = destination
}
....
}
接下来我们来看看核心的编码功能
public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
// 获取 request
var urlRequest = try urlRequest.asURLRequest()
// 获取参数, 如果没有参数, 那么直接返回
guard let parameters = parameters else { return urlRequest }
// 获取请求方法, 同时, 根据请求方法来判断是否需要编码参数到 url 中
if let method = HTTPMethod(rawValue: urlRequest.httpMethod ?? "GET"), encodesParametersInURL(with: method) {
// 直接编码到 url 中
// 获取 url
guard let url = urlRequest.url else {
throw AFError.parameterEncodingFailed(reason: .missingURL)
}
// 构建一个URLComponents 对象, 并在其中添加参数
if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) {
// 此处 map 是 optional 的map, 如果 optionvalue 不会空的时候, 会调用 map 内的闭包
// 如果 url 中本来就有一部分参数了, 那么就将新的参数附加在后面
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")
}
// 编码到 body 中
urlRequest.httpBody = query(parameters).data(using: .utf8, allowLossyConversion: false)
}
return urlRequest
}
注释我都写在代码中里面了.
其中这里面有用到几个函数, 一个是encodesParametersInURL
这个函数是来判断是否要编码参数到URL 中
实现如下
private func encodesParametersInURL(with method: HTTPMethod) -> Bool {
/// 如果在编码位置中明确指出了, 那么就根据这个来确定是否要编码在 url 中
switch destination {
case .queryString:
return true
case .httpBody:
return false
default:
break
}
/// 如果是依赖方法, 那么就根据请求的方法来决定
switch method {
case .get, .head, .delete:
return true
default:
return false
}
}
还有一个函数, query
, 这个用于将参数编码. 这个很细节, 可以后面有时间单独讲讲. 这里就略过.
URLEncoding
就这些了, JSONEncoding
, PropertyListEncoding
和这个类似, 只不过编码方式不同而已, 需要注意的是, 后面这两种, 都是直接编码到 HTTPBody
中的. 如果你想把 JSON 编码到 URL 中, 你就只有自己写一个, 你可以依葫芦画瓢, 也是很容易的.
接下来我们继续回到 Alamofire.request
函数中
HTTPHeaders
这个也是跟 Parameters 参数一样的, 一个别名而已
public typealias HTTPHeaders = [String: String]
你可以用这个参数, 在请求头中添加一些自定义的请求头.
讲到这里, 我们才把 Alamofire.request
的参数列表理了一遍, 接下来, 我们来看看函数体
另外, 我再 alamofire 中添加了一些中文注释, 愿意的话, 你也可看看https://github.com/ywwzwb/alamofire-chinese