Alamofire 学习

3,596 阅读8分钟

Alamofre 是 iOS 上名气比较大的 Swift 网络库。基本上,开发 Swift 项目,要进行网络请求的话,第一反应就是使用 Alamofire 库,应该其有简单易用的 API,使用起来方便易用。 但是我却一直没有深入了解过 Alamofire 的源码,也不清楚和直接使用 iOS 原生的 URLSession 相比,Alamofire 的优势究竟在哪?劣势呢? 本篇文章主要是为了解答这几个问题:

  1. 和直接使用 URLSession 相比,Alamofire 的优势和劣势在哪?
  2. Alamofre 提供了什么额外的功能?
  3. 作为一个第三方库,Alamofire API 的易用性、可扩展性如何?是如何做到的?
  4. Alamofire 里面使用了什么优秀的设计、结构?可以在别的项目上借鉴。

简单网络请求 API

因为 Alamofire 是基于 URLSession 的封装,我们第一个需要对比的当然是,分别使用 Alamofire 和 URLSession 发起一个简单的网络请求,代码调用的差异是什么,这样其实我们能简单看出,为什么这么多开发者选用了 Alamofire,而不是直接使用原生的 URLSession。

使用原生 URLSession:

guard let url = URL(string: "example.com") else { return }
        
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
		guard error == nil else { return }
		// handle data and response
}
task.resume()

使用 Alamofire:

Alamofire.request("example.com").response { (response) in
		guard response.error == nil else { return }
		// handle data and response
}

可以看到,和 URLSession 对比,使用 Alamofire 的代码简单了许多。具体表现在:

  1. 调用简单,不需要创建、持有 session 和 task,不需要对 task 执行 resume() 来触发请求。
  2. 请求的 URL,调用时不需检查其合法性。

因为 Alamofire 是基于 URLSession 的封装,也就是说在最底层,Alamofire 肯定也是通过调用 URLSession 来进行请求,让我们仔细看看整个请求发起的代码调用:

可以看到,请求的入口是 Alamofire.request():

/// Alamofire.swift

@discardableResult
public func request(
    _ url: URLConvertible,
    method: HTTPMethod = .get,
    parameters: Parameters? = nil,
    encoding: ParameterEncoding = URLEncoding.default,
    headers: HTTPHeaders? = nil)
    -> DataRequest
{
    return SessionManager.default.request(
        url,
        method: method,
        parameters: parameters,
        encoding: encoding,
        headers: headers
    )
}

这个方法只是一个 API 入口,方法内实际调用了 SessionManager.default.request

/// SessionManager.swift

public static let `default`: SessionManager = {
        let configuration = URLSessionConfiguration.default
        configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders

        return SessionManager(configuration: configuration)
}()

@discardableResult
    open func request(
        _ url: URLConvertible,
        method: HTTPMethod = .get,
        parameters: Parameters? = nil,
        encoding: ParameterEncoding = URLEncoding.default,
        headers: HTTPHeaders? = nil)
        -> DataRequest
    {
        var originalRequest: URLRequest?

        do {
            originalRequest = try URLRequest(url: url, method: method, headers: headers)
            let encodedURLRequest = try encoding.encode(originalRequest!, with: parameters)
            return request(encodedURLRequest)
        } catch {
            return request(originalRequest, failedWith: error)
        }
    }

打开 SessionManager.swift,可以看到,request(_ url: URLConvertible) 里面通过入参,创建了 URLRequest,并且调用了另外一个 request(urlRequest: URLRequest) 的方法:

/// SessionManager.swift

@discardableResult
    open func request(_ urlRequest: URLRequestConvertible) -> DataRequest {
        var originalRequest: URLRequest?

        do {
            originalRequest = try urlRequest.asURLRequest()
            let originalTask = DataRequest.Requestable(urlRequest: originalRequest!)

            let task = try originalTask.task(session: session, adapter: adapter, queue: queue)
            let request = DataRequest(session: session, requestTask: .data(originalTask, task))

            delegate[task] = request

            if startRequestsImmediately { request.resume() }

            return request
        } catch {
            return request(originalRequest, failedWith: error)
        }
    }

