Moya 代码阅读

3,865 阅读12分钟

第三方框架阅读的文章千千万,写下文章的目的一个是为了督促自己完整的学习,另一个目的是输出倒逼自己思考

Moya 是在 Alamofire 基础上的一个网络抽象层。简单的介绍可以移步对应的 Github 地址

POP

因为库的整体结构是根据 POP 面向协议编程的思想,所以先来说几句 POP。

Protocol

所谓协议,就是一组属性和/或方法的定义,而如果某个具体类型想要遵守一个协议,那它需要实现这个协议所定义的所有这些内容。协议实际上做的事情不过是“关于实现的约定”。

在喵神的博客中,由两片关于协议的应用,贴出来原文的地址

面向协议编程与 Cocoa 的邂逅 (上)

面向协议编程与 Cocoa 的邂逅 (下)

TargetType

是用来设置请求基本信息的协议。

  • baseURL
  • path
  • method
  • sampleData
  • task
  • validationType
  • header

利用枚举的特性就可以比较清晰的管理,在请求头中就可以点语法来设置请求的信息。利用 class/struct 可以实现分组管理 API 的效果。

sampleData 属性赋值自定义测试数据,用来配合做请求返回结果的测试

其中validationType 属性,是对 Alamofire 返回的结果根据 statusCode 验证,也就是 Alamofire 自动化验证功能(未做详细深入研究)。这部分的内容可以参考 Alamofire 自动验证

简单实现一个 target

enum NormalRequest {
	case solution1
	case solution2
}

extension NormalRequest: TargetType {
	var baseURL: URL { return URL(String: "")! }
	var path: String { 
		switch self {
			case .solution1
				return ""
			default
				return ""
		}
	}
	...
	var headers: [String : String]? {
        return ["Content-type" : "application/x-www-form-urlencoded"]
    }
}

在实现的时候,如果用一个 case 对应一个具体的 APi 的话,实际上的代码量其实还是会很多的。所以用 case 作为分组,然后在 case 中添加关联值。

case solution1(string)

用这样的方式,对应同一个功能模块的不同请求方法就可以做到统一管理,且可以在不修改请求代码的情况下,通过添加新的请求地址来完成网络请求。在一个 case 中也可以有多个关联值,所以从这个思路出发,对应 API 的使用就会变得比较灵活。

Moya 中还有一个 MultiTarget ,它是用来支持多个 target 合并成一个。官方示例

里面还有包含关联类型,利用泛型在得到请求结果之后进行数据转模型,一个 target 对应一个模型。

MoyaProvider

MoyaProvider 就是 Moya 最顶层的请求头,对应的协议就是 MoyaProviderType。简单的设置了 target 之后,初始化一个对应 target 的 provider,不需要额外的参数

let provider = MoyaProvider<NormalRequest>()

但是直接从 init 方法开始看,会发现可以设置 7 个参数,而且都给了默认实现

public init(endpointClosure: @escaping EndpointClosure = MoyaProvider.defaultEndpointMapping,
            requestClosure:  @escaping RequestClosure = MoyaProvider.defaultRequestMapping,
	          stubClosure:     @escaping StubClosure = MoyaProvider.neverStub,
            callbackQueue: DispatchQueue? = nil,
            session: Session = MoyaProvider<Target>.defaultAlamofireSession(),
            plugins: [PluginType] = [],
            trackInflights: Bool = false) {

        self.endpointClosure = endpointClosure
        self.requestClosure = requestClosure
        self.stubClosure = stubClosure
        self.session = sessio
        self.plugins = plugins
        self.trackInflights = trackInflights
        self.callbackQueue = callbackQueue
    }

MoyaProvider+Defaults 文件里,一共只有三个方法,都是通过 extension MoyaProvider 扩展实现。里面对应的就是 init 方法中用到的 endpointClosure ,requestClosure,manager 三者的 .default 默认实现方法。

EndpointClosure

