IOS源码解析:Alamofire 5 核心

原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 一、抓包工具
    • 1、Charles 抓包工具
    • 2、Fiddler Everywhere 抓包工具
  • 二、Alamofire 架构
    • 1、接口
    • 2、请求
    • 3、响应
    • 4、底层
    • 5、其他
  • 三、核心流程
  • 四、功能点解析
    • 1、AF
    • 2、Request
    • 3、HTTPMethod
    • 4、ParameterEncoding
    • 5、Validation
    • 6、DispatchQueue
    • 7、AFError
    • 8、DataResponse
  • Demo
  • 参考文献

一、抓包工具

1、Charles 抓包工具

a、配置各种证书比较麻烦

b、Charles抓包工具总是会疯狂报错(这个问题可以重装Charles抓包工具解决掉)

c、而且还存在抓取的内容乱码显示问题

这个问题解决起来比较麻烦,我差不多都快放弃使用这个工具了,突然却被我解决掉这个问题了,在这里画个圈圈诅咒软件的设计者,你这么复杂的配置让人怎么玩你?

在电脑端安装并信任证书

Charles中,设置ssl proxy Setting中的ssl proxying的代理网址,按图中填写即可,这一步非常重要,我就是设置了这一步后才没有乱码的

设置抓包的网址和端口,设置为全部都抓

大功告成,终于抓取到正常的内容了,留下了喜极而泣的泪水,没忍住想把创作者拖出去打的冲动。


2、Fiddler Everywhere 抓包工具

推荐使用简单明了的Fiddler Everywhere抓包工具,这是它的下载地址 Fiddler Everywhere,免费版本的就可以用得很顺溜了。

界面整体看起来也比较干净简洁,而且安装好以后就可以直接使用,不用处理一大堆的配置问题。

和其它抓包工具一样,Fiddler Everywhere默认也是只能抓取HTTP请求,需要通过下载证书或进行相关配置,才能正常拦截HTTPS请求,配置如下,信任证书并勾选捕捉。


二、Alamofire 架构

1、接口

Alamofire.swift:api 声明
// 全局静态变量
public let AF = Session.default
// 当前Alamofire版本
let version = "5.4.1"

2、请求

Request.swift:请求类,用于构建请求
public protocol RequestDelegate: AnyObject

public class Request
public class DataRequest: Request
public final class DataStreamRequest: Request
public class DownloadRequest: Request
public class UploadRequest: DataRequest
ParameterEncoding.swift:参数编码
public protocol ParameterEncoder

open class JSONParameterEncoder: ParameterEncoder
open class URLEncodedFormParameterEncoder: ParameterEncoder
MultipartFormData.swift:自定义表单类
open class MultipartFormData 
ServerTrustEvaluation.swift:服务器验证
public protocol ServerTrustEvaluating

open class ServerTrustManager 
public final class DefaultTrustEvaluator: ServerTrustEvaluating
public final class RevocationTrustEvaluator: ServerTrustEvaluating
public final class PinnedCertificatesTrustEvaluator: ServerTrustEvaluating
public final class PublicKeysTrustEvaluator: ServerTrustEvaluating
public final class CompositeTrustEvaluator: ServerTrustEvaluating
public final class DisabledTrustEvaluator: ServerTrustEvaluating

3、响应

Response.swift:响应类,用于构建响应
public struct DataResponse<Success, Failure: Error> 
public struct DownloadResponse<Success, Failure: Error>
Validation.swift:响应数据验证
extension Request
extension DataRequest
extension DataStreamRequest
extension DownloadRequest
Result+Alamofire.swift:请求结果表示
extension Result 
AFError.swift:错误类型
public enum AFError: Error
public enum MultipartEncodingFailureReason
public enum ParameterEncodingFailureReason
public enum ParameterEncoderFailureReason
public enum ResponseValidationFailureReason
public enum ResponseSerializationFailureReason
public enum ServerTrustFailureReason
public enum URLRequestValidationFailureReason

4、底层

Session.swift:请求session的管理类,底层使用NSURLSession实现
open class Session
MARK: - DataRequest
MARK: - DataStreamRequest
MARK: - UploadRequest
SessionDelegate.swift:请求Session的代理对象,主要实现NSURLSession的代理方法以及回调闭包
open class SessionDelegate: NSObject 
extension SessionDelegate: URLSessionDelegate
extension SessionDelegate: URLSessionTaskDelegate
extension SessionDelegate: URLSessionDataDelegate
extension SessionDelegate: URLSessionDownloadDelegate
DispatchQueue+Alamofire.swift:GCD扩展,增加了一个名为after的延迟调用方法
extension DispatchQueue

