Alamofire is now a must-have framework for the Swift project. This paper begins to study relevant knowledge (Alamofire version 5.4.4). First take a look at the project overview:

From the framework’s code structure, there should be quite a bit here. We will decompose the target, first study its workflow, understand the related classes, for the subsequent in-depth preparation.

By the Test case

It is recommended that when you open the project, you first go through the Tests folder and then run through all the Tests. You will be surprised. Don’t ask me how I know. It’s tears.

Note: Before you do this, you need to do two important things:

  1. Modify theTestsfolderTestHelpers.swiftIn the first50thehttpbin.orgReplace withwww.httpbin.org
  2. It’s the first one in this file220theHost.localhostReplace withHost.httpBin

This ensures that you can connect to the correct HTTP test server.

Ok, here are my test results:

There will be a variety of use cases, please fly!

The working process

In this section, we analyze a testRequestResponse test case to clarify the request flow. That is the main goal of this article.

After removing the Test code, there is only one core statement:

AF.request(url, parameters: ["foo": "bar"]).response { resp in
  response = resp
}
Copy the code

There are two main things done here:

  1. createRequest
  2. useRequestThe initiating

At the same time, it introduces several classes that we need to know:

Global instance object of the AF-session class

Session is the encapsulation of URLSession. Responsible for creating and managing requests. It also manages request queuing/interception/authentication/redirection and buffering.

Request & DataRequest

Af.request (URL, parameters: [“foo”: “bar”]) creates a DataRequest. The DataRequest inherits from Request.

Request encapsulates Request information, including Request status, progress, and event callback.

DataRequest adds the ability to receive data based on Request and store the received data in memory via mutableData.

DataResponse

DataResponse encapsulates relevant information. It is a generic structure that defines two generic data types, Success and Failure, for successful and failed requests, respectively.

The response type for the test case is AFDataResponse

is a type alias:
?>

public typealias AFDataResponse<Success> = DataResponse<Success.AFError>
Copy the code

It uses AFError as the default error type.

String them together. String them together

Okay, so that gives you an overview of the data types. Here we try to string together the whole process.

The first is the process of creating the Request.

It should be noted that requests are divided into the following types:

  1. DataRequest
  2. DataStreamRequest
  3. DownloadRequest
  4. UploadRequest

DataRequest is the most commonly used, followed by upload and download. DataStreamRequest says it has not contacted…

Each of these requests has a corresponding creation method. (Method signature is too long, I won’t post it.) Most of them have the same parameters:

  1. Convertible – Requests the corresponding URL
  2. Method – Request method
  3. Parameters – Request parameters
  4. Encoding (or encoder) – Is responsible for encoding parameters into the request
  5. Headers – Additional request header information
  6. Interceptor – an interceptor, more on this later
  7. RequestModifier – Request modifier that allows you to further modify the Request

After the collection of various parameters, can encapsulate RequestConvertible or RequestEncodableConvertible, both structure follows the URLRequestConvertible agreement, can generate URLRequest. Take a quick look at their implementation:

struct RequestConvertible: URLRequestConvertible {
    let url: URLConvertible
    let method: HTTPMethod
    let parameters: Parameters?
    let encoding: ParameterEncoding
    let headers: HTTPHeaders?
    let requestModifier: RequestModifier?
    
    func asURLRequest(a) throws -> URLRequest {
        / / / build Request
        var request = try URLRequest(url: url, method: method, headers: headers)
        // Use requestModifier to further customize the Request
        try requestModifier?(&request)
        /// encode the parameters
        return try encoding.encode(request, with: parameters)
    }
}
Copy the code
struct RequestEncodableConvertible<Parameters: Encodable> :URLRequestConvertible {
    let url: URLConvertible
    let method: HTTPMethod
    let parameters: Parameters?
    let encoder: ParameterEncoder
    let headers: HTTPHeaders?
    let requestModifier: RequestModifier?

    func asURLRequest(a) throws -> URLRequest {
        / / / build Request
        var request = try URLRequest(url: url, method: method, headers: headers)
        // Use requestModifier to further customize the Request
        try requestModifier?(&request)
        
        /// encode the parameters
        return try parameters.map { try encoder.encode($0, into: request) } ?? request
    }
}
Copy the code

Comparing the two implementations, only with respect to the subtle differences in coding, leaving a question mark here, you may have to go deep to understand the purpose of the design.

Then there is the configuration phase.

Perform (_ Request: Request) perform(_ Request: Request)

func perform(_ request: Request) {
    rootQueue.async {
        /// cancel, return directly
        guard !request.isCancelled else { return }
        /// Record the activity request
        self.activeRequests.insert(request)

        self.requestQueue.async {
            // Leaf types must come first, otherwise they will cast as their superclass.
            /// Distribute the configuration
            switch request {
            case let r as UploadRequest: self.performUploadRequest(r) // UploadRequest must come before DataRequest due to subtype relationship.
            case let r as DataRequest: self.performDataRequest(r)
            case let r as DownloadRequest: self.performDownloadRequest(r)
            case let r as DataStreamRequest: self.performDataStreamRequest(r)
            default: fatalError("Attempted to perform unsupported Request subclass: \ [type(of: request))")}}}}Copy the code

The configuration method of each version will eventually end up here:

