A review,

In the previous source exploration, the SessionManager manages the creation of Request and SessionDelegate objects and binds them to task; Request is responsible for the configuration of Request parameters and the creation of different tasks. Create methods that connect external (sending Request objects) and TaskDelegate. The TaskDelegate delegate event is handed over by the SessionDelegate task. Summary:

The purpose of the above processing is to do hierarchical processing of tasks, so that the structure is clear.

RequestAdapter- RequestAdapter

There is also a protocol RequestAdapter under the Request file. Create the call in Manager. As follows:

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)
    }
}
Copy the code

Context, adapter is not initialized, why? Here’s how it’s defined:

/// A type that can inspect and optionally adapt a `URLRequest` in some manner if necessary.
public protocol RequestAdapter {
    /// Inspects and adapts the specified `URLRequest` in some manner if necessary and returns the result.
    ///
    /// - parameter urlRequest: The URL request to adapt.
    ///
    /// - throws: An `Error` if the adaptation encounters an error.
    ///
    /// - returns: The adapted `URLRequest`.
    func adapt(_ urlRequest: URLRequest) throws -> URLRequest
}
Copy the code

A protocol internally defines a method that somehow checks and ADAPTS to URLRequest, essentially telling us to follow the protocol and implement the method as needed. Why implement it? In fact, it is not difficult to guess, since the type is given to us, it must be convenient for us to set parameters, such as token, device, vision and other public parameters, in fact, can be set, so let’s try it out.

1. Add public parameters

class MyAdapter: RequestAdapter{
    func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
             var request = urlRequest
        request.setValue("hibotoken", forHTTPHeaderField: "token")
        request.setValue("device", forHTTPHeaderField: "iOS")
        request.setValue("vision", forHTTPHeaderField: "1.0.0")
        return request
    }
}
Copy the code

Set up the adapter and send a request:

let urlStr = "http://onapp.yahibo.top/public/? s=api/test/list"
let url = URL.init(string: urlStr)!
Alamofire.SessionManager.default.adapter = MyAdapter(a)Alamofire.request(url,method: .post,parameters: ["page":"1"."size":"20"]).responseJSON {
    (response) in
    switch response.result{
    case .success(let json):
        print("json:\(json)")
        break
    case .failure(let error):
        print("error:\(error)")
        break}}Copy the code
  • inSessionManagerDefined in theadapterObject, to which an implementation is assignedadaptMethod subclass object here before the requestadaptIf the request header is set, run the following command to check whether the public parameters are added successfully:

Once added, parameters under development can be managed using this method alone.

2. Redirect

class redireatAdapter: RequestAdapter{
    func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
        let newURLRequest = URLRequest.init(url: URL.init(string: "http://onapp.yahibo.top/public/? s=api/test")!return newURLRequest
    }
}
Copy the code

Directly modify the original request address and redirect to another address.

Why add a public parameter, or redirect?

The code trace traces to the final use position as follows:

func task(session: URLSession, adapter: RequestAdapter? , queue: DispatchQueue) throws -> URLSessionTask {
    do {
        let urlRequest = try self.urlRequest.adapt(using: adapter)
        return queue.sync { session.dataTask(with: urlRequest) }
    } catch {
        throw AdaptError(error: error)
    }
}
func adapt(using adapter: RequestAdapter?) throws -> URLRequest {
    guard let adapter = adapter else { return self }
    return try adapter.adapt(self)}Copy the code

If the adapter does not use the URLRequest object created and set parameters, the Adapter calls the adapt method to transmit the current URLRequest object for processing.

Validate-custom validation

In the development process, it is often handled according to different status codes. For example, in the development process, a certain result needs to be defined as an error request and processed in error. In this framework, we can use validate to re-verify and define the request result. The code is as follows:

