Alamofre is the well-known Swift network library on iOS. Basically, when developing Swift projects and making network requests, the first reaction is to use the Alamofire library, which should have an easy-to-use API and be easy to use. However, I have never had an in-depth understanding of the source code of Alamofire, and I am not clear about the advantages of Alamofire compared with using the native URLSession of iOS directly. Disadvantage? The purpose of this article is to answer these questions:

  1. What are the advantages and disadvantages of Alamofire compared to using URLSession directly?
  2. What additional features does Alamofre offer?
  3. How easy and extensible is the Alamofire API as a third-party library? How is it done?
  4. What good design and structure is used in Alamofire? It can be used for reference in other projects.

Simple Network request API

Since Alamofire is the encapsulation based on URLSession, the first thing we need to compare is, of course, using Alamofire and URLSession respectively to initiate a simple network request, and what is the difference between code invocation? In fact, we can simply see that, Why are so many developers using Alamofire instead of using native URLSession directly?

Using native URLSession:

guard let url = URL(string: "example.com") else { return }
        
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
		guard error == nil else { return }
		// handle data and response
}
task.resume()
Copy the code

Using Alamofire:

Alamofire.request("example.com").response { (response) in
		guard response.error == nil else { return }
		// handle data and response
}
Copy the code

As you can see, the code using Alamofire is much simpler compared to URLSession. Specific performance:

  1. There is no need to create or hold a session or task, and no need to resume() the task to trigger the request.
  2. The requested URL is invoked without checking its validity.

Since Alamofire is an encapsulation based on URLSession, that is to say, at the lowest level, Alamofire must also call URLSession to make the request. Let’s take a closer look at the code call initiated by the entire request:

As you can see, the entry to the request is alamofire.request ():

/// Alamofire.swift

@discardableResult
public func request(
    _ url: URLConvertible,
    method: HTTPMethod = .get,
    parameters: Parameters? = nil,
    encoding: ParameterEncoding = URLEncoding.default,
    headers: HTTPHeaders? = nil)
    -> DataRequest
{
    return SessionManager.default.request(
        url,
        method: method,
        parameters: parameters,
        encoding: encoding,
        headers: headers
    )
}
Copy the code

This method is only an API entry, in the actual call our SessionManager. Default. The request

/// SessionManager.swift

public static let `default`: SessionManager = {
        let configuration = URLSessionConfiguration.default
        configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders

        return SessionManager(configuration: configuration)
}()

@discardableResult
    open func request(
        _ url: URLConvertible,
        method: HTTPMethod = .get,
        parameters: Parameters? = nil,
        encoding: ParameterEncoding = URLEncoding.default,
        headers: HTTPHeaders? = nil)
        -> DataRequest
    {
        var originalRequest: URLRequest?

        do {
            originalRequest = try URLRequest(url: url, method: method, headers: headers)
            letencodedURLRequest = try encoding.encode(originalRequest! , with: parameters)return request(encodedURLRequest)
        } catch {
            return request(originalRequest, failedWith: error)
        }
    }
Copy the code

Open sessionManager. swift and see that request(_ URL: URLConvertible) convertible creates an URLRequest with an incoming argument and calls another request(URLRequest: URLRequest) :

/// SessionManager.swift

@discardableResult
    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

Originaltask. task(session: session, adapter: adapter, queue: Queue) after the URLSessionTask is created, the DataRequest is created, and the corresponding Session and URLSessionTask are held. Subsequently, the management network request is held directly through the DataRequest. Resume () and return the Request object. A network request has been sent.

Let’s review the simplicity of Alamofire calls mentioned earlier and see how Alamofire does it:

  1. There is no need to create or hold a session or task, and no need to resume() the task to trigger the request.

SessionManager is an encapsulation of URLSession. SessionManager holds the underlying URLSession object and is responsible for creating the Request object.

By using the global sessionManager. default, let the general simple request does not need to manually create URLSession, let the request call more concise: As you can see from the previous code, URLSession is held by SessionManager as the underlying object. A simple network request can be initiated by calling Alamofire.request() directly, SessionManager. Default is used to initiate requests, so users do not need to manually create a SessionManager, reducing the amount of code. However, if the consumer wants to customize the URLSession configuration parameters, it must manually rebuild a SessionManager object.

  1. The requested URL is invoked without checking its validity. Swift code is characterized by enforcing type determination to ensure security. The input parameter of urlsession.datatask (with: URL) is the URL, so when generating the URL, you need to ensure that the URL is created before proceeding with the next operation, which leads to the need for additional code to determine the processing.

In the API of Alamofire, the input is URLConvertible protocol, and the extension of String, URL and URLComponents in Alamofire complies with this protocol. Complying with URLConvertible protocol implements asURL() to produce urls, which will throw an exception when the URL cannot be generated. This type of exception is thrown to the.response callback for uniform handling.

/// Alamofire.swift

/// Types adopting the `URLConvertible` protocol can be used to construct URLs, which are then used to construct
/// URL requests.
public protocol URLConvertible {
    /// Returns a URL that conforms to RFC 2396 or throws an `Error`.
    ///
    /// - throws: An `Error` if the type cannot be converted to a `URL`.
    ///
    /// - returns: A URL or throws an `Error`.
    func asURL() throws -> URL
}
Copy the code

