未验证 提交 cc183c78 编写于 作者: J Jon Shier 提交者: GitHub

Lifetime Event APIs (#3219)

* Add additional lifetime closures.

* Update formatting.

* Add documentation on the new lifetime methods.
上级 3b38a991
......@@ -309,11 +309,11 @@ AF.request(...)
Importantly, not all `Request` subclasses are able to report their progress accurately, or may have other dependencies to do so.
- For upload progress, progress can be determined in the following ways:
- By the length of the `Data` object provided as the upload body to an `UploadRequest`.
- By the length of a file on disk provided as the upload body of an `UploadRequest`.
- By the value of the `Content-Length` header on the request, if it has been manually set.
- By the length of the `Data` object provided as the upload body to an `UploadRequest`.
- By the length of a file on disk provided as the upload body of an `UploadRequest`.
- By the value of the `Content-Length` header on the request, if it has been manually set.
- For download progress, there is a single requirement:
- The server response must contain a `Content-Length` header.
- The server response must contain a `Content-Length` header.
Unfortunately there may be other, undocumented requirements for progress reporting from `URLSession` which prevents accurate progress reporting.
#### Handling Redirects
......@@ -370,11 +370,38 @@ AF.request(...)
}
```
#### A `Request`’s `URLRequest`s
#### Lifetime Values
Alamofire creates a variety of underlying values throughout the lifetime of a `Request`. Most of these are internal implementation details, but the creation of `URLRequest`s and `URLSessionTask`s are exposed to allow for direct interaction with other APIs.
##### A `Request`’s `URLRequest`s
Each network request issued by a `Request` is ultimately encapsulated in a `URLRequest` value created from the various parameters passed to one of the `Session` request methods. `Request` will keep a copy of these `URLRequest`s in its `requests` array property. These values include both the initial `URLRequest` created from the passed parameters, as well any `URLRequest`s created by `RequestInterceptor`s. That array does not, however, include the `URLRequest`s performed by the `URLSessionTask`s issued on behalf of the `Request`. To inspect those values, the `tasks` property gives access to all of the `URLSessionTasks` performed by the `Request`.
#### `URLSessionTask`s
In many ways, the various `Request` subclasses act as a wrapper for a `URLSessionTask`, presenting particular API for interacting with particular types of tasks. These tasks are made visible on the `Request` instance through the `tasks` array property. This includes both the initial task created for the `Request`, as well as any subsequent tasks created as part of the retry process, with one task per retry.
In addition to accumulating these values, every `Request` has an `onURLRequestCreation` method which calls a closure whenever a `URLRequest` is created for the `Request`. This `URLRequest` is the product of the initial parameters passed to the `Session`'s `request` method, as well as changes applied by any `RequestInterceptor`s. It will be called multiple times if the `Request` is retried and only one closure can be set at a time. `URLRequest` values cannot be modified in this closure; if you need to modify `URLRequest`s before they're issued, use a `RequestInterceptor` or compose your requests using the `URLRequestConvertible` protocol before passing them to Alamofire.
```swift
AF.request(...)
.onURLRequestCreation { request in
print(request)
}
.responseDecodable(of: SomeType.self) { response in
debugPrint(response)
}
```
##### `URLSessionTask`s
In many ways, the various `Request` subclasses act as a wrapper for a `URLSessionTask` and present specific API for interacting with the different types of tasks. These tasks are made visible on the `Request` instance through the `tasks` array property. This includes both the initial task created for the `Request`, as well as any subsequent tasks created as part of the retry process, with one task per retry.
In addition to accumulating these values, every `Request` has an `onURLSessionTaskCreation` method which calls a closure whenever a `URLSessionTask` is created for the `Request`. This clsoure will be called multiple times if the `Request` is retried and only one closure can be set at a time. The provided `URLSessionTask` *SHOULD **NOT*** be used to interact with the `task`'s lifetime, which should only be done by the `Request` itself. Instead, you can use this method to provide the `Request`'s active `task` to other APIs, like `NSFileProvider`.
```swift
AF.request(...)
.onURLSessionTaskCreation { task in
print(task)
}
.responseDecodable(of: SomeType.self) { response in
debugPrint(response)
}
```
#### Response
Each `Request` may have an `HTTPURLResponse` value available once the request is complete. This value is only available if the request wasn’t cancelled and didn’t fail to make the network request. Additionally, if the request is retried, only the *last* response is available. Intermediate responses can be derived from the `URLSessionTask`s in the `tasks` property.
......@@ -391,7 +418,7 @@ AF.request(...)
}
```
> Due to `FB7624529`, collection of `URLSessionTaskMetrics` on watchOS is currently disabled.
> Due to `FB7624529`, collection of `URLSessionTaskMetrics` on watchOS < 7 is currently disabled.
### `DataRequest`
`DataRequest` is a subclass of `Request` which encapsulates a `URLSessionDataTask` downloading a server response into `Data` stored in memory. Therefore, it’s important to realize that extremely large downloads may adversely affect system performance. For those types of downloads, using `DownloadRequest` to save the data to disk is recommended.
......
......@@ -92,8 +92,12 @@ public class Request {
var redirectHandler: RedirectHandler?
/// `CachedResponseHandler` provided to handle response caching.
var cachedResponseHandler: CachedResponseHandler?
/// Closure called when the `Request` is able to create a cURL description of itself.
var cURLHandler: ((String) -> Void)?
/// Queue and closure called when the `Request` is able to create a cURL description of itself.
var cURLHandler: (queue: DispatchQueue, handler: (String) -> Void)?
/// Queue and closure called when the `Request` creates a `URLRequest`.
var urlRequestHandler: (queue: DispatchQueue, handler: (URLRequest) -> Void)?
/// Queue and closure called when the `Request` creates a `URLSessionTask`.
var urlSessionTaskHandler: (queue: DispatchQueue, handler: (URLSessionTask) -> Void)?
/// Response serialization closures that handle response parsing.
var responseSerializers: [() -> Void] = []
/// Response serialization completion closures executed once all response serializers are complete.
......@@ -337,6 +341,10 @@ public class Request {
func didCreateURLRequest(_ request: URLRequest) {
dispatchPrecondition(condition: .onQueue(underlyingQueue))
$mutableState.read { state in
state.urlRequestHandler?.queue.async { state.urlRequestHandler?.handler(request) }
}
eventMonitor?.request(self, didCreateURLRequest: request)
callCURLHandlerIfNecessary()
......@@ -347,7 +355,8 @@ public class Request {
$mutableState.write { mutableState in
guard let cURLHandler = mutableState.cURLHandler else { return }
self.underlyingQueue.async { cURLHandler(self.cURLDescription()) }
cURLHandler.queue.async { cURLHandler.handler(self.cURLDescription()) }
mutableState.cURLHandler = nil
}
}
......@@ -358,7 +367,13 @@ public class Request {
func didCreateTask(_ task: URLSessionTask) {
dispatchPrecondition(condition: .onQueue(underlyingQueue))
$mutableState.write { $0.tasks.append(task) }
$mutableState.write { state in
state.tasks.append(task)
guard let urlSessionTaskHandler = state.urlSessionTaskHandler else { return }
urlSessionTaskHandler.queue.async { urlSessionTaskHandler.handler(task) }
}
eventMonitor?.request(self, didCreateTask: task)
}
......@@ -812,11 +827,36 @@ public class Request {
return self
}
// MARK: - Lifetime APIs
/// Sets a handler to be called when the cURL description of the request is available.
///
/// - Note: When waiting for a `Request`'s `URLRequest` to be created, only the last `handler` will be called.
///
/// - Parameter handler: Closure to be called when the cURL description is available.
/// - Parameters:
/// - queue: `DispatchQueue` on which `handler` will be called.
/// - handler: Closure to be called when the cURL description is available.
///
/// - Returns: The instance.
@discardableResult
public func cURLDescription(on queue: DispatchQueue, calling handler: @escaping (String) -> Void) -> Self {
$mutableState.write { mutableState in
if mutableState.requests.last != nil {
queue.async { handler(self.cURLDescription()) }
} else {
mutableState.cURLHandler = (queue, handler)
}
}
return self
}
/// Sets a handler to be called when the cURL description of the request is available.
///
/// - Note: When waiting for a `Request`'s `URLRequest` to be created, only the last `handler` will be called.
///
/// - Parameter handler: Closure to be called when the cURL description is available. Called on the instance's
/// `underlyingQueue` by default.
///
/// - Returns: The instance.
@discardableResult
......@@ -825,13 +865,59 @@ public class Request {
if mutableState.requests.last != nil {
underlyingQueue.async { handler(self.cURLDescription()) }
} else {
mutableState.cURLHandler = handler
mutableState.cURLHandler = (underlyingQueue, handler)
}
}
return self
}
/// Sets a closure to called whenever Alamofire creates a `URLRequest` for this instance.
///
/// - Note: This closure will be called multiple times if the instance adapts incoming `URLRequest`s or is retried.
///
/// - Parameters:
/// - queue: `DispatchQueue` on which `handler` will be called. `.main` by default.
/// - handler: Closure to be called when a `URLRequest` is available.
///
/// - Returns: The instance.
@discardableResult
public func onURLRequestCreation(on queue: DispatchQueue = .main, perform handler: @escaping (URLRequest) -> Void) -> Self {
$mutableState.write { state in
if let request = state.requests.last {
queue.async { handler(request) }
}
state.urlRequestHandler = (queue, handler)
}
return self
}
/// Sets a closure to be called whenever the instance creates a `URLSessionTask`.
///
/// - Note: This API should only be used to provide `URLSessionTask`s to existing API, like `NSFileProvider`. It
/// **SHOULD NOT** be used to interact with tasks directly, as that may be break Alamofire features.
/// Additionally, this closure may be called multiple times if the instance is retried.
///
/// - Parameters:
/// - queue: `DispatchQueue` on which `handler` will be called. `.main` by default.
/// - handler: Closure to be called when the `URLSessionTask` is available.
///
/// - Returns: The instance.
@discardableResult
public func onURLSessionTaskCreation(on queue: DispatchQueue = .main, perform handler: @escaping (URLSessionTask) -> Void) -> Self {
$mutableState.write { state in
if let task = state.tasks.last {
queue.async { handler(task) }
}
state.urlSessionTaskHandler = (queue, handler)
}
return self
}
// MARK: Cleanup
/// Final cleanup step executed when the instance finishes response serialization.
......
//
// RequestTests.swift
//
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
// Copyright (c) 2014-2020 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
......@@ -861,6 +861,29 @@ final class RequestCURLDescriptionTestCase: BaseTestCase {
XCTAssertEqual(components?.last, "\"\(urlString)\"")
}
func testGETRequestCURLDescriptionOnMainQueue() {
// Given
let url = URL.makeHTTPBinURL()
let expectation = self.expectation(description: "request should complete")
var isMainThread = false
var components: [String]?
// When
manager.request(url).cURLDescription(on: .main) {
components = self.cURLCommandComponents(from: $0)
isMainThread = Thread.isMainThread
expectation.fulfill()
}
waitForExpectations(timeout: timeout, handler: nil)
// Then
XCTAssertTrue(isMainThread)
XCTAssertEqual(components?[0..<3], ["$", "curl", "-v"])
XCTAssertTrue(components?.contains("-X") == true)
XCTAssertEqual(components?.last, "\"\(url)\"")
}
func testGETRequestCURLDescriptionSynchronous() {
// Given
let urlString = "https://httpbin.org/get"
......@@ -1122,3 +1145,39 @@ final class RequestCURLDescriptionTestCase: BaseTestCase {
.filter { $0 != "" && $0 != "\\" }
}
}
final class RequestLifetimeTests: BaseTestCase {
func testThatRequestProvidesURLRequestWhenCreated() {
// Given
let didReceiveRequest = expectation(description: "did receive task")
let didComplete = expectation(description: "request did complete")
var request: URLRequest?
// When
AF.request(URLRequest.makeHTTPBinRequest())
.onURLRequestCreation { request = $0; didReceiveRequest.fulfill() }
.responseDecodable(of: HTTPBinResponse.self) { _ in didComplete.fulfill() }
wait(for: [didReceiveRequest, didComplete], timeout: timeout, enforceOrder: true)
// Then
XCTAssertNotNil(request)
}
func testThatRequestProvidesTaskWhenCreated() {
// Given
let didReceiveTask = expectation(description: "did receive task")
let didComplete = expectation(description: "request did complete")
var task: URLSessionTask?
// When
AF.request(URLRequest.makeHTTPBinRequest())
.onURLSessionTaskCreation { task = $0; didReceiveTask.fulfill() }
.responseDecodable(of: HTTPBinResponse.self) { _ in didComplete.fulfill() }
wait(for: [didReceiveTask, didComplete], timeout: timeout, enforceOrder: true)
// Then
XCTAssertNotNil(task)
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册