let urlStr = "http://onapp.yahibo.top/public/? s=api/test/list2"
let url = URL.init(string: urlStr)!
Alamofire.request(url,method: .post,parameters: ["page":"1"."size":"20"]).responseJSON {
    (response) in
    switch response.result{
    case .success(let json):
        print("json:\(json)")
        break
    case .failure(let error):
        print("error:\(error)")
        break
    }
}.validate{ (request, response, data) -> Request.ValidationResult in
    print(response)
    guard let _ = data else {
        return .failure(NSError(domain: "There's no data.", code: 0, userInfo: nil))}guard response.statusCode == 200 else {
        return .failure(NSError(domain: "Is there some kind of mistake?", code: response.statusCode, userInfo: nil))}return .success
}
Copy the code
  • Through chained method callsvalidateAuthentication method, add authentication logic according to specific requirements
  • The returned data is null and is defined as an error message
  • statusCode ! = 200Consider the error request through the above trial to usAlamofireNow we know that both the monitoring of request progress and the validation are based on chain calls, which are convenient and quick.

RequestRetrier- Re-requests

UrlSession (_ session: urlSession, task: URLSessionTask, didCompleteWithError error: Error?) . The framework does the following in this proxy method:

if let retrier = retrier, let error = error {
    retrier.should(sessionManager, retry: request, with: error) { [weak self] shouldRetry, timeDelay in
        guard shouldRetry else { completeTask(session, task, error) ; return }
        DispatchQueue.utility.after(timeDelay) { [weak self] in
            guard let strongSelf = self else { return }
            letretrySucceeded = strongSelf.sessionManager? .retry(request) ??false
            if retrySucceeded, let task = request.task {
                strongSelf[task] = request
                return
            } else {
                completeTask(session, task, error)
            }
        }
    }
} else {
    completeTask(session, task, error)
}
Copy the code
  • When a request is wrong, judge firstretrierWhether defined if defined then calledshouldmethods
  • hereretrierIs an inheritance fromRequestRetrierClass object of the protocol

RequestRetrier

/// A type that determines whether a request should be retried after being executed by the specified session manager
/// and encountering an error.
public protocol RequestRetrier {
    /// Determines whether the `Request` should be retried by calling the `completion` closure.
    ///
    /// This operation is fully asynchronous. Any amount of time can be taken to determine whether the request needs
    /// to be retried. The one requirement is that the completion closure is called to ensure the request is properly
    /// cleaned up after.
    ///
    /// - parameter manager: The session manager the request was executed on.
    /// - parameter request: The request that failed due to the encountered error.
    /// - parameter error: The error encountered when executing the request.
    /// - parameter completion: The completion closure to be executed when retry decision has been determined.
    func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion)
}
Copy the code
  • withRequestAdapterAgain, you need to define classes and implement methods

Create a subclass and inherit the protocol as follows:

class MyRetrier: RequestRetrier{
    var count: Int = 0
    func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) {
        if count<3 {
            completion(true.2)
             count+ =1
        }else{
            completion(false.2)}}}Copy the code
  • Set the number of rerequests to three
  • Call the internally implemented closure, pass in a value, tell the internally whether to rerequest or abort the request
  • completionThere are two parametersshouldRetryIs whether to request,timeDelayIndicates the delay time of delayed requests. Set this parameter to 2 seconds
  • Delayed requests avoided, invalid requests

Here you can set up an error connection to send a request to try:

let urlStr = "http://onapp.yahibo.top/public/? s=api/test/list2"
let url = URL.init(string: urlStr)!
Alamofire.SessionManager.default.retrier = MyRetrier(a)Alamofire.request(url,method: .post,parameters: ["page":"1"."size":"20"]).responseJSON {
    (response) in
    switch response.result{
    case .success(let json):
        print("json:\(json)")
        break
    case .failure(let error):
        print("error:\(error)")
        break
    }
    }.validate{ (request, response, data) -> Request.ValidationResult in
        print(response)
        guard let _ = data else {
            return .failure(NSError(domain: "There's no data.", code: 10086, userInfo: nil))}if response.statusCode == 404 {
            return .failure(NSError(domain: "Password error", code: response.statusCode, userInfo: nil))}return .success
}
Copy the code
  • andRequestAdapterUse the same method, you need to configure SessionManagerretrierattribute

V. Response-response results

Alamofire processes the requested data and then returns it to us. We called responseJSON for all the above requests to obtain the final data. Let’s take a look at what is done inside responseJSON:

public func responseJSON(
    queue: DispatchQueue? = nil,
    options: JSONSerialization.ReadingOptions = .allowFragments,
    completionHandler: @escaping (DataResponse<Any>) -> Void) - >Self
{
    return response(
        queue: queue,
        responseSerializer: DataRequest.jsonResponseSerializer(options: options),
        completionHandler: completionHandler
    )
}
Copy the code