这里是从 Target → Endpoint 的映射。默认实现代码如下

final class func defaultEndpointMapping(for target: Target) -> Endpoint {
        return Endpoint(
            url: URL(target: target).absoluteString,
            sampleResponseClosure: { .networkResponse(200, target.sampleData) },
            method: target.method,
            task: target.task,
            httpHeaderFields: target.headers
        )
    }

在 init 方法里,利用的是逃逸闭包,所以返回的是这个函数。

Endpoint 还有两个实例方法,用来修改请求头数据和参数类型,返回的都是新的一个 Endpoint 对象

open func adding(newHTTPHeaderFields: [String: String]) -> Endpoint { ... }
open func replacing(task: Task) -> Endpoint { ... }

在 endpoint 这里第一次出现了修改请求头的方法和替换参数。在官方文档中是这么写的

Note that we can rely on the existing behavior of Moya and extend – instead of replace – it. The adding(newHttpHeaderFields:) function allows you to rely on the existing Moya code and add your own custom values.

请注意,我们可以依靠Moya的现有行为来扩展它,而不是替换它。 add(newHttpHeaderFields :)函数使您能够依赖现有的Moya代码并添加自己的自定义值。

let endpointClosure = { (target: MyTarget) -> Endpoint in
    let defaultEndpoint = MoyaProvider.defaultEndpointMapping(for: target)

    // Sign all non-authenticating requests
    switch target {
    case .authenticate:
        return defaultEndpoint
    default:
        return defaultEndpoint.adding(newHTTPHeaderFields: ["AUTHENTICATION_TOKEN": GlobalAppStorage.authToken])
    }
}
let provider = MoyaProvider<GitHub>(endpointClosure: endpointClosure)

可以理解为这两个修改方法,是搭配在原有的基础上进行修改的时候使用。

RequestClosure

从 Endpoint → URLRequest ,根据 Endpoint 的数据,生成一个 URLRequest。这个闭包作为一个过渡

public typealias RequestClosure = (Endpoint, @escaping RequestResultClosure) -> Void

生成一个 URLRequest,默认实现如下

final class func defaultRequestMapping(for endpoint: Endpoint, closure: RequestResultClosure) {
        do {
            let urlRequest = try endpoint.urlRequest()
            closure(.success(urlRequest))
        } catch MoyaError.requestMapping(let url) {
            closure(.failure(MoyaError.requestMapping(url)))
        } catch MoyaError.parameterEncoding(let error) {
            closure(.failure(MoyaError.parameterEncoding(error)))
        } catch {
            closure(.failure(MoyaError.underlying(error, nil)))
        }
    }

Session

这里的默认实现,是一个有着基本配置的 Alamofire.Session 对象。

对于 startRequestsImmediately ,文档中有这么解释

There is only one particular thing: since construct an Alamofire.Request in AF will fire the request immediately by default, even when "stubbing" the requests for unit testing. Therefore in Moya, startRequestsImmediately is set to false by default.

也就是之前每次构造出一个 request之后,默认情况下就是立刻执行。所以在 Moya 中设置为 false

final class func defaultAlamofireSession() -> Session {
      let configuration = URLSessionConfiguration.default
      configuration.headers = .default
      return Session(configuration: configuration, startRequestsImmediately: false)
}

Plugins

插件,Moya 一个十分特色的特性。对应的协议是 PluginType

在初始化方法中,参数的类型是个插件数组,可以一次传入多个插件。插件协议里的方法如下:

	/// Called to modify a request before sending.
    func prepare(_ request: URLRequest, target: TargetType) -> URLRequest

    /// Called immediately before a request is sent over the network (or stubbed).
    func willSend(_ request: RequestType, target: TargetType)

    /// Called after a response has been received, but before the MoyaProvider has invoked its completion handler.
    func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType)

    /// Called to modify a result before completion.
    func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError>

各个方法调用的时机

Stub