5、其他

NetworkReachabilityManager.swift:网络状态监听类
open class NetworkReachabilityManager 
Notifications.swift:定义通知
extension Notification
extension NotificationCenter
public final class AlamofireNotifications: EventMonitor

三、核心流程

这里并不会贴上源码里的大段代码,而是通过一个例子来进入到Alamofire中的各个模块,让读者更好理解。

a、安全认证

info.plist中添加App Transport Security Settings,再将Allow Arbitrary Loads修改为YES后控制台输出结果为如下:


b、核心流程的范例
// 淘宝的一个搜索api
let url = "http://suggest.taobao.com/sug"
// 对袜子进行搜索
let parameters: [String: Any] = [
    "code" : "utf-8",
    "q" : "袜子"
]

AF.request(url, method: .get, parameters: parameters)
    .validate(statusCode: [200])
    .responseData(queue: DispatchQueue.global())
    { (responseData) in
        switch responseData.result
        {
        case .success(let data):
          guard let jsonString = String(data: data, encoding: .utf8) else { return }
          print("json字符串:\(jsonString)")
        case .failure(let error):
          print("错误信息:\(error)")
        }
    }

c、范例中的功能点解析
  • 通过Alamofirerequest开始调用
  • 传入url
  • 使用get方法
  • 传入parameters,并在encoding中定义了参数的编码方式
  • 这里并不需要headers
  • validate中传入需要验证的statusCode的数组
  • responseData方法里传入全局队列和回调的处理闭包completionHandler
  • 在闭包里对返回的数据进行判断,若返回成功,打印jsonString,若失败则打印错误

d、输出结果
json字符串:
{"result":[["袜子女","988016.6933754483"],["袜子男","865386.2105082254"],["袜子女冬","662313.8682009963"],["袜子女中筒袜","390508.191495028"],["袜子男长袜","650152.593118986"],["袜子男冬","747552.2895411798"],["袜子女ins潮","286597.0935191857"],["袜子男纯棉","324485.1344208922"],["袜子女冬季 加绒","329439.1491313167"],["袜子男中筒","300330.61181601905"]]}

四、功能点解析

1、AF

a、全局静态变量
AF是最外层用于发起网络请求的静态变量,来自于全局静态变量Session.default
  • 降低使用门槛,将复杂的功能实现进行下沉
  • 提供单一接口来实现功能上的全面覆盖,没有多余代码,非常简洁
  • 方便编程者解读源码,探究SDK的封装思路
public let AF = Session.default
default是提供给全局使用的共享单例
open class Session
{
    public static let default = Session()
}

b、请求实际的调用者是URLSession
通过使用 URLSession 来初始化 Session
public init(session: URLSession,
            delegate: SessionDelegate,
            rootQueue: DispatchQueue,
            startRequestsImmediately: Bool = true,
            requestQueue: DispatchQueue? = nil,
            serializationQueue: DispatchQueue? = nil,
            interceptor: RequestInterceptor? = nil,
            serverTrustManager: ServerTrustManager? = nil,
            redirectHandler: RedirectHandler? = nil,
            cachedResponseHandler: CachedResponseHandler? = nil,
            eventMonitors: [EventMonitor] = [])
通过使用URLSessionConfiguration来初始化 Session,最终还是使用了URLSession
public convenience init(configuration: URLSessionConfiguration = URLSessionConfiguration.af.default,
                        delegate: SessionDelegate = SessionDelegate(),
                        rootQueue: DispatchQueue = DispatchQueue(label: "org.alamofire.session.rootQueue"),
                        startRequestsImmediately: Bool = true,
                        requestQueue: DispatchQueue? = nil,
                        serializationQueue: DispatchQueue? = nil,
                        interceptor: RequestInterceptor? = nil,
                        serverTrustManager: ServerTrustManager? = nil,
                        redirectHandler: RedirectHandler? = nil,
                        cachedResponseHandler: CachedResponseHandler? = nil,
                        eventMonitors: [EventMonitor] = [])
{
    let delegateQueue = OperationQueue(maxConcurrentOperationCount: 1, underlyingQueue: rootQueue, name: "org.alamofire.session.sessionDelegateQueue")
    let session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: delegateQueue)

    self.init(session: session,
              delegate: delegate,
              rootQueue: rootQueue,
              startRequestsImmediately: startRequestsImmediately,
              requestQueue: requestQueue,
              serializationQueue: serializationQueue,
              interceptor: interceptor,
              serverTrustManager: serverTrustManager,
              redirectHandler: redirectHandler,
              cachedResponseHandler: cachedResponseHandler,
              eventMonitors: eventMonitors)
}