ResponseJSON is an extension of DataRequest, derived from the Request class. The response method continues to be called internally. The method is as follows:

public func response<T: DataResponseSerializerProtocol>(
    queue: DispatchQueue? = nil,
    responseSerializer: T,
    completionHandler: @escaping (DataResponse<T.SerializedObject>) -> Void) - >Self
{
    delegate.queue.addOperation {
        let result = responseSerializer.serializeResponse(
            self.request,
            self.response,
            self.delegate.data,
            self.delegate.error
        )
        var dataResponse = DataResponse<T.SerializedObject>(
            request: self.request,
            response: self.response,
            data: self.delegate.data,
            result: result,
            timeline: self.timeline
        )
        dataResponse.add(self.delegate.metrics)
        (queue ?? DispatchQueue.main).async { completionHandler(dataResponse) }
    }
    return self
}
Copy the code
  • Method serializes the request results internally
  • Encapsulates the serialized result toDataResponseIn the object

We didn’t see the familiar serialization above, so we continued to search and found the following code:

public static func serializeResponseJSON( options: JSONSerialization.ReadingOptions, response: HTTPURLResponse? , data: Data? , error: Error?)
        -> Result<Any>
    {
        guard error == nil else { return .failure(error!) }
        if let response = response, emptyDataStatusCodes.contains(response.statusCode) { return .success(NSNull()}guard let validData = data, validData.count > 0 else {
            return .failure(AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength))
        }
        do {
            let json = try JSONSerialization.jsonObject(with: validData, options: options)
            return .success(json)
        } catch {
            return .failure(AFError.responseSerializationFailed(reason: .jsonSerializationFailed(error: error)))
        }
    }
Copy the code
  • Serialization results are encapsulated toResultIn the object
  • ResultObject is finally encapsulated toDataResponseObject to manage

The response object manages all parameters in the request:

var dataResponse = DataResponse<T.SerializedObject>(
    request: self.request,
    response: self.response,
    data: self.delegate.data,
    result: result,
    timeline: self.timeline
)
Copy the code

So in the request results, we can easily get all the information we need.

Timeline- Timeline

Why is there a timeline? In network requests, we need to know exactly how long the request takes so that the front-end or back-end can optimize the processing. Here’s a look at how Alamofire’s timeline was designed.

First we can see that the task is executed in a queue:

func task(session: URLSession, adapter: RequestAdapter? , queue: DispatchQueue) throws -> URLSessionTask {
    do {
        let urlRequest = try self.urlRequest.adapt(using: adapter)
        return queue.sync { session.dataTask(with: urlRequest) }
    } catch {
        throw AdaptError(error: error)
    }
}
Copy the code

Queues are created in SessionManager, Manager really does everything. The code is as follows:

let queue = DispatchQueue(label: "org.alamofire.session-manager." + UUID().uuidString)
Copy the code
  • Set the identifier to bind to the current deviceUUID
  • This queue manages initiated tasks, independent of the timeline

Next, initialize the TaskDelegate object. As follows:

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)
    }
}
Copy the code

Initialize the TaskDelegate object by passing the task task with.data(originalTask, task) :

self.queue = {
    let operationQueue = OperationQueue()
    operationQueue.maxConcurrentOperationCount = 1
    operationQueue.isSuspended = true
    operationQueue.qualityOfService = .utility
    return operationQueue
}()
Copy the code
  • Set the maximum concurrency to 1 so that the tasks are executed sequentially
  • The initialized queue is suspended by default because the task is not started yet

1. StartTime – Records the request initiation time

Tasks are created and executed in Request as follows:

open func resume(a) {
    guard let task = task else { delegate.queue.isSuspended = false ; return }
    if startTime == nil { startTime = CFAbsoluteTimeGetCurrent() }
    task.resume()
    NotificationCenter.default.post(
        name: Notification.Name.Task.DidResume,
        object: self,
        userInfo: [Notification.Key.Task: task]
    )
}
Copy the code
  • theresumeinSessionManagerCall execution in
  • Check if the task exists and if it does continue because some tasks will be suspended, restart it
  • taskIf no, the task is complete and the queue starts to perform other tasks
  • Record the initial time of the request before starting the task, because there is a pending conditionstartTimeA short call was made