在 target 中的 sampleData,可以是自己提供测试数据,用来不通过实际网络请求来模拟返回的数据结果。在 provider 初始化中,stubClosure 闭包是用来设置是否需要模拟测试

  • .never
  • .immediate
  • .delayed(seconds: TimeInterval)

在 Moya 中对于数据测试还有更多的选择,在 Endpoint 中的 sampleResponseClosure 还可以设置模拟各种错误

  • .networkResponse(Int, Data)
  • .response(HTTPURLResponse, Data)
  • .networkError(NSError)

那么 stub 分别可以在三个地方设置自己的配置,下面使用官方的例子举例

// 1. 设置测试数据
public var sampleData: Data {
    switch self {
    case .userRepositories(let name):
        return "[{\\"name\\": \\"Repo Name\\"}]".data(using: String.Encoding.utf8)!
    }
}
// 是否需要,且如何响应测试数据
let stubbingProvider = MoyaProvider<GitHub>(stubClosure: MoyaProvider.immediatelyStub)

// 以怎么样的响应数据返回
let customEndpointClosure = { (target: APIService) -> Endpoint in
    return Endpoint(url: URL(target: target).absoluteString,
                    sampleResponseClosure: { .networkResponse(401 , /* data relevant to the auth error */) },
                    method: target.method,
                    task: target.task,
                    httpHeaderFields: target.headers)
}

let stubbingProvider = MoyaProvider<GitHub>(endpointClosure: customEndpointClosure, stubClosure: MoyaProvider.immediatelyStub)

发送请求

provider.request(.post("")){(result) in 
	swtich result {
	case let .success(response):
		break
	case let .fail(_):
		break
	}
}

在 Moya 中 request 方法是一个统一的请求入口。只需要在方法中配置需要的参数,包括需要对应生成的请求地址,请求参数等通过枚举类型,十分清晰的分类和管理。利用 . 语法生成对应的枚举,然后依次生成 endpoint,URLRequest等。

方法中就可以发现,在这个方法里,还可以再次修改 callbackQueue (至于怎么用这个 callbackQueue 还没有见到相关的文档)

@discardableResult
    open func request(_ target: Target,
                      callbackQueue: DispatchQueue? = .none,
                      progress: ProgressBlock? = .none,
                      completion: @escaping Completion) -> Cancellable {

    let callbackQueue = callbackQueue ?? self.callbackQueue
    return requestNormal(target, callbackQueue: callbackQueue, progress: progress, completion: completion)
}

这个方法,做了一些生成请求时的通用处理。

在 init 方法中,不仅可以传入对应的参数,还有一个目的是持有这些属性。

初始化请求需要的闭包

func requestNormal(_ target: Target, callbackQueue: DispatchQueue?, progress: Moya.ProgressBlock?, completion: @escaping Moya.Completion) -> Cancellable {}

方法的一开始,定义了三个常量

let endpoint = self.endpoint(target)
let stubBehavior = self.stubClosure(target)
let cancellableToken = CancellableWrapper()

因为 init 时参数都是默认实现,所以第一句代码生成 endpoint 用的是 Moya 库里的默认实现方法。

如果不是自定义实现的 endpointClosure ,这里会调用的是 defaultEndpointMapping 方法,返回 一个 Endpoint 对象

如果初始化时传入了自己定义的 endpoint 映射闭包,那么就会以自定义的方法执行

open func endpoint(_ token: Target) -> Endpoint {
      return endpointClosure(token)
}

cancellableToken 是遵循 Cancellable 协议的类 CancellableWrapper 的对象。

这个协议就只有两行代码,是否已经取消的属性和取消请求的方法。而且这个方法作用就是将属性的值改为 true

/// A Boolean value stating whether a request is cancelled.
var isCancelled: Bool { get }

/// Cancels the represented request.
func cancel()

这里就是插件 process 的执行位置,对每个插件都执行一次。插件实现的 process 的方法对 result 的所有改动,最后都通过 completion 返回。

let pluginsWithCompletion: Moya.Completion = { result in
	let processedResult = self.plugins.reduce(result) { $1.process($0, target: target) }
	completion(processedResult)
}

字面上理解,就是真正执行请求的下一步了。这个闭包,是在 endpoint → URLRequest 方法执行完成后的闭包

let performNetworking = { (requestResult: Result<URLRequest, MoyaError>) in
	// 先判断这个请求是否取消,是则返回错误类型为 cancel 的错误提示数据
    if cancellableToken.isCancelled {
    self.cancelCompletion(pluginsWithCompletion, target: target)
        return
    }

    var request: URLRequest!

    switch requestResult {
    case .success(let urlRequest):
        request = urlRequest
    case .failure(let error):
        pluginsWithCompletion(.failure(error))
        return
    }

    // Allow plugins to modify request
	// 插件执行 prepare
    let preparedRequest = self.plugins.reduce(request) { $1.prepare($0, target: target) }
	// 定义返回结果闭包,这里返回的是请求返回的数据映射成了 Result
    let networkCompletion: Moya.Completion = { result in
        if self.trackInflights {
          ....
        } else {
		// 使用上面的闭包,通知所有插件,且返回结果
        pluginsWithCompletion(result)
        }
    }
		// 这一步就是执行请求的下一步了,将所有参数继续传递
    cancellableToken.innerCancellable = self.performRequest(target, request: preparedRequest, callbackQueue: callbackQueue, progress: progress, completion: networkCompletion, endpoint: endpoint, stubBehavior: stubBehavior)
}

接下去的就是将上面定义好的两个闭包,传入到 requestClosure 闭包中

endpoint 生成 URLRequest,然后请求完成之后执行 performNetworking 闭包内的代码

requestClosure(endpoint, performNetworking)

在这个方法中,有两段根据 trackInflights 属性的代码。

查阅一些资料之后,发现很少提及这个属性的解释,根据代码逻辑可以看出来,这是是否对重复请求情况的处理。其中有一个解释是:是否要跟踪重复网络请求

#229 中,提到这个特性是指 Moya 一开始跟踪 API 请求,然后做了防止重复请求的处理。问题提出,在有些时间我们是需要重复请求一个 API 的,所以当时的做法是,#232 删除了 防止重复请求的代码。#477 中,才有了现在的这一版,对重复 API 的请求利用字典管理。

if trackInflights {
    objc_sync_enter(self)
    var inflightCompletionBlocks = self.inflightRequests[endpoint]
    inflightCompletionBlocks?.append(pluginsWithCompletion)
    self.inflightRequests[endpoint] = inflightCompletionBlocks
    objc_sync_exit(self)

    if inflightCompletionBlocks != nil {
	    // 如果存在,就是说明已经有一个已经重复的请求了
        return cancellableToken
    } else {
		// 如果不存在 key 为 endpoint 的值,则初始化一个
        objc_sync_enter(self)
        self.inflightRequests[endpoint] = [pluginsWithCompletion]
        objc_sync_exit(self)
    }
}
	....

	let networkCompletion: Moya.Completion = { result in
        if self.trackInflights {
            self.inflightRequests[endpoint]?.forEach { $0(result) }

            objc_sync_enter(self)
            self.inflightRequests.removeValue(forKey: endpoint)
            objc_sync_exit(self)
        } else {
            pluginsWithCompletion(result)
        }
    }

一个请求在 init 的时候将 trackInflights 设置为 true,那么在 Moya 中就会存储这个请求的 endpoint。在返回数据的时候,如果需要跟踪了重复请求,那么就将一次实际发送请求返回的数据,多次返回。

下一步传递参数

cancellableToken.innerCancellable = 
	self.performRequest(target, 
	request: preparedRequest, // 插件执行 prepare 闭包返回 request
	callbackQueue: callbackQueue, 
	progress: progress, 
	completion: networkCompletion, // 网络请求成功之后,将结果返回的闭包
	endpoint: endpoint, 
	stubBehavior: stubBehavior)

这个方法的内部实现,根据 switch stubBehavior 和 endpoint.task 来分别执行对应的请求方式。这里只贴除了最简单的一个

switch stubBehavior {
 case .never:
  switch endpoint.task {
    case .requestPlain, .requestData, .requestJSONEncodable, .requestCustomJSONEncodable, .requestParameters, .requestCompositeData, .requestCompositeParameters:
         return self.sendRequest(target, request: request, callbackQueue: callbackQueue, progress: progress, completion: completion)
	  ....
	}
	default:
		return self.stubRequest...
}

一般请求的实现。在这一层,就是和 Alamofire 产生联系的一层。因为我们在之前都已经生成了 urlRequest 。所以在上一步,会调用对应的 Alamofire 请求头的方法,对应生成 DataRequest,DownloadRequest,UploadRequest

func sendRequest(_ target: Target, request: URLRequest, callbackQueue: DispatchQueue?, progress: Moya.ProgressBlock?, completion: @escaping Moya.Completion) -> CancellableToken {
    let initialRequest = manager.request(request as URLRequestConvertible)
    // 文章开头提到的 target 中比较特别的属性,在这里才用到
    let validationCodes = target.validationType.statusCodes
    let alamoRequest = validationCodes.isEmpty ? initialRequest : initialRequest.validate(statusCode: validationCodes)
  return sendAlamofireRequest(alamoRequest, target: target, callbackQueue: callbackQueue, progress: progress, completion: completion)
}

在接下去就是 Alamofire 的内容了。

实际请求

在这里一共有四个对应不同的请求方法

  • sendUploadMultipart
  • sendUploadFile
  • sendDownloadRequest
  • sendRequest

这几个方法最后都调用的是 sendAlamofireRequest 这个通用的方法

func sendAlamofireRequest<T>(_ alamoRequest: T, target: Target, callbackQueue: DispatchQueue?, progress progressCompletion: Moya.ProgressBlock?, completion: @escaping Moya.Completion) -> CancellableToken where T: Requestable, T: Request { }

这个方法做的事

  1. 插件通知 willsend
let plugins = self.plugins
plugins.forEach { $0.willSend(alamoRequest, target: target) }

  1. 初始化 progress 闭包
var progressAlamoRequest = alamoRequest
let progressClosure: (Progress) -> Void = { progress in
    let sendProgress: () -> Void = {
        progressCompletion?(ProgressResponse(progress: progress))
    }

    if let callbackQueue = callbackQueue {
        callbackQueue.async(execute: sendProgress)
    } else {
        sendProgress()
    }
}
//  转换成对应的请求头
if progressCompletion != nil {
    switch progressAlamoRequest {
    ···
    }
}

  1. 封装请求结果
//封装请求结果,
let result = convertResponseToResult(response, request: request, data: data, error: error)
// 方法的定义可以看出封装前后的数据格式
public func convertResponseToResult(_ response: HTTPURLResponse?, request: URLRequest?, data: Data?, error: Swift.Error?) ->
    Result<Moya.Response, MoyaError> { }

  1. 返回结果闭包,包括发送插件的 didReceive,传输的进度
let completionHandler: RequestableCompletion = { response, request, data, error in
    let result = convertResponseToResult(response, request: request, data: data, error: error)
    // Inform all plugins about the response
    plugins.forEach { $0.didReceive(result, target: target) }
    if let progressCompletion = progressCompletion {
        switch progressAlamoRequest {
        case let downloadRequest as DownloadRequest:
            progressCompletion(ProgressResponse(progress: downloadRequest.progress, response: result.value))
        case let uploadRequest as UploadRequest:
            progressCompletion(ProgressResponse(progress: uploadRequest.uploadProgress, response: result.value))
        case let dataRequest as DataRequest:
            progressCompletion(ProgressResponse(progress: dataRequest.progress, response: result.value))
        default:
            progressCompletion(ProgressResponse(response: result.value))
        }
    }
    completion(result)
}

  1. 执行请求。
progressAlamoRequest = progressAlamoRequest.response(callbackQueue: callbackQueue, completionHandler: completionHandler)

progressAlamoRequest.resume()
// 返回的就是一个 CancellableToken 对象
return CancellableToken(request: progressAlamoRequest)

Completion

在请求返回结果里,根据 Result 来判断请求成功和失败

public typealias Completion = (_ result: Result<Moya.Response, MoyaError>) -> Void

MoyaError 是自定义的错误类型枚举,继承自 Swift.Error

public enum MoyaError: Swift.Error { }

请求失败了时候,通过返回 response 获取回应的状态码

let code = (error as! MoyaError).response?.statusCode

其他错误的情况

// 取消请求时,会调用 cancelCompletion 方法
let error = MoyaError.underlying(NSError(domain: NSURLErrorDomain, code: NSURLErrorCancelled, userInfo: nil), nil)

// defaultRequestMapping 生成 URLRequest 时候会出现失败
do {
    let urlRequest = try endpoint.urlRequest()
    closure(.success(urlRequest))
} catch MoyaError.requestMapping(let url) {
    closure(.failure(MoyaError.requestMapping(url)))
} catch MoyaError.parameterEncoding(let error) {
    closure(.failure(MoyaError.parameterEncoding(error)))
} catch {
    closure(.failure(MoyaError.underlying(error, nil)))
}
// convertResponseToResult 将请求返回的结果封装 Result 失败的时候
switch (response, data, error) {
    ...
    case let (.some(response), _, .some(error)):
    let response = Moya.Response(statusCode: response.statusCode, data: data ?? Data(), request: request, response: response)
    let error = MoyaError.underlying(error, response)
    return .failure(error)
    ...

swift 中 defer { } 的作用是: defer 所声明的 block 会在当前代码(当前 scope 退出的时候调用)执行退出后被调用

NSRecursiveLock 可以在同一个线程获取多次,而不造成死锁的问题。

private let lock: NSRecursiveLock = NSRecursiveLock()
var willSend: ((URLRequest) -> Void)? {
        get {
            lock.lock(); defer { lock.unlock() }
            return internalWillSend
        }

        set {
            lock.lock(); defer { lock.unlock() }
            internalWillSend = newValue
        }
    }
fileprivate var lock: DispatchSemaphore = DispatchSemaphore(value: 1)

    public func cancel() {
        _ = lock.wait(timeout: DispatchTime.distantFuture)
        defer { lock.signal() }
        guard !isCancelled else { return }
        isCancelled = true
    }

关于面向协议的设计结构

在平时开发的时候,和协议相关用得比较的多应该就是 PluginType 和 TargetType

插件协议理解起来就很简单,在请求方法了已经设置好了调用时机。在请求方法的时候添加对应的插件,那么在请求过程中就会依次执行每个插件对应时机的实现。这个就能体现出面向协议编程。我们在业务代码里调用协议的方法,而不用考虑作为一个具体的对象。任何实现协议的对象,只需要关注协议方法的实现即可。

替换 Alamofire?

Moya 作为一个网络抽象,而且是面向协议。那么从实现角度来说,如果底层要换一个网络请求实现的库,应该是对之前的代码基本没有影响的。 如果需要替换底层实现的库,那么会需要改动哪些地方呢?

那么Moya 和 Alamofire 之间的联系都出现在哪些地方? Moya 和 Alamofire 之间的联系主要集中在 Moya+Alamofire.swift 这个文件中。如果需要替换,几乎只要把别名里的 Alamofire相关的属性替换掉,就可以完成底层请求的替换。

参考链接

如何更深入使用Moya

Moya的设计之道