Previous navigation:
Alamofire source learning directory collection
I just changed my job recently, so I was a little busy, so I didn’t have time to study. I have time today, so I can write.
Introduction to the
Before looking at the module function to see the source code implementation, in fact, will be more abstract. To learn the source code of the third party framework, the easier way to start is to run through the simplest usage, and then follow the call stack in the code to see his internal implementation, with the call of the code, can better help understand the principle of the framework.
1. Simple GET request HTML
First to the simplest, send a get request under Baidu’s home page:
AF.request("http://www.baidu.com").responseString(completionHandler: {
resp in
switch resp.result {
case let .success(str):
debugPrint("request success: \(str)")
case let .failure(err):
debugPrint("request fail: \(err)")}})Copy the code
Request success will print out a lot of copy, is baidu home page HTML string, can write this tuo string file, suffix set to. HTML, you can open to view the Baidu home page.
There are only three things in the outer layer:
- AF
- request()
- responseString()
1. AF
As mentioned before, AF is actually a singleton of Session.default in Alamofire, and Session is the core of the entire Alamofire
// in Alamofire.swift
public let AF = Session.default
Copy the code
2. request()
Hold Command + CTRL and left-click on request() to jump to the implementation of request in Session:
Ps: Call this request() method request() number one because there are so many request methods defined in the Session that it’s easy to get confused
//in Session.swift
// Method 1
open func request(_ convertible: URLConvertible.method: HTTPMethod = .get,
parameters: Parameters? = nil.encoding: ParameterEncoding = URLEncoding.default,
headers: HTTPHeaders? = nil.interceptor: RequestInterceptor? = nil.requestModifier: RequestModifier? = nil) -> DataRequest {
let convertible = RequestConvertible(url: convertible,
method: method,
parameters: parameters,
encoding: encoding,
headers: headers,
requestModifier: requestModifier)
return request(convertible, interceptor: interceptor)
}
Copy the code
As can be seen, the URL string we input is passed as URLConvertible protocol type, which can be regarded as the protocol abstraction of THE URL type. An asURL method needs to be implemented to generate the REQUESTED URL. Alamofire implements this protocol with the built-in extension string:
// in URLConvertible+URLRequestConvertible.swift
extension String: URLConvertible {
/// Returns a `URL` if `self` can be used to initialize a `URL` instance, otherwise throws.
///
/// - Returns: The `URL` initialized with `self`.
/// - Throws: An `AFError.invalidURL` instance.
public func asURL(a) throws -> URL {
guard let url = URL(string: self) else { throw AFError.invalidURL(url: self)}return url
}
}
Copy the code
Alamofire throws an Error by using the throws exception. In fact, after getting used to “Throws”, you will find that it is more comfortable than “Result”. Throws () throws () throws () throws () throws () throws (); Throws with Result.
So, in request() number one, there are six other parameters, but they are all set to default values, so you don’t have to pass them in, just use the default values, so if you’re interested in these parameters, you can see the previous article.
No 1 in the request () method implementation, using the parameters of the incoming call RequestEncodableConvertible () method creates a convertible local variables, type is a simple structure, AsURLRequest () ¶ Implement the URLRequestConvertible protocol type, similar to the above URLConvertible protocol type, which is an abstraction of the URLRequest type. There are also built-in extensions to implement the change protocol.
After generating the convertible local variable, return returns, calling request() 2, and continuing to create the DataRequest using the convertible local variable + the interceptor protocol object with the incoming parameters. Jump into the method again and see the implementation. There are three methods with the same name, and we need to determine which one we skipped:
Judgment skills:
- First the second method, shown on line 294, is the method we’re currently in, exclusion
- The third method, in line 340, is a similar implementation to the method in line 294, except that the encoder parameter is not used, and the first parameter convertible is both URLConvertible protocol type. But the request() 2 method we’re going to call passes in an argument of type URLRequestConvertible protocol type, which is also excluded
- So the first method I’m gonna go in and see the first parameter convertible type URLRequestConvertible protocol type, ok, so that’s the correct request() method number two
Secondary forcing: Methods with the same name are common in Swift. The worst offenders are protocol methods: a protocol has a method A, and then 10 classes implement the method change, so when you call method A somewhere and click on the implementation, you’ll see 11 prompts. Ha ha da. If you can’t decide which one to call, you’ll have the odd problem of an implementation change that doesn’t take effect. To find a method, on the one hand, look at the code when calling, according to the type of the caller and the type of the input parameter, if the caller or the input parameter is a protocol type, GG, you need to run the code, when running to this line, look at the caller and the specific type of the parameters, to determine which method to go to.
Take a look at the implementation of request() method 2:
// in Session.swift
open func request(_ convertible: URLRequestConvertible.interceptor: RequestInterceptor? = nil) -> DataRequest {
let request = DataRequest(convertible: convertible,
underlyingQueue: rootQueue,
serializationQueue: serializationQueue,
eventMonitor: eventMonitor,
interceptor: interceptor,
delegate: self)
perform(request)
return request
}
Copy the code
Similar to request() 1, but with an incoming convertible + interceptor object, and the Session’s rootQueue (callback execution queue), serializationQueue (response resolution queue), EventMonitor (event listener object) and itself as a RequestDelegate object build the DataRequest object (the initialization method here is a bit more complicated, see the previous Request article for more on initialization). Then perform() to process the created DataRequest object and return the request object. Note that even after return, resume() has not been called to send the request. The sending operation of the request is called in response related processing.
CMD + CTRL = perform(); / / Execute (); / / execute (); / / Execute ();
// in Session.swift
func perform(_ request: Request) {
rootQueue.async {
guard !request.isCancelled else { return }
self.activeRequests.insert(request)
self.requestQueue.async {
// Leaf types must come first, otherwise they will cast as their superclass.
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
It’s a little bit more and more, line by line,
- The method of processing request is executed in rootQueue, which is a serial queue. Most of the internal preprocessing of request in Alamofire is executed in this queue.
- If the request is cancelled, return it directly. Because the queue is serial, it is possible that the request was already marked cancelled when it was ready to process it
- If the Request is not cancelled, add it to the activeRequests array
- It is then sent to the requestQueue for special processing of different Request types. This requestQueue can be specified externally; if not, the default is rootQueue.
Our call is DataRequest, so take a look at the performDataRequest() method implementation:
// in Session.swift
func performDataRequest(_ request: DataRequest) {
dispatchPrecondition(condition: .onQueue(requestQueue))
performSetupOperations(for: request, convertible: request.convertible)
}
Copy the code
It’s very simple, just check to see if you’re in the requestQueue, and then continue to call performSetupOperations(), and bring out the URLRequestConvertible type in the DataRequest, Because the first argument to the performSetupOperations() method called is the Request superclass, and the convertible argument is only available in four subclasses.
So when we’re doing a request, we’re doing DataRequest the same way DataStreamRequest is called, and DownloadRequest and UploadRequest have to do some special work on the upload part, the download part, so it’s a little bit different.
Proceed to the performSetupOperations() implementation:
// in Session.swift
func performSetupOperations(for request: Request.convertible: URLRequestConvertible) {
dispatchPrecondition(condition: .onQueue(requestQueue))
let initialRequest: URLRequest
do {
initialRequest = try convertible.asURLRequest()
try initialRequest.validate()
} catch {
rootQueue.async { request.didFailToCreateURLRequest(with: error.asAFError(or: .createURLRequestFailed(error: error))) }
return
}
rootQueue.async { request.didCreateInitialURLRequest(initialRequest) }
guard !request.isCancelled else { return }
guard let adapter = adapter(for: request) else {
rootQueue.async { self.didCreateURLRequest(initialRequest, for: request) }
return
}
adapter.adapt(initialRequest, for: self) { result in
do {
let adaptedRequest = try result.get()
try adaptedRequest.validate()
self.rootQueue.async {
request.didAdaptInitialRequest(initialRequest, to: adaptedRequest)
self.didCreateURLRequest(adaptedRequest, for: request)
}
} catch {
self.rootQueue.async { request.didFailToAdaptURLRequest(initialRequest, withError: .requestAdaptationFailed(error: error)) }
}
}
}
Copy the code
It’s a little bit long, and I’ll talk about that in a previous article on sessions. Here’s a quick summary:
- Build the original URLRequest object A
- Perform a valid row check on A
- Inform Request that a has been initialized and pass A to Request for internal processing
- If there are Request adapters in the Request, call the adapters one by one to process A
- After the processing is complete, inform the Request that the adaptation is complete
- Finally, the didCreateURLRequest() method is called, and the performSetupOperations() method completes
Then continue to the didCreateURLRequest() method:
// in Session.swift
func didCreateURLRequest(_ urlRequest: URLRequest.for request: Request) {
dispatchPrecondition(condition: .onQueue(rootQueue))
request.didCreateURLRequest(urlRequest)
guard !request.isCancelled else { return }
let task = request.task(for: urlRequest, using: session)
requestTaskMap[request] = task
request.didCreateTask(task)
updateStatesForTask(task, request: request)
}
Copy the code
The core operation here: Call the request task(for:,using:) method to create an URLSessionTask. This is the task that will actually send the request, and then store this task in the Session’s requestTaskMap dictionary with key being request. This can be used at any time according to the request to find its corresponding task.
Then call the updateStatesForTask() method to update the state of the request based on the status of the task. This method is called in several places, and the call here, because it’s a newly created task, doesn’t actually do anything, it just breaks.
Note: Although URLSessionTask is created, the task’s resume() method is not actually called. In other words, the request has not been sent yet, and the task of sending the request is executed when a request first calls the response method. If you turn off the startRequestsImmediately property of the Session, you need to manually call the resume() method on the Request object to start sending requests.
AF has created an URLSessionTask to send the Request, created a Request object, and returned the Request object. Then we can make various calls to the Request object. Including methods like validate(), you can also start sending requests by calling the response() method. In this case, we are using the String format to receive the response.
3. responseString()
Request is created above and ready to send the Request. It starts from this method. Personally, the processing of response is relatively abstract here, because each call to response is not simply sent to task by calling resume(), but uses one closure after another, and saves the logic package related to the response in the closure. And then call those closures when appropriate, and then append them while you call them, which is crazy…
It all starts with the responseString() method… Notice that when you go into the method implementation there are also two implementations, and one is the DownloadRequest implementation, so don’t get it wrong.
// in ResponseSerialization.swift
@discardableResult
public func responseString(queue: DispatchQueue = .main,
dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor,
encoding: String.Encoding? = nil.emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = StringResponseSerializer.defaultEmptyRequestMethods,
completionHandler: @escaping (AFDataResponse<String- > >)Void) -> Self {
response(queue: queue,
responseSerializer: StringResponseSerializer(dataPreprocessor: dataPreprocessor,
encoding: encoding,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods),
completionHandler: completionHandler)
}
Copy the code
As you can see, there is also a bunch of arguments, but they all have default values, including: callback call queue (main queue), data preprocessor (string encoding preprocessor), text encoding, etc. The only argument that must be passed in is the completed callback closure.
Notice that this method returns type Self, which means that you can continue to make response calls to it, a Request can call response an infinite number of times, and you’ll see why that works when you see how response() works.
The implementation of the method is simple, just a further call to the response() method, passing the required parameters through. Continue to this method, also notice the interference of the method of the same name under DownloadRequest.
Here comes the core!! This method, which contains the logic to process the data after the Request is completed, but this logic is a pain in the ass, so instead of executing it right away, you write all the logic, you put it in the closure, you put it in the Request, and then when the Request is complete, you call the mandatory, so you can see in the closure implementation, Response is used to get the response header, self.data is used to get the response data, and self.metrics is used to get the metrics of the request.
When you first look at the implementation here, you might wonder: Isn’t the request not sent? Shouldn’t all three of these optional types be nil? That’s right! When we first go into this response method, all three of these properties are nil, but a whole bunch of the processing logic is actually in a closure, and when we actually execute these methods, it’s when the request completes, so all three of these properties have a value. The details of how to handle responses are not covered here, but you can check out the previous response and Parsing article.
Here’s a key design concept for this method – how do you implement chained calls
// in ResponseSerialization.swift
@discardableResult
public func response<Serializer: DataResponseSerializerProtocol> (queue: DispatchQueue = .main,
responseSerializer: Serializer.completionHandler: @escaping (AFDataResponse<Serializer.SerializedObject- > >)Void)
-> Self {
appendResponseSerializer {
//...
// A bunch of processing logic
}
return self
}
Copy the code
So, once we’ve created the Request, the responseString method returns type Self, and we can call responseString sequentially. So if you look at the simplified implementation of the response method above, and return Self, you can see that the appendResponseSerializer is simply called, and a large closure is passed in, and return Self. Because of the way the closure is written, This makes sense at first glance, but you can see that the appendResponseSerializer() method accepts a large lump of closure parameters and then look at the internal implementation.
Go to the appendResponseSerializer() method:
// in Request.swift
func appendResponseSerializer(_ closure: @escaping() - >Void) {
$mutableState.write { mutableState in
// Add the array
mutableState.responseSerializers.append(closure)
// If the request is complete, mark it to continue
if mutableState.state = = .finished {
mutableState.state = .resumed
}
// If the response parsing is complete, adjust the parsing method
if mutableState.responseSerializerProcessingFinished {
underlyingQueue.async { self.processNextResponseSerializer() }
}
// If it is not in the complete state, and can be converted to the continue state
if mutableState.state.canTransitionTo(.resumed) {
// Ask if the Session needs to start immediately. If so, call resume() immediately.
underlyingQueue.async { if self.delegate?.startImmediately = = true { self.resume() } }
}
}
}
Copy the code
Ps: Push the button: mutableState is just a struct wrapped around some thread-safe property
See, instead of executing the closure immediately when the method starts calling, the closure is stored in the responseSerializers array to delay the call based on the current state of the Request:
- If the current Request is marked finished, it indicates that the Request has completed and the response is being executed again. The Request is marked as resumed, indicating that the execution is underway. The subsequent logic does not send the Request, but directly calls the parsing callback.
- Detection under responseSerializerProcessingFinished field, this field is used to mark the currently executing a parsing callback is finished, if finished, , you can manually call it processNextResponseSerializer () method to perform the next callback closures. If not, you do not need to call it manually. Because the parsed logic checks itself to perform the next parse. Recursively…
- This is because only initialized and suspended can be changed to resume. This means that the task needs to call the resume method to send the request/continue the request. The resume() method must be automatically called when the request’s startImmediately setting is true. Otherwise, resume() needs to be called manually to send the request.
This answers the question: When is the resume() method called to send the Request after the Request has been created?
2. Request JSON
The principle is similar… Free repair
3. Upload the request
Same as above
conclusion
To summarize the process:
- You can create a custom Session to send requests, or you can directly use the default AF singleton Session to send requests
- External calls to the request() method, passing in URLConvertible, create the request object
- Internally start by creating the URLRequestConvertible object with URLConvertible
- Use URLRequestConvertible to create a Request (which is the object returned to the outside), save it in Session, and start preprocessing the Request
- You create an initial URLRequest, you preprocess it with a preprocessor, and there are ways to notify the Request before and after preprocessing, process changes, to notify event listeners, so if you look at the EventMonitor protocol, there’s a whole bunch of lifecycle call-back events.
- The preprocessing is complete and the Request object is returned
- The response series of methods are called externally, in which the response is handled
- Internally, processing closures are created for the raw response data
- The response is parsed first (and the parsing time is also recorded)
- Retries that failed to resolve
- Resolve the completion callback passed in successfully from the external execution
- Append the closure to the responseSerializers array in the Request
- Check if the current Request is complete, and if so, mark it as executing
- Tests if the current Request had finished all the response under the resolution, if it is, is performed manually processNextResponseSerializer () method to continue responseSerializers parsing the closure of the array
- Check if the current Request needs to send a Request, and if so, call the resume() method to send the Request
- Internally, processing closures are created for the raw response data
It is because of the logic of calling response method in the third step that we can continuously make response serial calls to a Request after it has been created. Although the request is sent only once, the response data can be parsed multiple times, each time independently.
Can be called individually or chained:
let stringHandle: (AFDataResponse<String- > >)Void = {
resp in
// This is string parsing
}
let jsonHandle: (AFDataResponse<Any- > >)Void = {
resp in
// This is json parsing
}
let req = AF.request("http://*****")
req.responseJSON(completionHandler: jsonHandle)
req.responseString(completionHandler: stringHandle).responseJSON(completionHandler: jsonHandle)
req.responseString(completionHandler: stringHandle)
Copy the code
Happy batch ~
supplement
After understanding the processing principle of response, other similar processing of request’s validate() check, etc., are also similar to the principle, by creating a closure, capture variables, and then save the closure, until when to execute, the idea is perfect, but the implementation is really not good to understand.
This is also the thinking of functional programming: use closures to capture local variables and delay execution of closures to extend the life cycle of local variables. This kind of thinking is the basis of RxSwift’s awesome framework, but it’s too abstract. It’s too hard to understand.
This is reflected in the code:
- Ordinary write code: write logic, run up to here, line by line execution, smooth, comfortable ~ ~
- Functional programming: write a bunch of logic, put it in a closure, run, here it is, oops, save the closure, don’t execute it, and then at some point later, remember I have a saved closure. Take out the execution, debug up will be very uncomfortable, line by line debugging will find that the next code directly skipped a lot of direct function return?? And then you go down, and then at some point it’s pulled out of an array of closures, and it jumps back to the lump… Asher…
All of the above are personal understanding ~ unavoidable mistakes, if there are mistakes, please comment to point out ~ thank you very much ~