2. EndTimer – Records the request end time

init(session: URLSession, requestTask: RequestTask, error: Error? = nil) {
    self.session = session
    switch requestTask {
    case .data(let originalTask, let task):
        taskDelegate = DataTaskDelegate(task: task)
        self.originalTask = originalTask
    case .download(let originalTask, let task):
        taskDelegate = DownloadTaskDelegate(task: task)
        self.originalTask = originalTask
    case .upload(let originalTask, let task):
        taskDelegate = UploadTaskDelegate(task: task)
        self.originalTask = originalTask
    case .stream(let originalTask, let task):
        taskDelegate = TaskDelegate(task: task)
        self.originalTask = originalTask
    }
    delegate.error = error
    delegate.queue.addOperation { self.endTime = CFAbsoluteTimeGetCurrent()}}Copy the code
  • Create and classify task agents for task delivery
  • Record the end time of the task

The above code does an initialization, why say end time, because the queue is synchronous queue, the last request will be executed after the task has finished. When the request is complete, the code looks like this:

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
    if let taskDidCompleteWithError = taskDidCompleteWithError {
        taskDidCompleteWithError(session, task, error)
    } else {
        if let error = error {
            if self.error == nil { self.error = error }
            if
                let downloadDelegate = self as? DownloadTaskDelegate.let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data
            {
                downloadDelegate.resumeData = resumeData
            }
        }
        queue.isSuspended = false}}Copy the code
  • queue.isSuspended = falseRestore the queue. After the recovery, the time recording tasks mentioned above can be added to the queue for execution

InitialResponseTime – Initializes the response time

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
    if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent()}}Copy the code
  • Initialize the data response time. Different tasks have initialization methods, such as download task and upload task

4. Set the TimeLine

In response initialization, initialize the timeline:

extension DataRequest {
    @discardableResult
    public func response(queue: DispatchQueue? = nil, completionHandler: @escaping (DefaultDataResponse) -> Void) - >Self {
        delegate.queue.addOperation {
            (queue ?? DispatchQueue.main).async {
                var dataResponse = DefaultDataResponse(
                    request: self.request,
                    response: self.response,
                    data: self.delegate.data,
                    error: self.delegate.error,
                    timeline: self.timeline
                )
                dataResponse.add(self.delegate.metrics)
                completionHandler(dataResponse)
            }
        }
        return self}}Copy the code
  • The timeline is development-oriented and is therefore encapsulated toResponseIn the

Initialize the timeline and manage the previous time records uniformly:

extension Request {
    var timeline: Timeline {
        let requestStartTime = self.startTime ?? CFAbsoluteTimeGetCurrent(a)let requestCompletedTime = self.endTime ?? CFAbsoluteTimeGetCurrent(a)let initialResponseTime = self.delegate.initialResponseTime ?? requestCompletedTime
        return Timeline(
            requestStartTime: requestStartTime,
            initialResponseTime: initialResponseTime,
            requestCompletedTime: requestCompletedTime,
            serializationCompletedTime: CFAbsoluteTimeGetCurrent()}}Copy the code

Initialize the timeline, calculate the request interval, serialize the interval:

public init(
    requestStartTime: CFAbsoluteTime = 0.0,
    initialResponseTime: CFAbsoluteTime = 0.0,
    requestCompletedTime: CFAbsoluteTime = 0.0,
    serializationCompletedTime: CFAbsoluteTime = 0.0)
{
    self.requestStartTime = requestStartTime
    self.initialResponseTime = initialResponseTime
    self.requestCompletedTime = requestCompletedTime
    self.serializationCompletedTime = serializationCompletedTime
    self.latency = initialResponseTime - requestStartTime
    self.requestDuration = requestCompletedTime - requestStartTime
    self.serializationDuration = serializationCompletedTime - requestCompletedTime
    self.totalDuration = serializationCompletedTime - requestStartTime
}
Copy the code

TimeLine records the operation time points in the request process, calculates the time interval of each operation, and encapsulates it in Response after the request ends. The operation in the request is synchronized through the queue to ensure the accuracy of startTime and endTime. The other time records are set in the request broker callback.

TimeLine: