RainedAllNight‘s Blog

源码简析-Moya

字数统计: 2k阅读时长: 9 min
2020/07/20 Share

Moya-version:14.0

如图是Moya/14.0版本的项目结构,在14.0之前Moya使用了自定义的Result包,此版本改为使用了原生的Result包,除此之外还引用了Alamofire这个网络请求库,这篇文章我们将聚焦于Moya本身,简单分析分析Moay的项目源码。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
provider = MoyaProvider<GitHub>()
provider.request(.zen) { result in
switch result {
case let .success(moyaResponse):
let data = moyaResponse.data
let statusCode = moyaResponse.statusCode
// do something with the response data or statusCode
case let .failure(error):
// this means there was a network failure - either the request
// wasn't sent (connectivity), or no response was received (server
// timed out). If the server responds with a 4xx or 5xx error, that
// will be sent as a ".success"-ful response.
}
}

一个简单的请求如上,它由一个provider发起,入参为GitHub.zen一个遵守了TargetType协议的枚举。以这两条线开始,我们回到项目源码展开分析。

Target&Task

TargetType

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



public protocol TargetType {

/// The target's base `URL`.
var baseURL: URL { get }

/// The path to be appended to `baseURL` to form the full `URL`.
var path: String { get }

/// The HTTP method used in the request.
var method: Moya.Method { get }

/// Provides stub data for use in testing.
var sampleData: Data { get }

/// The type of HTTP task to be performed.
var task: Task { get }

/// The type of validation to perform on the request. Default is `.none`.
var validationType: ValidationType { get }

/// The headers to be used in the request.
var headers: [String: String]? { get }
}

TargetType定义了MoyaProvider所必要的一些字段,也就是一个请求的基本入参。
完美提现了依赖倒置原则,后续的使用有点依赖注入的内味儿

Task

顾名思义即任务,Task定义了请求数据的传输方式以及编码方式,比如

1
2
3
4
5
6
7
8
9

/// A requests body set with encoded parameters.
case requestParameters(parameters: [String: Any], encoding: ParameterEncoding)

/// A "multipart/form-data" upload task.
case uploadMultipart([MultipartFormData])

/// A file download task to a destination.
case downloadDestination(DownloadDestination)

MoyaProvider

一个请求的发起者provider,是一个遵守了MoyaProviderType协议的类

1
2
3
4
5
6
7
8
9
public protocol MoyaProviderType: AnyObject {

associatedtype Target: TargetType

/// Designated request-making method. Returns a `Cancellable` token to cancel the request later.
func request(_ target: Target, callbackQueue: DispatchQueue?, progress: Moya.ProgressBlock?, completion: @escaping Moya.Completion) -> Cancellable
}

open class MoyaProvider<Target: TargetType>: MoyaProviderType

它的request方法定义了一个请求的几个基本元素

  • Target - 一个请求所需要的基本信息
  • callbackQueue - 回调所在的队列, 默认是主队类(由下层框架Alamofire决定)
  • progress - 请求进度的回调
  • completion - 请求完成的回调
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// Initializes a provider.
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 = session
self.plugins = plugins
self.trackInflights = trackInflights
self.callbackQueue = callbackQueue
}

###
MoyaProvider的初始化方法,除了定义一些上面request方法的入参之外,还包含了以下一些属性配置

  • endpointClosure
  • requestClosure
  • stubClosure
  • session
  • plugins
  • trackInflights

endpointClosure

1
2
/// Closure that defines the endpoints for the provider.
public typealias EndpointClosure = (Target) -> Endpoint

这个个闭包将会在每次请求时被执行. 它负责返回一个 Endpoint 实例对象,用来配置Moya.

大多数情况下, 这个闭包仅仅直接从target,method和task转化为一个 Endpoint 实例对象. 它将在每次request时被执行, 你可以做任何你想要的. 比如, 你可以在里面测试网络错误,如超时、设置header. 以下是Moya提供的默认实现

1
2
3
4
5
6
7
8
9
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
)
}

我们来看下Endpoint这个对象里做了什么,最主要的就是下面这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public func urlRequest() throws -> URLRequest {
guard let requestURL = Foundation.URL(string: url) else {
throw MoyaError.requestMapping(url)
}

var request = URLRequest(url: requestURL)
request.httpMethod = method.rawValue
request.allHTTPHeaderFields = httpHeaderFields

switch task {
case .requestPlain, .uploadFile, .uploadMultipart, .downloadDestination:
return request
case .requestData(let data):
request.httpBody = data
return request
.....
}

这个方法主要做了以下几件事

1.Init URLRequest

2.设置request的url、method、header

3.根据不同的Task设置request的body

可以说一个请求的几个关键参数都在在这里设置的,此外Endpoint还添加了一些添加header、替换task、支持hash等方法

requestClosure

1
2
3
4
5
/// Closure that decides if and what request should be performed.
public typealias RequestResultClosure = (Result<URLRequest, MoyaError>) -> Void

/// Closure that resolves an `Endpoint` into a `RequestResult`.
public typealias RequestClosure = (Endpoint, @escaping RequestResultClosure) -> Void

这个closure主要作用是通过endpoint获取到URLRequest对象(有异常则继续对外抛出异常),我们可以在发送请求前在此对当前这个urlRequest做一些修改动作,比如设置timeoutInterval(超时时间)等,以下是默认的defaultRequestMapping

1
2
3
4
5
6
7
8
9
10
11
12
13

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)))
}
}

