Alamofire源码解析专栏文章地址
Alamofire是Swift最流行的网络请求框架,其简易的使用方法和强大的可定制能力使得框架本身备受iOS开发人员的喜爱。我在使用这个框架的时候经常会产生这样的好奇:这个框架到底是通过什么样的方式去管理如此庞大的网络请求系统的?又是如何给每一个请求提供了高定制化的接口的?于是这样的好奇心驱使我给自己开了个新坑,没错,就是这个Alamofire框架源码阅读专栏,不知道自己是否能够坚持下来把所有东西都写完,但是我会尽量努力。本篇是Alamlfire源码阅读专栏的开篇,主要介绍了框架程序的入口,Session类中的成员属性以及一些进一步阅读源码之前需要了解的小组件
程序入口 程序的入口存在于Alamofire.swift文件之中,该文件中只有简简单单的一行代码,将Session类的一个default单例赋值给了AF这个常量变量。
1 public let AF = Session .default
经常使用Alamofire的朋友们看到这一行代码可能就会感觉到自己已经到达了Alamofire宇宙的大门前了。我们在平常使用的过程中,经常使用AF.request这样的代码去发起一个请求,实际上是在调用Session类实例的方法,Session是Alamofire中的一个核心入口类,该类定义在Session.swift文件中。Session这个类在Alamofire中主要负责创建/发起请求、管理请求、响应URLSession的代理方法、管理URLSession、响应/发起Alamofire系统中的各种通知等,从它的职责就可以看到这个类是Alamofire中的核心类,事实也确实是这样,Session类管理的所有请求的整个生命周期,所以我们阅读源码也要围绕这个类去展开,但是不用着急地去死磕这个类的源码,因为Session的源码中包含了许多与外围组件交互的代码,直接去死磕这样一个类是很难理解的。跟随我的脚步,我会一步步带你逐渐深入Alamofire
Session类中的实例变量 这里我们主要先简要介绍一下Session类中的实例变量,只是为了对其有个印象,有些实例变量你可能看不懂是什么意思或者什么作用,不要去纠结,看不懂的直接跳过,只需要对这些属性有个大概的印象,之后用到了这些属性再回来看,你就能很清楚每个属性的作用了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public static let `default ` = Session ()public let session: URLSession public let delegate: SessionDelegate public let rootQueue: DispatchQueue public let startRequestsImmediately: Bool public let requestQueue: DispatchQueue public let serializationQueue: DispatchQueue public let interceptor: RequestInterceptor? public let serverTrustManager: ServerTrustManager? public let redirectHandler: RedirectHandler? public let cachedResponseHandler: CachedResponseHandler? public let eventMonitor: CompositeEventMonitor public let defaultEventMonitors: [EventMonitor ] = [AlamofireNotifications ()]var requestTaskMap = RequestTaskMap ()var activeRequests: Set <Request > = []var waitingCompletions: [URLSessionTask : () -> Void ] = [:]
Session类中的入口方法 前面说到我们使用AF.request()这样的方式来发起一个请求,这样做实际上是调用的Session的request实例方法,接下来我们就来看一下这个实例方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 open func request (_ convertible: URLConvertible, method: HTTPMethod = .get , parameters: Parameters? = nil , encoding: ParameterEncoding = URLEncoding.default , headers: HTTPHeaders? = nil , interceptor: RequestInterceptor? = nil , requestModifier: RequestModifier? = nil ) -> DataRequest { let convertible = RequestConvertible (url: convertible, method: method, parameters: parameters, encoding: encoding, headers: headers, requestModifier: requestModifier) return request(convertible, interceptor: interceptor) }
可以看到这个方法是非常的简单,但是使用到了许多我们不知道的类型,接下来主要介绍这个方法中出现的一些类型以及这些类型的作用,这也是这篇文章主要分析的地方
URLConvertible URLConvertible是一个协议类型,其定义了一类可以通过asURL方法转换为系统原生URL的对象,协议定义如下
1 2 3 public protocol URLConvertible { func asURL () throws -> URL }
除了定义了这个协议,Alamofire中还对系统原生类做了URLConvertible的扩展。比如:Alamofire将原生的String扩展为URLConvertible类型,这样我们就可以直接使用URL字符串来发起请求,使得从外部调用方法发起一个请求变得十分的简便
1 2 3 4 5 6 7 8 9 10 extension String : URLConvertible { public func asURL () throws -> URL { guard let url = URL (string: self ) else { throw AFError .invalidURL(url: self ) } return url } } extension URL : URLConvertible { public func asURL () throws -> URL { self } }
这种Convertible的模式是一种值得学习的思想,它可以将一个类型与几个其他相关的类型以一种非常优雅的方式关联起来,即几个其他相关类型可以非常便捷的转换为一个目标类型。Alamofire中还有许多运用到了这种思想的地方,等我们遇到的时候再说
HTTPMethod 这个类型很好理解,就像名字那样,HTTPMethod是用来表示HTTP请求的方法的类型,其本身是一个结构体,只是通过多个静态变量,将请求方法字符串与各个变量对应起来,这样在使用的时候就不容易出错。至于为什么使用了结构体而不是用枚举,我想大概是因为使用结构体更易定制化,如果这里有使用一些比较不常见的方法的需求时,可以直接重新创建一个结构体的实例来表示这个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public struct HTTPMethod : RawRepresentable , Equatable , Hashable { public static let connect = HTTPMethod (rawValue: "CONNECT" ) public static let delete = HTTPMethod (rawValue: "DELETE" ) public static let get = HTTPMethod (rawValue: "GET" ) public static let head = HTTPMethod (rawValue: "HEAD" ) public static let options = HTTPMethod (rawValue: "OPTIONS" ) public static let patch = HTTPMethod (rawValue: "PATCH" ) public static let post = HTTPMethod (rawValue: "POST" ) public static let put = HTTPMethod (rawValue: "PUT" ) public static let trace = HTTPMethod (rawValue: "TRACE" ) public let rawValue: String public init (rawValue: String ) { self .rawValue = rawValue } }
parameters 这个非常简单,就是一个String-Any类型的字典,用来接收用户这次请求想要传递的参数
1 public typealias Parameters = [String : Any ]
ParameterEncoding ParameterEncoding是一个协议类型,该协议中只定义了一个encode方法,该方法实际的作用是将给定的参数parameters按照某种特定的策略编码,并将编码后的数据放到request合适的位置(比如get请求放到URL中,post请求经常需要将数据放到body中)
1 2 3 public protocol ParameterEncoding { func encode (_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest }
不过编码这件事情听起来简单,但其实需要处理很多细节,所以Alamofire这样完备的框架一定不会只提供一个协议让你自己去实现,相反,你在日常开发中几乎不需要自定义ParameterEncoding的协议实现,Alamofire系统中提供的两个遵循协议的结构体基本可以满足我们日常开发中的需求。这两个结构体分别是URLEncoding与JSONEncoding,我们先来分析URLEncoding的源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 public struct URLEncoding : ParameterEncoding { public enum Destination { case methodDependent case queryString case httpBody func encodesParametersInURL (for method: HTTPMethod) -> Bool { switch self { case .methodDependent: return [.get , .head, .delete].contains (method) case .queryString: return true case .httpBody: return false } } } public enum ArrayEncoding { case brackets case noBrackets func encode (key: String) -> String { switch self { case .brackets: return "\(key)[]" case .noBrackets: return key } } } 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" } } } }
接下来我们来看URLEncoding结构体的具体实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 public struct URLEncoding : ParameterEncoding { public static var `default `: URLEncoding { URLEncoding () } public static var queryString: URLEncoding { URLEncoding (destination: .queryString) } public static var httpBody: URLEncoding { URLEncoding (destination: .httpBody) } public let destination: Destination public let arrayEncoding: ArrayEncoding public let boolEncoding: BoolEncoding public init (destination: Destination = .methodDependent, arrayEncoding: ArrayEncoding = .brackets, boolEncoding: BoolEncoding = .numeric) { self .destination = destination self .arrayEncoding = arrayEncoding self .boolEncoding = boolEncoding } public func encode (_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest { var urlRequest = try urlRequest.asURLRequest() guard let parameters = parameters else { return urlRequest } 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 } }
先拿到urlRequest与parameters对象
让destination策略来决定编码后的数据应该被放入url中还是httpBody中
如果应该被放入url中,那么就调用帮助函数query,将参数编码为queryString的格式,最后与原URL进行拼接,最后再将拼接好的url设置进request。注意urlComponents.percentEncodedQuery是在拿URL中原本就拥有的queryString,如果原本的url中存在一些queryString,那么就会将旧的和新的一起拼接起来
如果数据是应该被放入httpBody中的,那么就会将request的Content-Type的Head设置为application/x-www-form-urlencoded,并且使用帮助函数query将参数转为queryString之后将字符串转为data,并放入http的body中
接下来让我们来看一下上面的encode方法中所用到的工具函数吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 private func query (_ parameters: [String: Any ]) -> String { var components: [(String , String )] = [] for key in parameters.keys.sorted(by: <) { let value = parameters[key]! components += queryComponents(fromKey: key, value: value) } return components.map { "\($0)=\($1)" }.joined(separator: "&" ) } public func queryComponents (fromKey key: String, value: Any ) -> [(String , String )] { var components: [(String , String )] = [] if let dictionary = value as ? [String : Any ] { for (nestedKey, value) in dictionary { components += queryComponents(fromKey: "\(key)[\(nestedKey)]" , value: value) } } else if let array = value as ? [Any ] { for value in array { components += queryComponents(fromKey: arrayEncoding.encode(key: key), value: value) } } else if let value = value as ? NSNumber { if value.isBool { components.append((escape(key), escape(boolEncoding.encode(value: value.boolValue)))) } else { components.append((escape(key), escape("\(value)" ))) } } else if let bool = value as ? Bool { components.append((escape(key), escape(boolEncoding.encode(value: bool)))) } else { components.append((escape(key), escape("\(value)" ))) } return components }
queryComponents这个方法参数字典中的一个键值对,而这个键值对的值的类型是Any的,也就是可以是任何类型,这个方法的目的就是针对不同的类型做出不同的转化,最终将一个键值对转换为一个或多个纯字符串键值对。
如果传进来的值是一个字典,则在编码是会生成 [(key[nestedKey1],val1),(key[nestedKey2], val2)...]
,并且还会对其中的value继续进行递归
如果传进来的值是一个数组,则会根据数组数据的编码策略生成 [(a[],1),(a[],2)...]
或者 [(a,1), (a,2),...]
这样的键值数据。
如果传进来的值是一个布尔值,则按照布尔类型的编码策略,将布尔值转化为”true”/“1”和”false”/“0”这样的字符串
如果这些类型都不是,则直接将Any类型的对象转换为一个字符串来显示
上述就是关于URLEncoding的所有内容,下面我们再来看一个开发中也经常会被用到的编码器:JSONEncoding
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public struct JSONEncoding : ParameterEncoding { public static var `default `: JSONEncoding { JSONEncoding () } public static var prettyPrinted: JSONEncoding { JSONEncoding (options: .prettyPrinted) } public let options: JSONSerialization .WritingOptions public init (options: JSONSerialization .WritingOptions = []) { self .options = options } public func encode (_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest { var urlRequest = try urlRequest.asURLRequest() guard let parameters = parameters else { return urlRequest } do { let data = try JSONSerialization .data(withJSONObject: parameters, options: options) if urlRequest.value(forHTTPHeaderField: "Content-Type" ) == nil { urlRequest.setValue("application/json" , forHTTPHeaderField: "Content-Type" ) } urlRequest.httpBody = data } catch { throw AFError .parameterEncodingFailed(reason: .jsonEncodingFailed(error: error)) } return urlRequest } }
JSONEncoding的encode方法非常简单:首先获取到参数和request,然后直接使用JSONSerialization将参数字典转换为jsonData,然后将该data放进httpData中,最后设置请求的Content-Type Header为”application/json”,仅仅是这样就完成了jsonEncoding的任务。
该类型很简单,只是用来存储此次请求需要设置的HTTPHeader列表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public struct HTTPHeaders { private var headers: [HTTPHeader ] public init () {} public init (_ headers: [HTTPHeader ]) { self .init () headers.forEach { update($0 ) } } public init (_ dictionary: [String : String ]) { self .init () dictionary.forEach { update(HTTPHeader (name: $0 .key, value: $0 .value)) } } public mutating func add (name: String, value: String) { update(HTTPHeader (name: name, value: value)) }. public mutating func add (_ header: HTTPHeader) { update(header) } public mutating func update (name: String, value: String) { update(HTTPHeader (name: name, value: value)) } public mutating func update (_ header: HTTPHeader) { guard let index = headers.index(of: header.name) else { headers.append(header) return } headers.replaceSubrange(index...index, with: [header]) } public mutating func remove (name: String) { guard let index = headers.index(of: name) else { return } headers.remove(at: index) } }
RequestInterceptor RequestInterceptor是Alamofire中定义的继承自两个其他协议的空协议,这两个协议分别是RequestAdapter和RequestRetrier。我们可以通过RequestInterceptor来实现很强大的请求自适应和失败重试功能,比如网络请求发起前的账户检查,添加通用的header以及网络请求失败后使用自定义的重传策略。Session与Request中都有一个属性用来存储RequestInterceptor拦截器,Session中的拦截器可以作用于Session发起的所有Request之上,而Request中的拦截器只能作用到自身Request之上。
1 2 3 4 5 6 7 8 9 public protocol RequestInterceptor : RequestAdapter , RequestRetrier {}public protocol RequestAdapter { func adapt (_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void ) } public protocol RequestRetrier { func retry (_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void ) }
RequestAdapter 协议允许在通过网络发出之前检查和修改Session执行的每个URLRequest,一个比较常见的用法是,在某些网络请求发出之前首先检查本地存储的身份验证信息,如果正常则附加到authorization请求头中,如果不正常则放弃请求。RequestAdapter协议中只规定了一个方法adapt,该方法会在每个Session执行每个URLRequest之前执行,方法的第三个参数,是一个回调函数,你在完成判断之后必须调用该回调函数,并传入你判断的结果以继续请求流程,如果允许执行就传递.success,如果不允许继续执行就传递.fail(someError),someError是说明此次不允许继续执行的错误原因
RequestRetrier 协议允许重试在执行时遇到错误的请求。协议中只规定了一个方法retry,该方法的最后一个参数同样也是一个回调,你在判断这个请求是否可以进行重试之后,需要将结果以回调函数参数的方式传递给Alamofire系统以继续重试流程,Alamofire中还定义了一个用来表示判断结果的枚举RetryResult
1 2 3 4 5 6 7 8 9 10 public enum RetryResult { case retry case retryWithDelay(TimeInterval ) case doNotRetry case doNotRetryWithError(Error ) }
另外,系统给RequestInterceptor协议提供了方法默认空实现,这样可以使得在Adapter和Retyier这两个功能只需要其中一种功能的情况下编写起来更加方便,只需要覆写需要的方法就可以了
1 2 3 4 5 6 7 8 9 10 11 12 extension RequestInterceptor { public func adapt (_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void ) { completion(.success(urlRequest)) } public func retry (_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void ) { completion(.doNotRetry) } }
Alamofire中还为我们提供了一个实现RequestInterceptor协议的RetryPolicy类,这个类可以快速地帮助我们实现网络错误重试的需求
在RetryPolicy类定义了一系列的默认值,我们可以选择直接忽略这个部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 open class RetryPolicy : RequestInterceptor { public static let defaultRetryLimit: UInt = 2 public static let defaultExponentialBackoffBase: UInt = 2 public static let defaultExponentialBackoffScale: Double = 0.5 public static let defaultRetryableHTTPMethods: Set <HT public static let defaultRetryableHTTPStatusCodes: Set <Int > = [408 , 500 , 502 , 503 , 504 ] public static let defaultRetryableURLErrorCodes: Set <URLError .Code > = [ .backgroundSessionInUseByAnotherProcess, .backgroundSessionWasDisconnected, .badServerResponse, .callIsActive, .cannotConnectToHost, .cannotFindHost, .cannotLoadFromNetwork, .dataNotAllowed, .dnsLookupFailed, .downloadDecodingFailedMidStream, .downloadDecodingFailedToComplete, .internationalRoamingOff, .networkConnectionLost, .notConnectedToInternet, .secureConnectionFailed, .serverCertificateHasBadDate, .serverCertificateNotYetValid, .timedOut ] }
接下来是RetryPolicy类的核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 open class RetryPolicy : RequestInterceptor { public let retryLimit: UInt public let exponentialBackoffBase: UInt public let exponentialBackoffScale: Double public let retryableHTTPMethods: Set <HTTPMethod > public let retryableHTTPStatusCodes: Set <Int > public let retryableURLErrorCodes: Set <URLError .Code > public init (retryLimit: UInt = RetryPolicy .defaultRetryLimit, exponentialBackoffBase: UInt = RetryPolicy .defaultExponentialBackoffBase, exponentialBackoffScale: Double = RetryPolicy .defaultExponentialBackoffScale, retryableHTTPMethods: Set <HTTPMethod > = RetryPolicy .defaultRetryableHTTPMethods, retryableHTTPStatusCodes: Set <Int > = RetryPolicy .defaultRetryableHTTPStatusCodes, retryableURLErrorCodes: Set <URLError .Code > = RetryPolicy .defaultRetryableURLErrorCodes) { precondition (exponentialBackoffBase >= 2 , "The `exponentialBackoffBase` must be a minimum of 2." ) self .retryLimit = retryLimit self .exponentialBackoffBase = exponentialBackoffBase self .exponentialBackoffScale = exponentialBackoffScale self .retryableHTTPMethods = retryableHTTPMethods self .retryableHTTPStatusCodes = retryableHTTPStatusCodes self .retryableURLErrorCodes = retryableURLErrorCodes } open func retry (_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void ) { if request.retryCount < retryLimit, let httpMethod = request.request?.method, retryableHTTPMethods.contains (httpMethod), shouldRetry(response: request.response, error: error) { let timeDelay = pow(Double (exponentialBackoffBase), Double (request.retryCount)) * exponentialBackoffScale completion(.retryWithDelay(timeDelay)) } else { completion(.doNotRetry) } } private func shouldRetry (response: HTTPURLResponse?, error: Error) -> Bool { if let statusCode = response?.statusCode, retryableHTTPStatusCodes.contains (statusCode) { return true } else { let errorCode = (error as ? URLError )?.code let afErrorCode = (error.asAFError?.underlyingError as ? URLError )?.code if let code = errorCode ?? afErrorCode, retryableURLErrorCodes.contains (code) { return true } else { return false } } } }
首先会进行一系列的条件判断,判断当前发生错误的request是否满足可以进行重试的条件。具体的条件包括:状态码是否在可重试状态码集合中,请求方法是否在可重试请求方法集合中,发生的错误Error是否在可重试错误集合中。如果判断通过,则开始计算重试的延时事件,如果判断不通过,则不允许重试
系统提供的RetryPolicy使用延时重试的策略,基于提供的相关系数以及重试的次数计算出延时的时间,是关于重试次数的指数函数,重试次数越多,延时越久
RequestModifier RequestModifier的定义很简单,其定义的目的就是为了增强可扩展性,允许用户对request做出更加细化的修改和定制
typealias RequestModifier = (inout URLRequest) throws -> Void
系统会在创建了URLRequest之后,对其调用resume之前,调用你传入的RequestModifier闭包。
本小节所要分析的源码已经全部展示完毕了,未完待续……