request(urlRequest: URLRequest)中,通过 originalTask.task(session: session, adapter: adapter, queue: queue) 创建了 URLSessionTask 后,继续创建了 DataRequest,持有了对应的 Session 和 URLSessionTask,后续直接通过 DataRequest 来持有管理网络请求。直接执行 request.resume() 并返回 request 对象,至此一个网络请求就发送成功了。

下面来回顾一下前面提到的 Alamofire 调用简洁的点,看看 Alamofire 是如何做到的:

  1. 调用简单,不需要创建、持有 session 和 task,不需要对 task 执行 resume() 来触发请求。

SessionManager 是对 URLSession 的一个封装,SessionManager 持有了底层 URLSession 对象,负责创建请求 Request 对象。

通过全局 SessionManager.default 的使用,让一般简单请求不需要手动创建 URLSession,让请求的调用更简洁: 前面的代码可以看到 URLSession 是作为底层对象被 SessionManager 持有,一个简单的网络请求,可以通过直接调用 Alamofire.request() 来发起,默认使用 SessionManager.default 来发起请求,所以不需要使用者手动创建 SessionManager,减少了代码量。但是如果使用者希望自定义 URLSession 的配置参数,则必须手动重建一个 SessionManager 对象。

  1. 请求的 URL,调用时不需检查其合法性。 Swift 代码的特色就是强制类型判断,确保安全性。URLSession.dataTask(with: URL) 的入参为 URL,所以生成 URL 时,需要确保 URL 创建了,才能进行下一步的操作,这就导致需要额外的代码进行判断处理。

而在 Alamofire 的 API 中,入参为 URLConvertible 的协议,Alamofire 中对String, URL , URLComponents 都扩展遵从了该协议。遵从 URLConvertible 协议会实现 asURL() 的方法来生产 URL,当无法生成 URL 时, asURL() 会抛出异常。这类异常会抛出到 .response 里面的回调进行统一处理。

/// Alamofire.swift

/// Types adopting the `URLConvertible` protocol can be used to construct URLs, which are then used to construct
/// URL requests.
public protocol URLConvertible {
    /// Returns a URL that conforms to RFC 2396 or throws an `Error`.
    ///
    /// - throws: An `Error` if the type cannot be converted to a `URL`.
    ///
    /// - returns: A URL or throws an `Error`.
    func asURL() throws -> URL
}

这样的设计很好得减少了代码量,而且同时保证了类型安全性。另外通过 URLConvertible 这个协议,在大型网络请求 APP 的开发时,能够以路由的方式更好地管理接口,而不只是简单的 URL String,这样能提高接口文件的易读性与可维护性。这个后面会提到。

  • 总结:可以看到使用 Alamofire 发送请求,调用比 URLSession 简单许多,不需要创建多个对象、不需要单独处理 URL 异常、不需要对 task 执行 resume() 来触发请求。

文件结构与类结构

前一节分析了 Alamofire 一个简单的网络请求的实现,下面让我们再仔细看看 Alamofire 的整体文件结构与类结构:

文件结构

文件结构

上面就是 Alamofire 的所有实现文件,我们来简单解释一下每个文件所完成的功能:

  • AFError.swift 封装好的各种错误类型,包括错误的描述
  • Alamofire.swift 封装好的发送请求的便捷方法,使用Alamofire的方法发送请求会统一使用全局的 SessionManager.default 的配置进行请求
  • DispatchQueue+Alamofire.swift 封装了 dispatchQueue 多线程的调用和asyncAfter方法
  • MultipartFormData.swift 对 MultipartFormData 类型的支持,什么是multipart/form-data请求 - nd - 博客园
  • NetworkReachabilityManager.swift 判断网络连接状况的类,基于 SCNetworkReachability 和 SCNetworkReachabilityFlags,并且可以监听网络状况的变化
  • Notifications.swift 封装的通知名,与通知 userInfo 里面的 key
  • ParameterEncoding.swift 对urlRequest的参数进行编码,包括不同的encoding方式
  • Request.swift 代表网络请求的类,包括 dataRequest, downloadRequest, uploadRequest, streamRequest 这四个子类
  • Response.swift 网络请求结果,包括 DefaultDataResponse, DataResponse, DefaultDownloadResponse, DownloadResponse,
  • ResponseSerialization.swift Response 序列化,将 response 序列化为 result
  • Result.swift 网络请求结果,请求结果,用来标记请求是否成功
  • ServerTrustPolicy.swift 进行网络请求证书验证的类
  • SessionDelegate.swift 处理 SessionManager 发起请求的所有回调
  • SessionManager.swift 统一创建和管理 request 的类,同时管理底层的 NSURLSession。网络请求回调交给 SessionDelegate 处理
  • TaskDelegate.swift 处理单独每个 request 请求的回调
  • Timeline.swift 记录请求完整生命周期的相关时间
  • Validation.swift 检查一个 request 的返回结果是否成功,根据请求的 statusCode ,若验证失败,则会产生一个相关的错误,可进行复写自定义