2、Request

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

open func request(_ convertible: URLRequestConvertible, interceptor: RequestInterceptor? = nil) -> DataRequest
a、URLConvertible协议

我们在例子里传入的urlString类型,也可以将它转成URL类型再传入,结果都正确。这是因为StringURL都遵循URLConvertible协议,通过实现协议里的asURL()方法,从StringURLURLComponents类型转换为URL,若失败则抛出为AFError的错误。

public protocol URLConvertible
{
    func asURL() throws -> URL
}
extension String: URLConvertible
{
    public func asURL() throws -> URL
    {
        guard let url = URL(string: self) 
        return url
    }
}
extension URL: URLConvertible
{
    public func asURL() throws -> URL { self }
}
extension URLComponents: URLConvertible
{
    public func asURL() throws -> URL
    {
        // 如果转换 url 失败, 抛出一个异常
        guard let url = url else { throw AFError.invalidURL(url: self) }
        return url
    }
}

b、URLRequestConvertible协议

负责URLRequest的转换,和上面类似,就不多介绍了。

// 可以转换成 urlrequest 的协议
public protocol URLRequestConvertible
{
    // 返回一个 urlrequest, 如果有错, 可以抛出异常
    func asURLRequest() throws -> URLRequest
}

// 创建 urlRequest
extension URLRequestConvertible
{
    
    public var urlRequest: URLRequest? { try? asURLRequest() }
}

// 返回自身
extension URLRequest: URLRequestConvertible
{
    public func asURLRequest() throws -> URLRequest { self }
}

c、返回Request的子类
❶ 包括request、download、upload等方法都是传入所需参数,发起请求,再返回Request的子类
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
open func download(_ convertible: URLConvertible,
                   method: HTTPMethod = .get,
                   parameters: Parameters? = nil,
                   encoding: ParameterEncoding = URLEncoding.default,
                   headers: HTTPHeaders? = nil,
                   interceptor: RequestInterceptor? = nil,
                   requestModifier: RequestModifier? = nil,
                   to destination: DownloadRequest.Destination? = nil) -> DownloadRequest
open func upload(_ data: Data,
                 to convertible: URLConvertible,
                 method: HTTPMethod = .post,
                 headers: HTTPHeaders? = nil,
                 interceptor: RequestInterceptor? = nil,
                 fileManager: FileManager = .default,
                 requestModifier: RequestModifier? = nil) -> UploadRequest 
❷ 返回给我们的Request,可以让我们控制请求的暂停,恢复,取消等

请求状态

fileprivate var mutableState = MutableState()

public var state: State { mutableState.state }
public var isInitialized: Bool { state == .initialized }// 初始化
public var isResumed: Bool { state == .resumed }// 恢复请求
public var isSuspended: Bool { state == .suspended }// 暂停请求
public var isCancelled: Bool { state == .cancelled }// 取消请求
public var isFinished: Bool { state == .finished }// 完成请求

暂停请求

public func suspend() -> Self
{
    $mutableState.write
    { mutableState in
        // 如果不能暂停, 那么就跳过
        guard mutableState.state.canTransitionTo(.suspended) else { return }
        // 变更可变状态为暂停
        mutableState.state = .suspended
        // 在下层队列中更新暂停状态,didSuspend()在暂停完成时调用
        underlyingQueue.async { self.didSuspend() }

        guard let task = mutableState.tasks.last, task.state != .completed else { return }
        // 调用真正的网络请求URLSessionTask的suspend()方法
        task.suspend()
        // 在下层队列中更新暂停状态,didSuspendTask()在URLSessionTask暂停时调用
        underlyingQueue.async { self.didSuspendTask(task) }
    }

    return self
}

取消请求

public func cancel() -> Self
{
    $mutableState.write
    { mutableState in
        guard mutableState.state.canTransitionTo(.cancelled) else { return }

        mutableState.state = .cancelled

        underlyingQueue.async { self.didCancel() }

        // Resume to ensure metrics are gathered.
        task.resume()
        // 取消
        task.cancel()
        underlyingQueue.async { self.didCancelTask(task) }
    }

    return self
}

恢复请求