This is a nice way to reduce the amount of code while maintaining type-safety. In addition, URLConvertible protocol, in the development of large network request APP, can better manage the interface in the way of routing, rather than just a simple URL String, so as to improve the legibility and maintainability of interface files. We’ll talk about that later.

  • Summary: You can see that using Alamofire to send a request is much simpler than URLSession. There is no need to create multiple objects, handle URL exceptions separately, and execute resume() to the task to trigger the request.

File structure versus class structure

The previous section analyzed the implementation of a simple network request by Alamofire. Now let’s take a closer look at the overall file structure and class structure of Alamofire:

File structure

Here are all the Alamofire implementation files, and let’s briefly explain what each file does:

  • AFError. Swift encapsulates various error types, including error descriptions
  • Alamofire. Swift encapsulates a convenient way to send requests. Sending requests using Alamofire will uniformly use the global configuration of SessionManager
  • DispatchQueue+Alamofire. Swift encapsulates DispatchQueue multi-threaded calls and asyncAfter methods
  • MultipartFormData. Swift support for the MultipartFormData type. What is multipart/form-data request-nD-blog garden
  • NetworkReachabilityManager. Swift judgment network connection condition of class, based on SCNetworkReachability and SCNetworkReachabilityFlags, and can monitor the state of the network changes
  • Notifications. Swift encapsulates the notification name with the key inside the notification userInfo
  • ParameterEncoding. Swift Encodes urlRequest parameters, including different encoding modes
  • Request. Swift represents the class of network Request, including dataRequest, downloadRequest, uploadRequest and streamRequest
  • Response. Swift network request result, including DefaultDataResponse, DataResponse, DefaultDownloadResponse, DownloadResponse,
  • ResponseSerialization. Swift Response serialization, will Response serialized as result
  • Result. Swift network request Result, which is used to mark whether the request is successful
  • Servertrustpolicy. swift Class for authenticating network request certificates
  • Sessiondelegate. swift handles all callbacks to requests initiated by SessionManager
  • Sessionmanager. swift creates and manages the request classes and the underlying NSURLSession. The network request callback is handled by SessionDelegate
  • Taskdelegate. swift handles the callbacks for each individual request
  • Timeline. Swift Records the related time of the complete request life cycle
  • Swift checks whether a request has returned a successful result based on the statusCode of the request. Failure to validate will result in an associated error that can be overridden for customization

Class structure

Design patterns and code structures used for reference

Code design pattern

1. Single responsibility Principle

In Alamofire, the responsibilities of different classes are clearly divided. SessionManager is responsible for the scenario and configuration of Request, SessionDelegate is responsible for callback of Request results, and Request is responsible for creating URLSessionTask corresponding to Request. When we create our own Manager, in order to be convenient, we will directly put most of the code into the Manager and call it encapsulation, but this will lead to a lot of coupling of the Manager code, and when changing, it will affect the whole body.

2. Interface oriented programming

Alamofire, use a lot of interface, for example, URLEncoding, RequestAdapter, URLRequestConvertible, DataResponseSerializerProtocol, etc. As a third-party library, using protocols instead of inheritance increases the readability and extensibility of the code. Protocol oriented benefits:

  • In Swfit, you cannot inherit multiple classes. Protocols can inherit multiple classes.
  • The interface is more readable.
  • More componentized code, less code dependencies, better extensibility.
  • Facilitate unit testing.
  • Better separation of code responsibilities to avoid excessive concentration of responsibilities.

The function

1. Provides the implementation of network request retry.

When a request fails, the RequestRetrier is invoked to decide whether to retry the request. Revalidation of OAuth can be implemented with RequestAdapter. Retrying and Adapting

2. Chain call

Alamofire uses a lot of chained calls, such as: Request ().validate().response().downloadProgress()

When Alamofire calls a method, the method will return to the type itself. At the same time, because the request is asynchronous, the actual function code is not executed immediately, but saved in the request and called when the result is returned. For example: request().validate().response().downloadProgress()

  • Request () initiates the request, but note that the request is returned asynchronously.
  • Validate (), which validates the status of the request result, since the request has just been initiated and has not been returned, there is no way to validate the request. So calling validate() actually does this:validations.append(validationExecution)Validation methods are saved and not executed.
  • Response (), the same call is executeddelegate.queue.addOperation { }Add the action to the TaskDelegate’s queue
  • DownloadProgress (), which, when called, is assigned todataDelegate.progressHandler = (closure, queue)

As you can see, when all of the above calls are done, you are actually just sending a network request and assigning or adding all the operations that need to be done during the callback. When the request receives the data, the above operations are invoked one by one. For example, delegate.queue. IsSuspend = true at the start of the request, and delegate.queue. IsSuspend = false at the end of the request. The successful callbacks of previously added requests are executed one by one.

The advantage of chain calls is that for scenarios with a large number of callbacks, chain calls are much easier to read and do not result in code nested in a bunch of {} lines that are indistinguishable.