func performSetupOperations(for request: Request.convertible: URLRequestConvertible.shouldCreateTask: @escaping() - >Bool = { true })
{
    /// condition precheck
    dispatchPrecondition(condition: .onQueue(requestQueue))

    let initialRequest: URLRequest

    do {
        /// the URLRequestConvertible method is called to create an URLRequest
        initialRequest = try convertible.asURLRequest()
        /// verify that the request is valid. The default implementation is that the GET method passing data through body is considered illegal
        try initialRequest.validate()
    } catch {
        /// Event callback: Validity verification failed
        rootQueue.async { request.didFailToCreateURLRequest(with: error.asAFError(or: .createURLRequestFailed(error: error))) }
        return
    }
    /// Event callback: the request has been created
    rootQueue.async { request.didCreateInitialURLRequest(initialRequest) }
    /// Check the status of the request again, cancel no further work
    guard !request.isCancelled else { return }
    /// The adapter that gets the Request takes into account the individual Request level and Session level
    guard let adapter = adapter(for: request) else {
        guard shouldCreateTask() else { return }
        / / / 1.?
        rootQueue.async { self.didCreateURLRequest(initialRequest, for: request) }
        return
    }
    
    This logic is used by default, unless interceptor is configured
    
    let adapterState = RequestAdapterState(requestID: request.id, session: self)
    /// adapt Request and perform subsequent processing
    adapter.adapt(initialRequest, using: adapterState) { result in
        do {
            /// Obtain the adapted Request
            let adaptedRequest = try result.get()
            try adaptedRequest.validate()
            /// Event callback: request has been adapted
            self.rootQueue.async { request.didAdaptInitialRequest(initialRequest, to: adaptedRequest) }

            guard shouldCreateTask() else { return }
            / / 1.?
            self.rootQueue.async { self.didCreateURLRequest(adaptedRequest, for: request) }
        } catch {
            /// Event callback: request adaptation failed
            self.rootQueue.async { request.didFailToAdaptURLRequest(initialRequest, withError: .requestAdaptationFailed(error: error)) }
        }
    }
}
Copy the code

Here is the final configuration:

func didCreateURLRequest(_ urlRequest: URLRequest.for request: Request) {
    dispatchPrecondition(condition: .onQueue(rootQueue))
    /// Event callback: URLRequest has been created
    request.didCreateURLRequest(urlRequest)
    // cancel and return directly
    guard !request.isCancelled else { return }
    / / / to create a task
    let task = request.task(for: urlRequest, using: session)
    // record request->task mapping
    requestTaskMap[request] = task
    /// Event callback: Task has been created
    request.didCreateTask(task)

    updateStatesForTask(task, request: request)
}
Copy the code

You can see that the main task is created here. Finally, the configuration of task. (this is really the last time to configure 😂) :

func updateStatesForTask(_ task: URLSessionTask.request: Request) {
    dispatchPrecondition(condition: .onQueue(rootQueue))

    request.withState { state in
        switch state {
        case .initialized, .finished:
            // Do nothing.
            break
        case .resumed:
            task.resume()
            rootQueue.async { request.didResumeTask(task) }
        case .suspended:
            task.suspend()
            rootQueue.async { request.didSuspendTask(task) }
        case .cancelled:
            // Resume to ensure metrics are gathered.
            task.resume()
            task.cancel()
            rootQueue.async { request.didCancelTask(task) }
        }
    }
}
Copy the code

The task synchronizes the Request status to task.

Finally, the process of creating a Request is complete! 🤣

The next step is to resume the request. request.response { resp in xxx }

Here the response method definitions in ResponseSerialization. Swift file DataRequest an extension of:

@discardableResult
public func response(queue: DispatchQueue = .main, completionHandler: @escaping (AFDataResponse<Data? - > >)Void) -> Self {
    /// Add a response serializer, and the Closure is for the serialization operation
    / / / add the serializer in system callback ` urlSession (_ : task: didCompleteWithError:) after ` will be executed
    appendResponseSerializer {
        // build result directly with data and error
        // Start work that should be on the serialization queue.
        let result = AFResult<Data? >(value:self.data, error: self.error)
        // End work that should be on the serialization queue.

        self.underlyingQueue.async {
            /// Build the Response object
            let response = DataResponse(request: self.request,
                                        response: self.response,
                                        data: self.data,
                                        metrics: self.metrics,
                                        serializationDuration: 0,
                                        result: result)
            /// Event callback: response parsed
            self.eventMonitor?.request(self, didParseResponse: response)
            /// Add a callback after all serializers are complete to call this method back out
            self.responseSerializerDidComplete { queue.async { completionHandler(response) } }
        }
    }

    return self
}
Copy the code

There are several variations of the response method to request different data types:

  1. Parse the request data toStringtheresponseString
  2. Parse the request data toJSONtheresponseJSON
  3. Parse request data into modelresponseDecodable
  4. .

These different versions essentially add different parsers using the appendResponseSerializer(_:) method.

func appendResponseSerializer(_ closure: @escaping() - >Void) {
    /// $mutableState is the use of the property wrapper, in this case for thread safety. The specific method of use will be discussed later.
    $mutableState.write { mutableState in
        // use the array record serializer
        mutableState.responseSerializers.append(closure)
        If the resumed serializer has already been called, another serializer needs to reset its state to resumed
        if mutableState.state = = .finished {
            mutableState.state = .resumed
        }
        // if all the previous serializers have been executed, the new serializers need to be processed
        if mutableState.responseSerializerProcessingFinished {
            underlyingQueue.async { self.processNextResponseSerializer() }
        }
        /// resume the request
        if mutableState.state.canTransitionTo(.resumed) {
            underlyingQueue.async { if self.delegate?.startImmediately = = true { self.resume() } }
        }
    }
}
Copy the code

conclusion

Ok, so today we have a general overview of Alamofire’s request flow and how a request goes from creation to initiation. I will follow this idea and analyze the details of the process step by step. Stay tuned. Thanks for reading!