public func resume() -> Self
{
    $mutableState.write { mutableState in
        guard mutableState.state.canTransitionTo(.resumed) else { return }

        mutableState.state = .resumed

        underlyingQueue.async { self.didResume() }

        guard let task = mutableState.tasks.last, task.state != .completed else { return }

        task.resume()
        underlyingQueue.async { self.didResumeTask(task) }
    }

    return self
}

did方法,以恢复请求为例。

func didResume()
{
    dispatchPrecondition(condition: .onQueue(underlyingQueue))

    eventMonitor?.requestDidResume(self)
}

func didResumeTask(_ task: URLSessionTask)
{
    dispatchPrecondition(condition: .onQueue(underlyingQueue))

    eventMonitor?.request(self, didResumeTask: task)
}

3、HTTPMethod

a、HTTPMethod是个结构体
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 post = HTTPMethod(rawValue: "POST")
    ...

    public let rawValue: String

    public init(rawValue: String)
    {
        self.rawValue = rawValue
    }
}

b、打印HTTPMethod的值
open func request(_ convertible: URLConvertible,
                  method: HTTPMethod = .get,
                  ...
{
    print("打印HTTPMethod的枚举:\(method)")
    print("打印HTTPMethod的枚举关联值:\(method.rawValue)")
    ......
}

输出结果

打印HTTPMethod的值:HTTPMethod(rawValue: "GET")
打印HTTPMethod的原始值:GET

c、会话配置
❶ Session的初始化方法中使用了默认的会话配置
public convenience init(configuration: URLSessionConfiguration = URLSessionConfiguration.af.default....)
❷ 使用了系统提供的默认会话配置,并为其配置了默认的HTTP的头部信息
public static var default: URLSessionConfiguration
{
    let configuration = URLSessionConfiguration.default
    configuration.headers = .default

    return configuration
}
❸ HTTP默认的头部信息包括User-Agent、Accept-Encoding、Accept-Language
extension HTTPHeaders
{
    public static let default: HTTPHeaders = [.defaultAcceptEncoding,
                                                .defaultAcceptLanguage,
                                                .defaultUserAgent]
}
public static let defaultAcceptEncoding: HTTPHeader = {
    let encodings: [String]
    encodings = ["br", "gzip", "deflate"]
    return .acceptEncoding(encodings.qualityEncoded())
}()
public static let defaultAcceptLanguage: HTTPHeader =
{
    .acceptLanguage(Locale.preferredLanguages.prefix(6).qualityEncoded())
}()
public static let defaultUserAgent: HTTPHeader =
{
    let bundle = info?[kCFBundleIdentifierKey as String] as? String ?? "Unknown"
    let appVersion = info?["CFBundleShortVersionString"] as? String ?? "Unknown"
    let appBuild = info?[kCFBundleVersionKey as String] as? String ?? "Unknown"
    let alamofireVersion = "Alamofire/\(version)"
    ...
    let userAgent = "\(executable)/\(appVersion) (\(bundle); build:\(appBuild); \(osNameVersion)) \(alamofireVersion)"
    return .userAgent(userAgent)
}
❹ 运行结果
{
  "args": {},
  "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": "UseAlamofire/1.0 (com.xiejiapei.UseAlamofire; build:1; iOS 14.3.0) Alamofire/5.4.1",
    "X-Amzn-Trace-Id": "Root=1-600e58ba-22848fce5ae105571e598f1e"
  },
  "origin": "222.76.251.163",
  "url": "https://httpbin.org/get"
}

4、ParameterEncoding

https://johnny:p4ssw0rd@www.example.com:443/script.ext;param=value?query=value#ref

==========url的组成=========

scheme //https
user //johnny
password //p4ssw0rd
host //www.example.com
port //443
path // /script.ext
pathExtension //ext
pathComponents //["/", "script.ext"]
parameterString //param=value
query //query=value
fragment //ref
encoding: ParameterEncoding = URLEncoding.default,

如果我有一个参数字典,这个参数字典又是如何从客户端传递到服务器的呢?Alamofire中是这样实现的:URLEncoding编码方式会把参数直接拼接到URL中或通过requesthttpBody传值。JSONEncoding编码方式会把参数字典编码成JSONData后赋值给requesthttpBody


a、ParameterEncoding协议
  • ParameterEncoding协议:是一个定义如何编码的协议,常会用到的URLEncodingJSONEncoding编码方式都遵循这个协议
  • encode函数:用来完成对传入的parameters的编码工作,把参数绑定到urlRequest之中
  • urlRequest参数:需要实现URLRequestConvertible协议,实现该协议的对象能够转换成URLRequest
  • parameters参数:类型为Parameters,也就是字典:public typealias Parameters = [String: Any]
  • 函数返回值类型为URLRequest:至于返回的urlRequest是不是之前的urlRequest,这个不一定。另一个比较重要的是该函数会抛出异常
public protocol ParameterEncoding
{
    func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest
}

b、URLEncoding
❶ url-encoded编码方式
  • URLEncoding会生成一个使用url-encoded方式编码过的字符串,可以添加到url或是请求体中,至于使用何种方式取决于编码的目的地参数
  • http 头中的 Content-Type 字段会被设置为 application/x-www-form-urlencoded; charset=utf-8
  • 由于没有一个明确的规定如何编码一个集合,我们在这里约定,对于数组,我们会在名字后面加上一个中括号[] 如(foo[]=1&foo[]=2),对于字典,则在中括号中再加入键值,如foo[bar]=baz
public struct URLEncoding: ParameterEncoding
❷ 编码后的参数位置:定义编码后的字符串是放到url还是请求体中
  • methodDependent:对于 .get.head.delete 请求,它会将已编码查询字符串应用到现有的查询字符串中;对于其他类型的请求,会将其设置为 HTTP body
  • queryString: 将编码字符串设置或追加到请求的 URL
  • httpBody:将编码字符串设置为 URLRequestHTTP body
public enum Destination
{
    case methodDependent
    case queryString
    case httpBody

    // 是否将编码字符串放到url中
    func encodesParametersInURL(for method: HTTPMethod) -> Bool
    {
        switch self
        {
        case .methodDependent: return [.get, .head, .delete].contains(method)
        case .queryString: return true
        case .httpBody: return false
        }
    }
}
❸ ParameterEncoding 协议的实现:编码并设置 request对象
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 = urlRequest.method, destination.encodesParametersInURL(for: method)// 直接编码到 url 中
    {
        // 获取 url
        guard let url = urlRequest.url else
        {
            throw AFError.parameterEncodingFailed(reason: .missingURL)
        }

        // 构建一个URLComponents对象,并在其中添加参数
        if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), !parameters.isEmpty
        {
            // 此处 map 是 optional 的map,如果 optionvalue 不为空,则会调用 map 内的闭包
            // 如果 url 中本来就有一部分参数了,那么就将新的参数附加在后面
            let percentEncodedQuery = (urlComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(parameters)
            urlComponents.percentEncodedQuery = percentEncodedQuery
            urlRequest.url = urlComponents.url
        }
    }
    else// 这里是要添加到请求体中
    {
        // 如果请求头尚未设置 Content-Type
        if urlRequest.headers["Content-Type"] == nil
        {
            // 在请求头中设置编码格式
            urlRequest.headers.update(.contentType("application/x-www-form-urlencoded; charset=utf-8"))
        }
        // 编码到请求体中
        urlRequest.httpBody = Data(query(parameters).utf8)
    }
}
❹ 将参数编码为查询字符串

可以看到URLEncoding方式是将parameters通过添加 & ,= 的方式拼接到url身后。

private func query(_ parameters: [String: Any]) -> String
{
    // 创建一个数组,这个数组中存放的是元组数据,元组中存放的是key和字符串类型的value
    var components: [(String, String)] = []

    // 遍历参数,对参数做进一步的处理,然后拼接到数组中
    for key in parameters.keys.sorted(by: <)
    {
        let value = parameters[key]!

        // key的类型是String,但value的类型是any
        // 也就是说value不一定是字符串,也有可能是数组或字典,因此针对value需要做进一步的处理
        components += queryComponents(fromKey: key, value: value)
    }
    // 把元组内部的数据用=号拼接,然后用符号&把数组拼接成字符串
    return components.map { "\($0)=\($1)" }.joined(separator: "&")
}
❺ 根据value类型进行百分号转义

key的类型是String,但value的类型是any,也就是说value不一定是字符串,也有可能是数组或字典,因此针对value需要做进一步的处理。