stubClosure

1
2
/// Closure that decides if/how a request should be stubbed.
public typealias StubClosure = (Target) -> Moya.StubBehavior

这个closure主要是用来配置是否以及如何使用mock的响应数据,一般用于单元测试时使用

session

Alamofire中定义的 Session对象,Moya只负责请求的组织,具体如何请求的交给底层请求库,默认使用的是Alamofire

plugins

1
2
3
4
5
6
7
8
9
10
11
12
13
public protocol 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>
}

插件的方法在请求之前和之后会被调用(类似于hook了一个request的生命周期),我们可以在这边做很多事,比如在prepare中修改request,添加一个HUDPlugin,在请求发起和结束后显示HUD,添加一个LogPlugin,打印request以及response等等。

trackInflights

表示是跟踪正在发送的请求, 如果为true, 正在发送的请求将被加入inflightRequests这个字典里, 默认为false

1
2
3
public let trackInflights: Bool

open internal(set) var inflightRequests: [Endpoint: [Moya.Completion]] = [:]

以上就是MoyaProvider类中的主要内容,当然其中的很多对象的定义都放到了单独的类(符合单一原则),比如

  • Endpoint.swift
  • MoyaProvider+Defaults.swift(定义defaultRequestMapping/defaultEndpointMapping等)
  • Plugin.swift
  • Moya+Alamofire.swift(关联Alamofire,主要是一些命名空间的重定义和功能的转接)
  • RequestTypeWrapper(RequestType包装器)
  • ValidationType(状态码过滤)

对外的request方法并没有展示多少细节,相关的实现都放在了internal的requestNormal中, 我们来看看这个方法的具体实现细节

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
61
62
63
64
65
66
func requestNormal(_ target: Target, callbackQueue: DispatchQueue?, progress: Moya.ProgressBlock?, completion: @escaping Moya.Completion) -> Cancellable {
// 通过target + endpointClosure 初始化 获得 endpoint对象
let endpoint = self.endpoint(target)
// 通过target + stubClosure 初始化 获得 stubBehavior对象
let stubBehavior = self.stubClosure(target)
获取request cancellableToken, 用来后续cancel request
let cancellableToken = CancellableWrapper()

// 遍历plugin,在completion之前 process response
let pluginsWithCompletion: Moya.Completion = { result in
let processedResult = self.plugins.reduce(result) { $1.process($0, target: target) }
completion(processedResult)
}
//如果支持追踪请求,那就讲请求以endpoint: [completion]的形式存入字典中, 并返回cancel token
if trackInflights {
lock.lock()
var inflightCompletionBlocks = self.inflightRequests[endpoint]
inflightCompletionBlocks?.append(pluginsWithCompletion)
self.inflightRequests[endpoint] = inflightCompletionBlocks
lock.unlock()

if inflightCompletionBlocks != nil {
return cancellableToken
} else {
lock.lock()
self.inflightRequests[endpoint] = [pluginsWithCompletion]
lock.unlock()
}
}

// 定义 RequestResultClosure
let performNetworking = { (requestResult: Result<URLRequest, MoyaError>) in
//如果请求已经取消,走cancel completion
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
}

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

self.lock.lock()
self.inflightRequests.removeValue(forKey: endpoint)
self.lock.unlock()
} else {
pluginsWithCompletion(result)
}
}
// 调用performRequest根据不同Task send request,plugin的遍历、body的设置,statusCodes的过滤等等一些操作都在后续的流程中
cancellableToken.innerCancellable = self.performRequest(target, request: request, callbackQueue: callbackQueue, progress: progress, completion: networkCompletion, endpoint: endpoint, stubBehavior: stubBehavior)
}

requestClosure(endpoint, performNetworking)

return cancellableToken
}

上面我们提到stubClosure时提到Moya提供了stub的功能,它是使用单独的请求方法,以做stub时的一些处理操作,比如针对plugin和cancel的处理

1
open func stubRequest(_ target: Target, request: URLRequest, callbackQueue: DispatchQueue?, completion: @escaping Moya.Completion, endpoint: Endpoint, stubBehavior: Moya.StubBehavior) -> CancellableToken {//do some thing}

总结

总的来说,Moya+Alamofire的组合有点类似于Android中的Retrofit+OKHttp,Moya在这里扮演的是一个网络抽象层的角色,就源码来说并不复杂,其中设计的思路值得学习学习。

CATALOG
  1. 1. Target&Task
    1. 1.1. TargetType
    2. 1.2. Task
  2. 2. MoyaProvider
    1. 2.1. endpointClosure
    2. 2.2. requestClosure
    3. 2.3. stubClosure
    4. 2.4. session
    5. 2.5. plugins
    6. 2.6. trackInflights
  3. 3. 总结