类结构

类结构
Alamofire 封装的整体结构最主要的是 SessionManager, SessionDelegate, Request 和 TaskDelegate。 SessionManager 作为请求的入口,只负责持有底层的 URLSession,创建请求的 Request。SessionManager 发起请求后,请求的结果则由 SessionDelegate 回调来处理,同时 SessionDelegate 持有所有的发起请求的 Task,结果返回时,能再交由 TaskDelegate 处理。

使用的设计模式、代码结构可借鉴之处

代码设计模式

1. 单一职责原则(Single responsibility principle)

Alamofire 中,不同类的职责分得较为清晰,SessionManager 负责 Request 的场景与配置,SessionDelegate 负责处理请求结果的回调,Request 负责创建请求对应的 URLSessionTask。 我们平时创建自己的 Manager 时,为了贪求方便,都会直接把大部分的代码放进 Manager,并美其名为封装,但是会导致 Manager 的代码大量耦合,改动时,牵一发而动全身。

2. 面向接口编程

Alamofire 中,使用了大量的接口,例如, URLEncoding, RequestAdapter, URLRequestConvertible, DataResponseSerializerProtocol 等。作为第三方库,使用协议,而不使用继承,更加是增加了代码的可读性与可扩展性。面向协议好处:

  • Swfit 中,不能继承多个类,协议能够继承多个。
  • 接口可读性更强。
  • 代码更加组件化,减少代码依赖,扩展性更好。
  • 方便单元测试。
  • 能更好地区分代码职责,避免出现职责过度集中的情况。

功能上

1. 提供网络请求重试的实现。

当请求失败时,会调用RequestRetrier,来决定是否对请求进行重试。结合 RequestAdapter 能够实现 OAuth 的重新验证。Retrying and Adapting

2. 链式调用

Alamofire 中使用了大量的链式调用,如: request().validate().response().downloadProgress()

Alamofire 对链式调用的实现,调用方法时,方法会返回给类型本身,同时,因为请求是异步的,所以实际的功能代码没有立即执行,而是通过保存在 request 里,等结果返回时,再进行调用。例如上面的例子: request().validate().response().downloadProgress()

  • request(),会发起请求,但是注意请求是异步返回的。
  • validate(), 对请求结果进行状态验证,因为此时实际上请求刚刚发起,并没有返回,没有办法对请求进行验证。所以此时调用 validate() ,实际上做的是:validations.append(validationExecution) 将验证的方法存起来,并没有执行。
  • response(),同样的调用时,会执行 delegate.queue.addOperation { } 将操作添加到 TaskDelegate 的队列里
  • downloadProgress(), 调用时,会赋值给 dataDelegate.progressHandler = (closure, queue)

可以看到,当执行完上面所有调用时,实际上只是发送了一个网络请求,并且将所有回调时需要执行的操作都进行赋值或添加。等到请求收到数据时,会一一调用上述的操作。 例如,在请求开始时会将 delegate.queue.isSuspend = true,此时里面添加的操作将不会执行,当请求结束后,会将 delegate.queue.isSuspend = false,此时,之前添加的请求成功的回调便会一一执行。

链式调用的好处在于,对于有大量 callback 的场景,链式调用更加的易读,不会导致代码嵌套在一大堆 { } 之间,难以区分。