public func queryComponents(fromKey key: String, value: Any) -> [(String, String)]
{
    // 最终结果
    var components: [(String, String)] = []
    
    switch value
    {
    // 如果value依然是字典,那么键后面加上[key]再调用自身,也就是做递归处理
    case let dictionary as [String: Any]:
        for (nestedKey, value) in dictionary
        {
            components += queryComponents(fromKey: "\(key)[\(nestedKey)]", value: value)
        }
    // 如果value是数组,通过遍历在键后面加上[]后依然调用自身
    // 把数组拼接到url中的规则是这样的:数组["a", "b", "c"]拼接后的结果是key[]="a"&key[]="b"&key[]="c"
    case let array as [Any]:
        for value in array
        {
            components += queryComponents(fromKey: arrayEncoding.encode(key: key), value: value)
        }
    // 如果value是NSNumber,要进一步判断这个NSNumber是不是表示布尔类型
    case let number as NSNumber:
        if number.isBool// bool 值的处理
        {
            components.append((escape(key), escape(boolEncoding.encode(value: number.boolValue))))
        }
        else
        {
            components.append((escape(key), escape("\(number)")))
        }
    // 如果value是Bool,转义后直接拼接进数组
    case let bool as Bool:
        components.append((escape(key), escape(boolEncoding.encode(value: bool))))
    // 其他情况,转义后直接拼接进数组
    default:
        components.append((escape(key), escape("\(value)")))
    }
    return components
}
❻ 百分号转义

上边函数中的key已经是字符串类型了,那么为什么还要进行转义的?这是因为在url中有些字符是不允许的,这些字符会干扰url的解析。:#[]@!$&'()*+,;=这些字符必须要做转义,而?/可以不用转义。转义的意思就是百分号编码。

public func escape(_ string: String) -> String
{
    // 使用了系统自带的函数来进行百分号编码
    string.addingPercentEncoding(withAllowedCharacters: .afURLQueryAllowed) ?? string
}
❼ 进行验证

将核心流程范例里的url变为使用URLEncoding对参数编码后的字符串,并且向parameternil,再进行请求依然会得到同样的结果。

let url = "http://suggest.taobao.com/sug?code=utf-8&q=%E8%A2%9C%E5%AD%90"
AF.request(url, method: .get, parameters: nil)

输出结果为:

json字符串:
{"result":[["袜子女","981907.3641717125"],["袜子男","774851.6019127683"],["袜子女冬","627697.2976415411"],["袜子女中筒袜","350247.61393421463"],["袜子男长袜","615388.650406337"],["袜子男冬","703565.6863077434"],["袜子女ins潮","285066.1735027441"],["袜子男纯棉","303921.3111099697"],["袜子女冬季 加绒","335283.97906813805"],["袜子男中筒","331615.3393111639"]]}

d、JSONParameterEncoder

JSONEncoding的主要作用是把参数以JSON的形式编码到request之中,当然是通过requesthttpBody进行赋值的。JSONEncoding提供了两种处理函数,一种是对普通的字典参数进行编码,另一种是对字符串数组进行编码,处理这两种情况的函数基本上是相同的。

❶ 使用 json 编码参数
public struct JSONEncoding: ParameterEncoding
{
    // 使用默认参数构造
    public static var `default`: JSONEncoding { JSONEncoding() }

    // 让其拥有更好的展示效果
    public static var prettyPrinted: JSONEncoding { JSONEncoding(options: .prettyPrinted) }

    // JSON序列化的写入方式
    public let options: JSONSerialization.WritingOptions
    public init(options: JSONSerialization.WritingOptions = [])
    {
        self.options = options
    }
}
❷ ParameterEncoding 协议的实现:将parameters转化为二进制放入httpBody里

因此也很好理解,为什么get方法的安全性不如post方法,因为get方法将请求参数暴露在外,而post放在请求体内。

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

    guard let parameters = parameters else { return urlRequest }

    do
    {
        // json 格式化数据
        let data = try JSONSerialization.data(withJSONObject: parameters, options: options)

        // 如果 Content-Type 尚未设置
        if urlRequest.headers["Content-Type"] == nil
        {
            // 设置请求头的Content-Type
            urlRequest.headers.update(.contentType("application/json"))
        }
        // 加上请求体
        urlRequest.httpBody = data
    }
    catch
    {
        throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
    }

    return urlRequest
}
还有一个encode方法,基本同上一致,但是可以接受数组的 json
public func encode(_ urlRequest: URLRequestConvertible, withJSONObject jsonObject: Any? = nil) throws -> URLRequest

5、Validation

.validate(statusCode: [200])
a、响应码的认证

validateDataRequest的扩展中增加的方法。我们这里先看对statusCode的认证。

extension DataRequest
{

    public typealias Validation = (URLRequest?, HTTPURLResponse, Data?) -> ValidationResult

    public func validate<S: Sequence>(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int
    {
        validate { [unowned self] _, response, _ in
            self.validate(statusCode: acceptableStatusCodes, response: response)
        }
    }
}

那么到底是如何认证的呢?这里我们再进入它的内部实现。

fileprivate func validate<S: Sequence>(statusCode acceptableStatusCodes: S,
                                       response: HTTPURLResponse)
    -> ValidationResult
    where S.Iterator.Element == Int
{
    // 若validate传入code包含response响应的code则认证成功
    if acceptableStatusCodes.contains(response.statusCode)
    {
        return .success(())
    }
    // 若不包括,则返回失败,抛出AFError
    else
    {
        let reason: ErrorReason = .unacceptableStatusCode(code: response.statusCode)
        return .failure(AFError.responseValidationFailed(reason: reason))
    }
}

b、错误验证

acceptableStatusCodes为我们实际在validate中传入的statusCode数组,在例子中为[200]。将例子中的[200]变为[201]就会打印出抛出的错误,因为http的正确返回码为200,我们传入的数组不包含200,就会抛出错误。当然我们也可以在例子中不调用validate这样就不会有这些验证。

错误信息:responseValidationFailed(reason: Alamofire.AFError.ResponseValidationFailureReason.unacceptableStatusCode(code: 200))

c、响应内容的认证

这里只介绍了statusCode响应码的认证,实际上validate里还有对contentType响应内容等的认证。

public func validate<S: Sequence>(contentType acceptableContentTypes: @escaping @autoclosure () -> S) -> Self where S.Iterator.Element == String
{
    validate { [unowned self] _, response, data in
        self.validate(contentType: acceptableContentTypes(), response: response, data: data)
    }
}
fileprivate func validate<S: Sequence>(contentType acceptableContentTypes: S,
                                       response: HTTPURLResponse,
                                       data: Data?)
    -> ValidationResult
    where S.Iterator.Element == String
{
    guard let data = data, !data.isEmpty else { return .success(()) }

    return validate(contentType: acceptableContentTypes, response: response)
}

6、DispatchQueue

a、responseData

我们会把请求返回的处理放入queue所传入的队列中,若我们没有向queue传入队列,那么会默认把处理放入主队列中。范例子中我们选择将处理放入全局队列中。

.responseData(queue: DispatchQueue.global())

public func responseData(queue: DispatchQueue = .main...)

b、DispatchQueue+Alamofire.swift

可以顺便看一下 DispatchQueue+Alamofire.swift 文件,里面增加了一个名为after的延迟调用方法,没有添加public关键字,那么说明只想在Alamofire内部调用,而不想暴露给我们。

extension DispatchQueue
{
    func after(_ delay: TimeInterval, execute closure: @escaping () -> Void)
    {
        asyncAfter(deadline: .now() + delay, execute: closure)
    }
}

c、Request.swift
// 所有内部异步操作的串行队列
public let underlyingQueue: DispatchQueue
// 用于所有序列化操作的队列。默认情况下,它是一个以 underlyingQueue 为目标的串行队列
public let serializationQueue: DispatchQueue

init(id: UUID = UUID(),
     underlyingQueue: DispatchQueue,
     serializationQueue: DispatchQueue,
     eventMonitor: EventMonitor?,
     interceptor: RequestInterceptor?,
     delegate: RequestDelegate)
{
    self.id = id
    self.underlyingQueue = underlyingQueue
    self.serializationQueue = serializationQueue
    self.eventMonitor = eventMonitor
    self.interceptor = interceptor
    self.delegate = delegate
}

7、AFError

a、AFDataResponse
.responseData(queue: DispatchQueue.global())
{ (responseData) in
    switch responseData.result
    {
    case .failure(let error):
      print("错误信息:\(error)")
    }
}

public func responseData(completionHandler: @escaping (AFDataResponse<Data>) -> Void)
public typealias AFDataResponse<Success> = DataResponse<Success, AFError>

b、AFError的类型

Alamofire里的错误都已经被定义到AFError中,我们打开 AFError.swift。在下面列举的AFError的类型中,除了invalidURL的参数是遵循协议URLConvertible,其余类型的参数又由枚举类型组成。这样就将很多种的错误类型,包括在了这5种类型中。

//这里为了看得方便,将AFError重新整理了下,源码中的内容要更多,但类型是一样的
enum AFError 
{     
  case invalidURL(url: URLConvertible) //无效的URL 
  case parameterEncodingFailed(reason: ParameterEncodingFailureReason) //请求参数编码失败 
  case multipartEncodingFailed(reason: MultipartEncodingFailureReason) //多部分编码失败 
  case responseValidationFailed(reason: ResponseValidationFailureReason) //响应验证失败 
  case responseSerializationFailed(reason: ResponseSerializationFailureReason) //响应序列化失败 
}

c、localizedDescription

AFError.swift里面还有很多内容,例如对localizedDescription的实现,在请求抛出错误时,我们打印error.localizedDescription看到的错误信息就是在这里定义的。

// 请求参数编码失败
extension AFError.ParameterEncodingFailureReason
{
    var localizedDescription: String
    {
        switch self
        {
        case .missingURL:
            return "URL request to encode was missing a URL"
        case let .jsonEncodingFailed(error):
            return "JSON could not be encoded because of error:\n\(error.localizedDescription)"
        case let .customEncodingFailed(error):
            return "Custom parameter encoder failed with error: \(error.localizedDescription)"
        }
    }
}

8、DataResponse

.responseData(queue: DispatchQueue.global())
{ (responseData) in
a、Any类型

假如说序列化后的数据是data,最直接的想法就是把data设置为Any类型,在实际用到的时候在进行判断,这也是最普通的一种开发思维。现在我们就要打破这种思维。我们需要封装一个对象,这个对象能够表达任何结果,这就用到了swift中的泛型。

public func responseJSON(queue: DispatchQueue = .main,
                         dataPreprocessor: DataPreprocessor = JSONResponseSerializer.defaultDataPreprocessor,
                         emptyResponseCodes: Set<Int> = JSONResponseSerializer.defaultEmptyResponseCodes,
                         emptyRequestMethods: Set<HTTPMethod> = JSONResponseSerializer.defaultEmptyRequestMethods,
                         options: JSONSerialization.ReadingOptions = .allowFragments,
                         completionHandler: @escaping (AFDataResponse<Any>) -> Void) -> Self

b、Result<Any>

上边的这个函数的主要目的是把请求成功后的结果序列化为JSONcompletionHandler函数的参数类型为DataResponse<Any>,其中的Any就会传递给Result,也就是Result<Any>

public typealias AFDataResponse<Success> = DataResponse<Success, AFError>
public struct DataResponse<Success, Failure: Error>
{
    public let result: Result<Success, Failure>
}

@frozen public enum Result<Success, Failure> where Failure : Error
{
    /// A success, storing a `Success` value.
    case success(Success)

    /// A failure, storing a `Failure` value.
    case failure(Failure)
}

c、jsonObject

那么问题来了,不是把数据解析成JSON了吗?为什么要返回Any类型呢?json本质上很类似于JavaScript中的对象和数组。JSONSerialization.jsonObject返回的类型是Any,这是因为解析后的数据有可能是数组,也有可能是字典。当然如果不是这两种格式的数据,使用JSONSerialization.jsonObject解析会抛出异常。

// 字典
{
    "people":
    [
        {"firstName":"Brett","lastName":"McLaughlin","email":"aaaa"},
        {"firstName":"Jason","lastName":"Hunter","email":"bbbb"},
        {"firstName":"Elliotte","lastName":"Harold","email":"cccc"}
    ]
}

// 数组
[
    "a",
    "b",
    "c"
]

d、打印信息

为了能够打印更加详细的信息,又使DataResponse实现了CustomStringConvertibleCustomDebugStringConvertible协议。

description
extension DataResponse: CustomStringConvertible, CustomDebugStringConvertible
{
    public var description: String
    {
        "\(result)"
    }
}
debugDescription
extension DataResponse: CustomStringConvertible, CustomDebugStringConvertible
{
    public var debugDescription: String
    {
        guard let urlRequest = request else { return "[Request]: None\n[Result]: \(result)" }

        let requestDescription = DebugDescription.description(of: urlRequest)

        let responseDescription = response.map { response in
            let responseBodyDescription = DebugDescription.description(for: data, headers: response.headers)

            return """
            \(DebugDescription.description(of: response))
                \(responseBodyDescription.indentingNewlines())
            """
        } ?? "[Response]: None"

        let networkDuration = metrics.map { "\($0.taskInterval.duration)s" } ?? "None"

        return """
        \(requestDescription)
        \(responseDescription)
        [Network Duration]: \(networkDuration)
        [Serialization Duration]: \(serializationDuration)s
        [Result]: \(result)
        """
    }
}

续文见下篇:IOS源码解析:Alamofire 5 功能模块


Demo

Demo在我的Github上,欢迎下载。
SourceCodeAnalysisDemo

参考文献

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容