iOS [YTKNetwork Source Parsing]

Posted by electricshoe on Fri, 24 May 2019 19:00:52 +0200

Even useless for iOS developers YTKNetwork Framework I've probably seen it, I've heard it.It is an open source network request framework for ape library technical teams, encapsulating AFNetworking internally.It instantiates each request, manages its life cycle, and can manage multiple requests.

Before explaining the source code formally, I'll talk about the architecture and design patterns used by the framework.I always feel that if I have a certain understanding of architecture and design, it will help to understand the source code.

1. Architecture

First figure:


YTKRequest schema diagram

Here is a brief explanation:

  1. The YTKNetwork framework instantiates each request, YTKBaseRequest is the base class for all request classes, and YTKRequest is its subclass.So if we want to send a request, we need to create and instantiate a custom request class (CustomRequest) that inherits from YTKRequest and send the request.
  2. YTKNetworkAgent is a single instance that manages all request classes (such as CustomRequest).When CustomRequest sends a request, it places itself in a dictionary held by YTKNetworkAgent and lets it manage itself.
  3. We say that YTKNetwork encapsulates AFNetworking. In fact, YTKNetworkAgent encapsulates AFNetworking, which is responsible for sending requests for AFNetworking and processing callbacks for AFNetworking.So if we want to replace a third-party network request library, we can do it here.YTKRequest is more responsible for processing the cache.
  4. The specific functions of YTKNetworkConfig and YTKPriviate are not described at this time and will be given later.

OK, now that we know the relationship between classes and classes in YTKNetwork and the approximate functions of key classes, I'll tell you why YTKNetwork uses this relationship and what the benefits of using it are.

2. Design Mode

The YTKNetwork framework uses a Command Pattern design pattern.

First, look at the definition of the command mode:

Command mode encapsulates requests as objects so that different requests, queues, or logs can be used to parameterize other objects.Command mode also supports undoable operations.
From: <Head First Design Mode

Take a look at the class diagram for command mode:



Command Mode Class Diagram.png

The meaning of the picture in English:

English Chinese
Command Abstract command class
ConcreteCommand Implementation class (subclass) of command class
Invoker caller
Receiver Command Receiver (Executor)
Client Client

Describe in detail:

  1. The essence of command mode is to encapsulate commands, separating the responsibility for issuing and executing commands.
  2. Command mode allows the requesting party and the receiving party to be independent, so that the requesting party does not have to know the interface of the receiving party, how the request was received, whether the operation was executed, when it was executed, and how it was executed.

Maybe it's a bit abstract, so here's one <Head First Design Mode For example, a guest orders in a restaurant:

  1. You wrote your order and handed it to the waiter.
  2. The waiter gives the order to the chef.
  3. When the chef has prepared the dish, he gives it to the waiter.
  4. Finally the waiter gives you the dish.

Here, a command is like an order, and you are the originator of the command.Your order (order) is given to the executor (chef) of the order through the waiter (caller).
So you don't know who made this dish or how to do it. All you do is issue commands and accept the results.And for restaurants, chefs are free to change, and you probably don't know anything about it.On the other hand, a cook just needs to do a good job of preparing the dish, and doesn't need to think about who ordered it.

Combining the class diagram of the command pattern above with examples of restaurant meals, let's clarify the functions within YTKNetwork

scene Command ConcreteCommand Invoker Receiver Client
Restaurant Blank Order Order filled in with the name of the dish Waiter cook Guest
YTKNetwork YTKBaseRequest CustomRequest YTKNetworkAgent AFNetworking ViewController/ViewModel

As you can see, the implementation of the command mode by YTKNetwork conforms well to its design standards by separating the originator and recipient of the request (separated by the caller) and allowing us to change the recipient at any time.

In addition, because requests are encapsulated, we can manage either a single request, multiple requests at the same time, or even the sending of a beeper request.As for multiple requests, we can also imagine that in a restaurant, you can remember to eat something else during the meal, such as snacks, drinks, etc. You can fill out multiple orders (and of course write them together) and hand them to the waiter.

Believe that at this point, you should have a good understanding of the design and architecture of YTKNetwork. Now let's go to the real source code parsing. Let's combine its code to see how YTKNetwork implements and manages network requests.

3. Source Code Parsing

Before really explaining the source code, let me elaborate on the responsibilities of each class:

3.1 Introduction of Responsibility

Class name Duty
YTKBaseRequest Base class for all request classes.Holds important data such as NSURLSessionTask instance, responseData, responseObject, error, provides methods related to network requests that need subclasses to implement, handles callback agents and block s, and commands YTKNetworkAgent to initiate network requests.
YTKRequest A subclass of YTKBaseRequest.Responsible for the processing of the cache: query the cache before request; write to the cache after request.
YTKNetworkConfig Accessed by YTKRequest and YTKNetworkAgent.Responsible for global configuration of all requests, such as baseUrl and CDNUrl.
YTKNetworkPrivate Provides JSON validation, appVersion, and other helpful methods; adds some classifications to YTKBaseRequest.
YTKNetworkAgent The class that actually initiates the request.Responsible for making requests, ending them, and holding a dictionary to store the requests being executed.
YTKBatchRequest Bulk requests can be made, holding an array to hold all request classes.This array is traversed after the request is executed to initiate the request, and if one of the requests fails to return, the group request is determined to have failed.
YTKBatchRequestAgent Responsible for managing multiple instances of YTKBatchRequest, holding an array to hold YTKBatchRequest.Supports adding and deleting YTKBatchRequest instances.
YTKChainRequest Chained requests can be made, holding an array to hold all request classes.The next request cannot be initiated until the end of a request. If one of the requests fails to return, the request chain is determined to have failed.
YTKChainRequestAgent Responsible for managing multiple instances of YTKChainRequestAgent, holding an array to hold YTKChainRequest.Supports adding and deleting YTKChainRequest instances.

OK, now that you know the responsibility assignment within YTKNetwork, let's first look at what YTKNetwork does from the entire process of a single request (configuration, initiation, completion).

3.2 Single Request

3.21 Configuration of a single request

Official Tutorials It is recommended that we set parameters such as baseUrl and cdnUrl in the AppDelegate.m file for the requested global configuration.

- (BOOL)application:(UIApplication *)application 
   didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
   YTKNetworkConfig *config = [YTKNetworkConfig sharedConfig];
   config.baseUrl = @"http://yuantiku.com";
   config.cdnUrl = @"http://fen.bi";
}

If we need to create a new registration request, we need to create a class, RegisterApi, that inherits from the registration interface of YTKRequest and configure the request parameters:

// RegisterApi.h
#import "YTKRequest.h"

@interface RegisterApi : YTKRequest

- (id)initWithUsername:(NSString *)username password:(NSString *)password;

@end


// RegisterApi.m
#import "RegisterApi.h"

@implementation RegisterApi {
    NSString *_username;
    NSString *_password;
}

//Initialize by passing in two parameter values
- (id)initWithUsername:(NSString *)username password:(NSString *)password {
    self = [super init];
    if (self) {
        _username = username;
        _password = password;
    }
    return self;
}

//Addresses that need to be stitched to baseUrl
- (NSString *)requestUrl {
    // "http://www.yuantiku.com" is set in YTKNetworkConfig, where only the remaining address information of the domain name is removed
    return @"/iphone/register";
}

//Request method, someone is GET
- (YTKRequestMethod)requestMethod {
    return YTKRequestMethodPOST;
}

//Requestor
- (id)requestArgument {
    return @{
        @"username": _username,
        @"password": _password
    };
}

@end

Now that we know how to configure global parameters and parameters for a request, let's look at how a single request was initiated.

3.22 Initiation of a single request

Or the just registered API, which, after instantiation, can be launched by calling the start WithCompletionBlockWithSuccess:failure method directly:

//LoginViewController.m
- (void)loginButtonPressed:(id)sender {
    NSString *username = self.UserNameTextField.text;
    NSString *password = self.PasswordTextField.text;
    if (username.length > 0 && password.length > 0) {
        RegisterApi *api = [[RegisterApi alloc] initWithUsername:username password:password];
        [api startWithCompletionBlockWithSuccess:^(YTKBaseRequest *request) {
            // You can use self directly here
            NSLog(@"succeed");
        } failure:^(YTKBaseRequest *request) {
            // You can use self directly here
            NSLog(@"failed");
        }];
    }
}

The callback above is in the form of a block, and YTKNetwork also supports callbacks from proxies:

//LoginViewController.m
- (void)loginButtonPressed:(id)sender {
    NSString *username = self.UserNameTextField.text;
    NSString *password = self.PasswordTextField.text;
    if (username.length > 0 && password.length > 0) {
        RegisterApi *api = [[RegisterApi alloc] initWithUsername:username password:password];
        api.delegate = self;
        [api start];
    }
}

- (void)requestFinished:(YTKBaseRequest *)request {
    NSLog(@"succeed");
}

- (void)requestFailed:(YTKBaseRequest *)request {
    NSLog(@"failed");
}

There are two points to note:

  1. The startWithCompletionBlockWithSuccess:failure method (or start method) must be called on a custom request class (RegisterApi) to actually initiate the request.
  2. In cases where both callback proxy and callback block are set, the callback proxy method is called back first, and then the callback block is moved back.

Knowing how the YTKRequest request was originated externally, let's start with the startWithCompletionBlockWithSuccess:failure method and see what YTKNetwork does:

First come to the YTKBaseRequest class (because it first defined the method):

//YTKBaseRequest.m
//Pass in successful and failed block s and save them
- (void)startWithCompletionBlockWithSuccess:(YTKRequestCompletionBlock)success
                                    failure:(YTKRequestCompletionBlock)failure {
    //Save successful and failed callback block s for future callbacks
    [self setCompletionBlockWithSuccess:success failure:failure];
    //Initiate Request
    [self start];
}

//Save successful and failed block s
- (void)setCompletionBlockWithSuccess:(YTKRequestCompletionBlock)success
                              failure:(YTKRequestCompletionBlock)failure {
    self.successCompletionBlock = success;
    self.failureCompletionBlock = failure;
}

After saving the successful and failed block s, the start method is called, and you come to the YTKRequest class (note that although YTKBaseRequest also implements the start method, since the YTKRequest class is a subclass of it and also implements the start method, the start method of the YTKRequest class is the first one here):

//YTKRequest.m
- (void)start {

    //1. If Cache->Request is ignored
    if (self.ignoreCache) {
        [self startWithoutCache];
        return;
    }

    //2. If there are download incomplete files - > requests
    if (self.resumableDownloadPath) {
        [self startWithoutCache];
        return;
    }

    //3. Failed to get cache - > Request
    if (![self loadCacheWithError:nil]) {
        [self startWithoutCache];
        return;
    }

    //4. At this point, you know you will get the available cache and you can call back directly (because you will get the available cache, so you must call the successful block s and proxies)
    _dataFromCache = YES;

    dispatch_async(dispatch_get_main_queue(), ^{

        //5. Action before callback
        //5.1 Cache Processing
        [self requestCompletePreprocessor];

        //5.2 Users can do pre-callback actions here
        [self requestCompleteFilter];

        YTKRequest *strongSelf = self;

        //6. Execute callback
        //6.1 Agent Requesting Completion
        [strongSelf.delegate requestFinished:strongSelf];

        //6.2 Successful block request
        if (strongSelf.successCompletionBlock) {
            strongSelf.successCompletionBlock(strongSelf);
        }

        //7. Set both successful and failed block s to nil to avoid circular references
        [strongSelf clearCompletionBlock];
    });
}

As we have said before, YTKRequest is responsible for processing the cache, so in the start method above, it does the query and check of the cache before requesting:

  • If the cache is ignored or the cache acquisition fails, call the startWithoutCache method (refer to 1-3) to initiate the request.
  • If the cache is successfully fetched, a direct callback is made (refer to 4-7).

Let's look at the implementation of each step:

  1. The ignoreCache property is set manually by the user, and if the user forces the cache to be ignored, the request is sent directly regardless of whether the cache exists or not.
  2. resumableDownloadPath is the breakpoint download path. If the path is not empty, indicating that there is an unfinished download task, send a request directly to continue downloading.
  3. loadCacheWithError: The method verifies the success of loading the cache (a return value of YES indicates that the cache can be loaded; and vice versa) and looks at the implementation:
//YTKRequest.m
- (BOOL)loadCacheWithError:(NSError * _Nullable __autoreleasing *)error {

    // If the cache time is less than 0, it is returned (cache time defaults to -1, which requires manual user settings in seconds)
    if ([self cacheTimeInSeconds] < 0) {
        if (error) {
            *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorInvalidCacheTime userInfo:@{ NSLocalizedDescriptionKey:@"Invalid cache time"}];
        }
        return NO;
    }

    // Is there cached metadata, if not, returns an error
    if (![self loadCacheMetadata]) {
        if (error) {
            *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorInvalidMetadata userInfo:@{ NSLocalizedDescriptionKey:@"Invalid metadata. Cache may not exist"}];
        }
        return NO;
    }

    // Cached and then validated
    if (![self validateCacheWithError:error]) {
        return NO;
    }

    // Cached and valid, then verify if it can be removed
    if (![self loadCacheData]) {
        if (error) {
            *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorInvalidCacheData userInfo:@{ NSLocalizedDescriptionKey:@"Invalid cache data"}];
        }
        return NO;
    }

    return YES;
}

First, let's talk about what metadata is: metadata refers to data of data, which describes some of the characteristics of cached data itself: version number, cache time, sensitive information, and so on, which will be described in more detail later.

Let's look at the above method for getting metadata about caching: the loadCacheMetadata method

//YTKRequest.m
- (BOOL)loadCacheMetadata {

    NSString *path = [self cacheMetadataFilePath];
    NSFileManager * fileManager = [NSFileManager defaultManager];
    if ([fileManager fileExistsAtPath:path isDirectory:nil]) {
        @try {
            //Deserialize the file that was saved on disk after serialization to the property cacheMetadata of the current object
            _cacheMetadata = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
            return YES;
        } @catch (NSException *exception) {
            YTKLog(@"Load cache metadata failed, reason = %@", exception.reason);
            return NO;
        }
    }
    return NO;
}

cacheMetadata (YTKCacheMetadata) is a property of the current reqeust class that holds cache metadata.
The YTKCacheMetadata class is defined in the YTKRequest.m file:

//YTKRequest.m
@interface YTKCacheMetadata : NSObject<NSSecureCoding>

@property (nonatomic, assign) long long version;
@property (nonatomic, strong) NSString *sensitiveDataString;
@property (nonatomic, assign) NSStringEncoding stringEncoding;
@property (nonatomic, strong) NSDate *creationDate;
@property (nonatomic, strong) NSString *appVersionString;

@end

It describes the version number, sensitive information, creation time, app version of the cache, and supports serialization, which can be saved to disk.
Therefore, the purpose of the loadCacheMetadata method is to deserialize the previously serialized cache metadata information and assign it to its own cacheMetadata property.

Now that the cached metadata has been acquired and assigned to its cacheMetadata attribute, the next step is to verify that the information in the metadata meets the requirements one by one, in validateCacheWithError:

//YTKRequest.m
- (BOOL)validateCacheWithError:(NSError * _Nullable __autoreleasing *)error {

    // Is it greater than expiration time
    NSDate *creationDate = self.cacheMetadata.creationDate;
    NSTimeInterval duration = -[creationDate timeIntervalSinceNow];
    if (duration < 0 || duration > [self cacheTimeInSeconds]) {
        if (error) {
            *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorExpired userInfo:@{ NSLocalizedDescriptionKey:@"Cache expired"}];
        }
        return NO;
    }

    // Whether the version number of the cache matches
    long long cacheVersionFileContent = self.cacheMetadata.version;
    if (cacheVersionFileContent != [self cacheVersion]) {
        if (error) {
            *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorVersionMismatch userInfo:@{ NSLocalizedDescriptionKey:@"Cache version mismatch"}];
        }
        return NO;
    }

    // Compliance of sensitive information
    NSString *sensitiveDataString = self.cacheMetadata.sensitiveDataString;
    NSString *currentSensitiveDataString = ((NSObject *)[self cacheSensitiveData]).description;
    if (sensitiveDataString || currentSensitiveDataString) {
        // If one of the strings is nil, short-circuit evaluation will trigger
        if (sensitiveDataString.length != currentSensitiveDataString.length || ![sensitiveDataString isEqualToString:currentSensitiveDataString]) {
            if (error) {
                *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorSensitiveDataMismatch userInfo:@{ NSLocalizedDescriptionKey:@"Cache sensitive data mismatch"}];
            }
            return NO;
        }
    }

    // Is the version of app compatible
    NSString *appVersionString = self.cacheMetadata.appVersionString;
    NSString *currentAppVersionString = [YTKNetworkUtils appVersionString];
    if (appVersionString || currentAppVersionString) {
        if (appVersionString.length != currentAppVersionString.length || ![appVersionString isEqualToString:currentAppVersionString]) {
            if (error) {
                *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorAppVersionMismatch userInfo:@{ NSLocalizedDescriptionKey:@"App version mismatch"}];
            }
            return NO;
        }
    }
    return YES;
}

If each metadata information passes, verify that the cache can be retrieved in the loadCacheData method:

//YTKRequest.m
- (BOOL)loadCacheData {

    NSString *path = [self cacheFilePath];
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSError *error = nil;

    if ([fileManager fileExistsAtPath:path isDirectory:nil]) {
        NSData *data = [NSData dataWithContentsOfFile:path];
        _cacheData = data;
        _cacheString = [[NSString alloc] initWithData:_cacheData encoding:self.cacheMetadata.stringEncoding];
        switch (self.responseSerializerType) {
            case YTKResponseSerializerTypeHTTP:
                // Do nothing.
                return YES;
            case YTKResponseSerializerTypeJSON:
                _cacheJSON = [NSJSONSerialization JSONObjectWithData:_cacheData options:(NSJSONReadingOptions)0 error:&error];
                return error == nil;
            case YTKResponseSerializerTypeXMLParser:
                _cacheXML = [[NSXMLParser alloc] initWithData:_cacheData];
                return YES;
        }
    }
    return NO;
}

If the final test passes, the cache corresponding to the current request meets the requirements and can be successfully retrieved, that is, it can be called back directly.

After confirming that the cache can be successfully extracted, manually set the dataFromCache property to YES to indicate that the current request result is from the cache and not from the network.

Then the following is done before the actual callback:

//YTKRequest.m: 
- (void)start{

    ....

    //5. Action before callback
    //5.1 Cache Processing
    [self requestCompletePreprocessor];

    //5.2 Users can do pre-callback actions here
    [self requestCompleteFilter];

    ....
}

5.1: requestCompletePreprocessor method:

//YTKRequest.m: 
- (void)requestCompletePreprocessor {

    [super requestCompletePreprocessor];

    //Whether to write responseData to the cache asynchronously (the task to write to the cache is placed in a dedicated queue ytkrequest_cache_writing_queue)
    if (self.writeCacheAsynchronously) {

        dispatch_async(ytkrequest_cache_writing_queue(), ^{
            //Save response data to cache
            [self saveResponseDataToCacheFile:[super responseData]];
        });

    } else {
        //Save response data to cache
        [self saveResponseDataToCacheFile:[super responseData]];
    }
}
//YTKRequest.m: 
//Save response data to cache
- (void)saveResponseDataToCacheFile:(NSData *)data {

    if ([self cacheTimeInSeconds] > 0 && ![self isDataFromCache]) {
        if (data != nil) {
            @try {
                // New data will always overwrite old data.
                [data writeToFile:[self cacheFilePath] atomically:YES];

                YTKCacheMetadata *metadata = [[YTKCacheMetadata alloc] init];
                metadata.version = [self cacheVersion];
                metadata.sensitiveDataString = ((NSObject *)[self cacheSensitiveData]).description;
                metadata.stringEncoding = [YTKNetworkUtils stringEncodingWithRequest:self];
                metadata.creationDate = [NSDate date];
                metadata.appVersionString = [YTKNetworkUtils appVersionString];
                [NSKeyedArchiver archiveRootObject:metadata toFile:[self cacheMetadataFilePath]];

            } @catch (NSException *exception) {
                YTKLog(@"Save cache failed, reason = %@", exception.reason);
            }
        }
    }
}

As we can see, the task of the requestCompletePreprocessor method is to save the response data, that is, to cache it.However, there are two conditions for the cache to be saved: one requires the cacheTimeInSeconds method to return a positive integer (cache time, in seconds, which will be explained later); the other requires the isDataFromCache method to return NO.
But we know that if the cache is available, this property will be set to YES, so when we get here, we won't do caching.

Next, look at the 5.2:requestCompleteFilter method, which requires the user to provide his own implementation, specifically as some of the processing before the callback succeeds:

//YTKBaseRequest.m
- (void)requestCompleteFilter {
}

Now that the processing before the callback is complete, let's take a look at the callback when the cache is available:

//YTKRequest.m
- (void)start{

    ...

    YTKRequest *strongSelf = self;

    //6. Execute callback
    //6.1 Agent Requesting Completion
    [strongSelf.delegate requestFinished:strongSelf];

    //6.2 Successful block request
    if (strongSelf.successCompletionBlock) {
         strongSelf.successCompletionBlock(strongSelf);
    }

    //7. Set both successful and failed block s to nil to avoid circular references
    [strongSelf clearCompletionBlock];
}

As we can see, there are two callbacks at the same time: callbacks from the proxy and callbacks from the block.The proxy callback is executed first, then the block callback.And after the callback ends, YTKNetwork helps us empty the callback block:

//YTKBaseRequest.m
- (void)clearCompletionBlock {
    // Clear the block at the end of the request to avoid circular references
    self.successCompletionBlock = nil;
    self.failureCompletionBlock = nil;
}

Note that both proxy and block are invoked when the user implements both.

Here, we learn how YTKNetwork validates the cache before a network request, and how it calls back when the cache is valid.

Conversely, if the cache is invalid (or the cache is ignored), the network needs to be requested immediately.So now let's see what YTKNetwork has done at this time:

Looking closely at the start method above, we can see that if the cache does not meet the criteria, the startWithoutCache method is called directly:

//YTKRequest.m
- (void)start{

    //1. If Cache->Request is ignored
    if (self.ignoreCache) {
        [self startWithoutCache];
        return;
    }

    //2. If there are download incomplete files - > requests
    if (self.resumableDownloadPath) {
        [self startWithoutCache];
        return;
    }

    //3. Failed to get cache - > Request
    if (![self loadCacheWithError:nil]) {
        [self startWithoutCache];
        return;
    }

    ......
}

So what has been done in the startWithoutCache method?

//YTKRequest.m
- (void)startWithoutCache {

    //1. Clear the cache
    [self clearCacheVariables];

    //2. Initiation request calling parent class
    [super start];
}

//Clear all caches corresponding to the current request
- (void)clearCacheVariables {
    _cacheData = nil;
    _cacheXML = nil;
    _cacheJSON = nil;
    _cacheString = nil;
    _cacheMetadata = nil;
    _dataFromCache = NO;
}

Here, all data about the cache is cleared first, then the parent class's start method is called:

//YTKBaseRequest.m:
- (void)start {

    //1. Tell Accessories that a callback is imminent (actually a request is about to be made)
    [self toggleAccessoriesWillStartCallBack];

    //2. Ask an agent to add a request and make a request. This is not a composite relationship here. An agent is a singleton
    [[YTKNetworkAgent sharedAgent] addRequest:self];
}

Accessories in the first step are objects that follow the <YTKRequestAccessory>proxy.This proxy defines some methods for tracking the status of requests.It is defined in the YTKBaseRequest.h file:

//The proxy used to track the status of the request.
@protocol YTKRequestAccessory <NSObject>

@optional

///  Inform the accessory that the request is about to start.
///
///  @param request The corresponding request.
- (void)requestWillStart:(id)request;

///  Inform the accessory that the request is about to stop. This method is called
///  before executing `requestFinished` and `successCompletionBlock`.
///
///  @param request The corresponding request.
- (void)requestWillStop:(id)request;

///  Inform the accessory that the request has already stoped. This method is called
///  after executing `requestFinished` and `successCompletionBlock`.
///
///  @param request The corresponding request.
- (void)requestDidStop:(id)request;

@end

So as long as an object complies with this proxy, it can be traced to the state where the request is about to start, end, and end.

Next, take a look at the second step: YTKNetworkAgent adds the current request object to itself and sends the request.Take a look at its implementation:

//YTKNetworkAgent.m
- (void)addRequest:(YTKBaseRequest *)request {

    //1. Get task
    NSParameterAssert(request != nil);

    NSError * __autoreleasing requestSerializationError = nil;

    //Get user-defined request URL
    NSURLRequest *customUrlRequest= [request buildCustomUrlRequest];

    if (customUrlRequest) {

        __block NSURLSessionDataTask *dataTask = nil;
        //If there is a user-defined request, go directly to AFNetworking's dataTaskWithRequest:
        dataTask = [_manager dataTaskWithRequest:customUrlRequest completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
            //Unified processing of responses
            [self handleRequestResult:dataTask responseObject:responseObject error:error];
        }];
        request.requestTask = dataTask;

    } else {

        //If the user does not have a custom url, go directly here
        request.requestTask = [self sessionTaskForRequest:request error:&requestSerializationError];

    }

    //If serialization fails, the request is deemed to have failed
    if (requestSerializationError) {
        //Request failed processing
        [self requestDidFailWithRequest:request error:requestSerializationError];
        return;
    }

    NSAssert(request.requestTask != nil, @"requestTask should not be nil");

    // Priority Mapping
    // !!Available on iOS 8 +
    if ([request.requestTask respondsToSelector:@selector(priority)]) {
        switch (request.requestPriority) {
            case YTKRequestPriorityHigh:
                request.requestTask.priority = NSURLSessionTaskPriorityHigh;
                break;
            case YTKRequestPriorityLow:
                request.requestTask.priority = NSURLSessionTaskPriorityLow;
                break;
            case YTKRequestPriorityDefault:
                /*!!fall through*/
            default:
                request.requestTask.priority = NSURLSessionTaskPriorityDefault;
                break;
        }
    }

    // Retain request
    YTKLog(@"Add request: %@", NSStringFromClass([request class]));

    //2. Put the request in the dictionary where the request is saved, with taskIdentifier as key and request as value
    [self addRequestToRecord:request];

    //3. Start task
    [request.requestTask resume];
}

It's a long way to go, but don't be scared. It's divided into three parts:

  • The first part is to get the task corresponding to the current request and assign the request Task property to the request (requests mentioned later are instances of the current request class that the user customizes).
  • The second part is to put the request in a dictionary specifically designed to hold the request, with the key being the taskIdentifier.
  • The third part is to start the task.

Let me explain each section in turn:

Part One: Get the task corresponding to the current request and assign it to the request:

//YTKNetworkAgent.m
- (void)addRequest:(YTKBaseRequest *)request {

  ...

  if (customUrlRequest) {

        __block NSURLSessionDataTask *dataTask = nil;
        //If there is a user-defined request, go directly to AFNetworking's dataTaskWithRequest:
        dataTask = [_manager dataTaskWithRequest:customUrlRequest completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
            //Unify request responses
            [self handleRequestResult:dataTask responseObject:responseObject error:error];
        }];
        request.requestTask = dataTask;

    } else {

        //If the user does not have a custom url, go directly here
        request.requestTask = [self sessionTaskForRequest:request error:&requestSerializationError];

    }

  ...
}

Here you can determine if the user has customized the request:

  1. If so, call AFNetworking's dataTaskWithRequest: method directly.
  2. If not, call YTKRequest's own method of generating task.

Let's not mention the first case, because AF has helped us do it.Here's a look at the second case, sessionTaskForRequest: error: Inside the method:

//YTKNetworkAgent.m
//Return NSURLSessionTask based on different request types, serialization types, and request parameters
- (NSURLSessionTask *)sessionTaskForRequest:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error {

    //1. Get the type of request (GET, POST, etc.)
    YTKRequestMethod method = [request requestMethod];

    //2. Get the request url
    NSString *url = [self buildRequestUrl:request];

    //3. Get Request Parameters
    id param = request.requestArgument;
    AFConstructingBlock constructingBlock = [request constructingBodyBlock];

    //4. Get the request serializer
    AFHTTPRequestSerializer *requestSerializer = [self requestSerializerForRequest:request];

    //5. Return the corresponding task according to different request types
    switch (method) {

        case YTKRequestMethodGET:

            if (request.resumableDownloadPath) {
                //Download Tasks
                return [self downloadTaskWithDownloadPath:request.resumableDownloadPath requestSerializer:requestSerializer URLString:url parameters:param progress:request.resumableDownloadProgressBlock error:error];

            } else {
                //Common get request
                return [self dataTaskWithHTTPMethod:@"GET" requestSerializer:requestSerializer URLString:url parameters:param error:error];
            }

        case YTKRequestMethodPOST:
            //POST Request
            return [self dataTaskWithHTTPMethod:@"POST" requestSerializer:requestSerializer URLString:url parameters:param constructingBodyWithBlock:constructingBlock error:error];

        case YTKRequestMethodHEAD:
            //HEAD Request
            return [self dataTaskWithHTTPMethod:@"HEAD" requestSerializer:requestSerializer URLString:url parameters:param error:error];

        case YTKRequestMethodPUT:
            //PUT Request
            return [self dataTaskWithHTTPMethod:@"PUT" requestSerializer:requestSerializer URLString:url parameters:param error:error];

        case YTKRequestMethodDELETE:
            //DELETE Request
            return [self dataTaskWithHTTPMethod:@"DELETE" requestSerializer:requestSerializer URLString:url parameters:param error:error];

        case YTKRequestMethodPATCH:
            //PATCH Request
            return [self dataTaskWithHTTPMethod:@"PATCH" requestSerializer:requestSerializer URLString:url parameters:param error:error];
    }
}

From the final switch statement of this method, you can see that the purpose of this method is to return an instance of the NSU LSessionTask of the current request.Also, the method that ultimately generates the NSURLSessionTask instance is through the private method dataTaskWithHTTPMethod:requestSerializer:URLString:parameters:error:Before explaining this key private method, let's step by step explain how to get each parameter required by this private method:

  1. Get the request type (GET, POST, etc.):
//YTKNetworkAgent.m
- (NSURLSessionTask *)sessionTaskForRequest:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error {

  ...
  YTKRequestMethod method = [request requestMethod];
  ...

}

The requestMethod method was initially implemented in YTKBaseRequest and returns YTKRequestMethodGET by default.

Its enumeration type is defined in YTKBaseRequest.h:

//YTKBaseRequest.h
///  HTTP Request method.
typedef NS_ENUM(NSInteger, YTKRequestMethod) {
    YTKRequestMethodGET = 0,
    YTKRequestMethodPOST,
    YTKRequestMethodHEAD,
    YTKRequestMethodPUT,
    YTKRequestMethodDELETE,
    YTKRequestMethodPATCH,
};

Users can override this method in a custom request class based on their actual needs:

//RegisterAPI.m
- (YTKRequestMethod)requestMethod {
    return YTKRequestMethodPOST;
}

2. Get the request url:

//YTKNetworkAgent.m
- (NSURLSessionTask *)sessionTaskForRequest:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error {

  ...
  NSString *url = [self buildRequestUrl:request];
  ...

}


//Return current request url
- (NSString *)buildRequestUrl:(YTKBaseRequest *)request {

    NSParameterAssert(request != nil);

    //User-defined URL (excluding base_url set in YTKConfig)
    NSString *detailUrl = [request requestUrl];
    NSURL *temp = [NSURL URLWithString:detailUrl];

    // URLs with host and scheme returned correctly immediately
    if (temp && temp.host && temp.scheme) {
        return detailUrl;
    }

    // If you need to filter url s, filter
    NSArray *filters = [_config urlFilters];
    for (id<YTKUrlFilterProtocol> f in filters) {
        detailUrl = [f filterUrl:detailUrl withRequest:request];
    }

    NSString *baseUrl;
    if ([request useCDN]) {
        //If you use a CDN, return the globally configured CDN if the current request does not have a CDN address configured
        if ([request cdnUrl].length > 0) {
            baseUrl = [request cdnUrl];
        } else {
            baseUrl = [_config cdnUrl];
        }
    } else {
        //If baseUrl is used, baseUrl is not configured in the current request, returning the globally configured baseUrl
        if ([request baseUrl].length > 0) {
            baseUrl = [request baseUrl];
        } else {
            baseUrl = [_config baseUrl];
        }
    }
    // If there is no / at the end, add a / at the end
    NSURL *url = [NSURL URLWithString:baseUrl];

    if (baseUrl.length > 0 && ![baseUrl hasSuffix:@"/"]) {
        url = [url URLByAppendingPathComponent:@""];
    }

    return [NSURL URLWithString:detailUrl relativeToURL:url].absoluteString;
}

3. Get Request Parameters

//YTKNetworkAgent.m
- (NSURLSessionTask *)sessionTaskForRequest:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error {

   ...
      //Get user supplied request parameters
    id param = request.requestArgument;

    //Get the block of a user-provided construct request body (default is none)
    AFConstructingBlock constructingBlock = [request constructingBodyBlock];

   ...

}

In this case, requestArgument is a get method that requires the user to define the body of the request, for example, two request parameters are defined in the Register API:

//RegisterApi.m
- (id)requestArgument {
    return @{
        @"username": _username,
        @"password": _password
    };
}

4. Get the request serializer

//YTKNetworkAgent.m
- (NSURLSessionTask *)sessionTaskForRequest:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error {

   ...

   //4. Get the request serializer
   AFHTTPRequestSerializer *requestSerializer = [self requestSerializerForRequest:request];

   ...

}


- (AFHTTPRequestSerializer *)requestSerializerForRequest:(YTKBaseRequest *)request {

    AFHTTPRequestSerializer *requestSerializer = nil;

    //HTTP or JSON
    if (request.requestSerializerType == YTKRequestSerializerTypeHTTP) {
        requestSerializer = [AFHTTPRequestSerializer serializer];
    } else if (request.requestSerializerType == YTKRequestSerializerTypeJSON) {
        requestSerializer = [AFJSONRequestSerializer serializer];
    }

    //timeout
    requestSerializer.timeoutInterval = [request requestTimeoutInterval];

    //Allow data services
    requestSerializer.allowsCellularAccess = [request allowsCellularAccess];

    //If the current request needs validation
    NSArray<NSString *> *authorizationHeaderFieldArray = [request requestAuthorizationHeaderFieldArray];
    if (authorizationHeaderFieldArray != nil) {
        [requestSerializer setAuthorizationHeaderFieldWithUsername:authorizationHeaderFieldArray.firstObject
                                                          password:authorizationHeaderFieldArray.lastObject];
    }

    //If the current request requires a custom HTTPHeaderField
    NSDictionary<NSString *, NSString *> *headerFieldValueDictionary = [request requestHeaderFieldValueDictionary];
    if (headerFieldValueDictionary != nil) {
        for (NSString *httpHeaderField in headerFieldValueDictionary.allKeys) {
            NSString *value = headerFieldValueDictionary[httpHeaderField];
            [requestSerializer setValue:value forHTTPHeaderField:httpHeaderField];
        }
    }
    return requestSerializer;
}

This method above obtains an instance of AFHTTPRequestSerializer from the incoming request instance based on some of its configurations (user supplied).

So far, several parameters have been taken to get the NSURLSessionTask instance, and all that remains is to call the dataTaskWithHTTPMethod:requestSerializer:URLString:parameters:error: method to get the NSURLSessionTask instance.Let's look at the implementation of this approach:

//YTKNetworkAgent.m
- (NSURLSessionDataTask *)dataTaskWithHTTPMethod:(NSString *)method
                               requestSerializer:(AFHTTPRequestSerializer *)requestSerializer
                                       URLString:(NSString *)URLString
                                      parameters:(id)parameters
                                           error:(NSError * _Nullable __autoreleasing *)error {
    return [self dataTaskWithHTTPMethod:method requestSerializer:requestSerializer URLString:URLString parameters:parameters constructingBodyWithBlock:nil error:error];
}

//Final return to NSURLSessionDataTask instance
- (NSURLSessionDataTask *)dataTaskWithHTTPMethod:(NSString *)method
                               requestSerializer:(AFHTTPRequestSerializer *)requestSerializer
                                       URLString:(NSString *)URLString
                                      parameters:(id)parameters
                       constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block
                                           error:(NSError * _Nullable __autoreleasing *)error {
    NSMutableURLRequest *request = nil;

    //Get request based on whether there is a block to construct the requester
    if (block) {
        request = [requestSerializer multipartFormRequestWithMethod:method URLString:URLString parameters:parameters constructingBodyWithBlock:block error:error];
    } else {
        request = [requestSerializer requestWithMethod:method URLString:URLString parameters:parameters error:error];
    }

    //Get the dataTask after you get the request
    __block NSURLSessionDataTask *dataTask = nil;
    dataTask = [_manager dataTaskWithRequest:request
                           completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *_error) {
                               //Unified processing of responses
                               [self handleRequestResult:dataTask responseObject:responseObject error:_error];
                           }];

    return dataTask;
}

Both methods, the above method calls below to get the final instance of NSU RLSessionDataTask.

OK, now that we know how the NSURLSessionDataTask instance was acquired, let's take a look at addRequest: The next step in the method is to handle the serialization failure:

//YTKNetworkAgent.m
- (void)addRequest:(YTKBaseRequest *)request {

   ...

  //Serialization failed
    if (requestSerializationError) {
        //Request failed processing
        [self requestDidFailWithRequest:request error:requestSerializationError];
        return;
    }

   ...

}

RequDidFailWithRequest: The method is designed to handle requests that fail because it is included in the Unified Processing of Request Callbacks method, so it will be explained in more detail later when explaining the Unified Processing of Request Callbacks.

Move on to the Priority Mapping section:

//YTKNetworkAgent.m
- (void)addRequest:(YTKBaseRequest *)request {

   ...

    // Priority Mapping
    // !!Available on iOS 8 +
    if ([request.requestTask respondsToSelector:@selector(priority)]) {
        switch (request.requestPriority) {
            case YTKRequestPriorityHigh:
                request.requestTask.priority = NSURLSessionTaskPriorityHigh;
                break;
            case YTKRequestPriorityLow:
                request.requestTask.priority = NSURLSessionTaskPriorityLow;
                break;
            case YTKRequestPriorityDefault:
                /*!!fall through*/
            default:
                request.requestTask.priority = NSURLSessionTaskPriorityDefault;
                break;
        }
    }

   ...

}

RequPriority is an enumeration property of YTKBaseRequest whose enumeration is defined in YTKBaseRequest.h:

typedef NS_ENUM(NSInteger, YTKRequestPriority) {
    YTKRequestPriorityLow = -4L,
    YTKRequestPriorityDefault = 0,
    YTKRequestPriorityHigh = 4,
};

Here, the YTKRequestPriority set by the user is mapped to the priority of the NSURLSessionTask.

Here, we take an instance of task and prioritize it, followed by the second part of the addRequest:method:
YTKNetworkAgent places the request instance in a dictionary and saves it:

Part two: Put requests in a dictionary specifically designed to hold requests, with key as taskIdentifier:

//YTKNetworkAgent.m
- (void)addRequest:(YTKBaseRequest *)request {

   ...

   ...

  //Put the request instance in the dictionary where the request is saved, taskIdentifier is key and request is value
  [self addRequestToRecord:request];

   ...

}


- (void)addRequestToRecord:(YTKBaseRequest *)request {

    //Locking
    Lock();
    _requestsRecord[@(request.requestTask.taskIdentifier)] = request;
    Unlock();
}

#define Lock() pthread_mutex_lock(&_lock)
#define Unlock() pthread_mutex_unlock(&_lock)

You can see that both before and after adding are locked and unlocked.And when the request instance is saved, save its task identifier as a key.

After the current request has been saved, the final step is to formally initiate the request:

Part Three: Starting the task

//YTKNetworkAgent.m
- (NSURLSessionTask *)sessionTaskForRequest:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error {

   ...

   [request.requestTask resume];

   ...

}

So far, we've learned what a request in YTKNetwork does before it starts: find an available cache, generate an instance of NSURLSessionTask, get url, request Serializer, put request into a dictionary of YTKNetworkAgent, and so on (detailed process will be given later).

Now let's look at how YTKNetwork handles the callback for the request.

Point-of-eye classmates may notice that when getting an instance of NSURLSessionTask, there are two comments for "Unified Processing of Responses", which you can search to find: handleRequestResult:responseObject:error:.This method is responsible for handling callbacks to requests, including success and failure, of course.Let's see what we've done in this way:

//YTKNetworkAgent.m
//Unify request results, including success and failure
- (void)handleRequestResult:(NSURLSessionTask *)task responseObject:(id)responseObject error:(NSError *)error {

    //1. Get the task request
    Lock();
    YTKBaseRequest *request = _requestsRecord[@(task.taskIdentifier)];
    Unlock();

    //If no corresponding request exists, return immediately
    if (!request) {
        return;
    }

    . . . 

    //2. Get the response for the request
    request.responseObject = responseObject;

    //3. Get responseObject, responseData, and responseString
    if ([request.responseObject isKindOfClass:[NSData class]]) {

       //3.1 Get response data
        request.responseData = responseObject;

        //3.2 Get response String
        request.responseString = [[NSString alloc] initWithData:responseObject encoding:[YTKNetworkUtils stringEncodingWithRequest:request]];

         //3.3 Get responseObject (or responseJSONObject)
        //Get the corresponding type of response based on the serialized type of response returned
        switch (request.responseSerializerType)
        {
            case YTKResponseSerializerTypeHTTP:
                // Default serializer. Do nothing.
                break;

            case YTKResponseSerializerTypeJSON:
                request.responseObject = [self.jsonResponseSerializer responseObjectForResponse:task.response data:request.responseData error:&serializationError];
                request.responseJSONObject = request.responseObject;
                break;

            case YTKResponseSerializerTypeXMLParser:
                request.responseObject = [self.xmlParserResponseSerialzier responseObjectForResponse:task.response data:request.responseData error:&serializationError];
                break;
        }
    }

    //4. Determine if there is an error, assign the error object to requestError, and change the Boolean value of succeed.The purpose is to determine whether a successful callback or a failed callback is made based on the succeed value
    if (error) {
        //If the error passed in by this method is not nil
        succeed = NO;
        requestError = error;

    } else if (serializationError) {
        //If serialization fails
        succeed = NO;
        requestError = serializationError;

    } else {

        //Verify that the request is valid even if there is no error and the serialization passes
        succeed = [self validateResult:request error:&validationError];
        requestError = validationError;
    }

    //5. Call the appropriate processing based on the Boolean value of succeed
    if (succeed) {
        //Request successfully processed
        [self requestDidSucceedWithRequest:request];
    } else {

        //Request failed processing
        [self requestDidFailWithRequest:request error:requestError];
    }

     //6. Processing done by callback
    dispatch_async(dispatch_get_main_queue(), ^{
        //6.1 Remove the current request from the dictionary
        [self removeRequestFromRecord:request];
         //6.2 Clear all block s
        [request clearCompletionBlock];
    });
}

Simply explain the code above:

  • First get the corresponding request from the dictionary saved by YTKNetworkAgent using the task's identifier value.
  • The resulting responseObject is then processed, and the processed responseObject, responseData, and responseString are assigned to the current request instance request.
  • These values are then used to determine the success or failure of the final callback (changing the succeed value).
  • Finally, callbacks for success and failure are made based on the value of succeed.

Here's how to judge the effectiveness of json:

//YTKNetworkAgent.m
//Judging whether code meets scope and json's validity
- (BOOL)validateResult:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error {

    //1. Determine if code is between 200 and 299
    BOOL result = [request statusCodeValidator];

    if (!result) {
        if (error) {
            *error = [NSError errorWithDomain:YTKRequestValidationErrorDomain code:YTKRequestValidationErrorInvalidStatusCode userInfo:@{NSLocalizedDescriptionKey:@"Invalid status code"}];
        }
        return result;
    }

    //2. The presence of result determines whether json is valid
    id json = [request responseJSONObject];
    id validator = [request jsonValidator];

    if (json && validator) {
        //Judging whether json is valid by json and validator
        result = [YTKNetworkUtils validateJSON:json withValidator:validator];

        //If json is invalid
        if (!result) {
            if (error) {
                *error = [NSError errorWithDomain:YTKRequestValidationErrorDomain code:YTKRequestValidationErrorInvalidJSONFormat userInfo:@{NSLocalizedDescriptionKey:@"Invalid JSON format"}];
            }
            return result;
        }
    }
    return YES;
}

Here, first, use the statusCodeValidator method to determine if the code of the response is in the correct range:

//YTKBaseReqiest.m
- (BOOL)statusCodeValidator {
    NSInteger statusCode = [self responseStatusCode];
    return (statusCode >= 200 && statusCode <= 299);
}
- (NSInteger)responseStatusCode {
    return self.response.statusCode;
}

Then judge the validity of json:

//YTKNetworkUtils.m
//Judging json's validity
+ (BOOL)validateJSON:(id)json withValidator:(id)jsonValidator {
    if ([json isKindOfClass:[NSDictionary class]] &&
        [jsonValidator isKindOfClass:[NSDictionary class]]) {
        NSDictionary * dict = json;
        NSDictionary * validator = jsonValidator;
        BOOL result = YES;
        NSEnumerator * enumerator = [validator keyEnumerator];
        NSString * key;
        while ((key = [enumerator nextObject]) != nil) {
            id value = dict[key];
            id format = validator[key];
            if ([value isKindOfClass:[NSDictionary class]]
                || [value isKindOfClass:[NSArray class]]) {
                result = [self validateJSON:value withValidator:format];
                if (!result) {
                    break;
                }
            } else {
                if ([value isKindOfClass:format] == NO &&
                    [value isKindOfClass:[NSNull class]] == NO) {
                    result = NO;
                    break;
                }
            }
        }
        return result;
    } else if ([json isKindOfClass:[NSArray class]] &&
               [jsonValidator isKindOfClass:[NSArray class]]) {
        NSArray * validatorArray = (NSArray *)jsonValidator;
        if (validatorArray.count > 0) {
            NSArray * array = json;
            NSDictionary * validator = jsonValidator[0];
            for (id item in array) {
                BOOL result = [self validateJSON:item withValidator:validator];
                if (!result) {
                    return NO;
                }
            }
        }
        return YES;
    } else if ([json isKindOfClass:jsonValidator]) {
        return YES;
    } else {
        return NO;
    }
}

Note that the class YTKNetworkUtils is defined in YTKNetworkPirvate, which has some methods for tool classes that you will encounter later.

After verifying that the returned JSON data is valid, you can call back:

//YTKNetworkAgent.m
- (void)handleRequestResult:(NSURLSessionTask *)task responseObject:(id)responseObject error:(NSError *)error {

    ...
    //5. Call the appropriate processing based on the Boolean value of succeed
    if (succeed) {
        //Request successfully processed
        [self requestDidSucceedWithRequest:request];
    } else {

        //Request failed processing
        [self requestDidFailWithRequest:request error:requestError];
    }

    //6. Processing done by callback
    dispatch_async(dispatch_get_main_queue(), ^{
        //6.1 Remove the current request from the dictionary
        [self removeRequestFromRecord:request];
         //6.2 Clear all block s
        [request clearCompletionBlock];
    });

    ...

}

Let's first look at the successful and failed processing of requests, respectively:

Request successfully processed:

//YTKNetworkAgent.m
//Successful Request: Primary responsibility is to write results to the cache & callback successful proxies and block s
- (void)requestDidSucceedWithRequest:(YTKBaseRequest *)request {

    @autoreleasepool {
        //Write Cache 
        [request requestCompletePreprocessor];
    }

    dispatch_async(dispatch_get_main_queue(), ^{

        //Tell Accessories that the request is stopping
        [request toggleAccessoriesWillStopCallBack];

        //Handling before true callback, user-defined
        [request requestCompleteFilter];

        //If there is a proxy, call the successful proxy
        if (request.delegate != nil) {
            [request.delegate requestFinished:request];
        }

        //Called if a successful callback code is passed in
        if (request.successCompletionBlock) {
            request.successCompletionBlock(request);
        }

        //Tell Accessories that the request has ended
        [request toggleAccessoriesDidStopCallBack];
    });
}

As we can see, after the request succeeds, the first thing to do is write to the cache. Let's take a look at the implementation of the requestCompletePreprocessor method:

//YTKRequest.m
- (void)requestCompletePreprocessor {

    [super requestCompletePreprocessor];

    //Whether to write responseData to the cache asynchronously (write to the cache task in a dedicated queue)
    if (self.writeCacheAsynchronously) {

        dispatch_async(ytkrequest_cache_writing_queue(), ^{
            //Write to Cache File
            [self saveResponseDataToCacheFile:[super responseData]];
        });

    } else {
         //Write to Cache File
        [self saveResponseDataToCacheFile:[super responseData]];
    }
}

//Write to Cache File
- (void)saveResponseDataToCacheFile:(NSData *)data {

    if ([self cacheTimeInSeconds] > 0 && ![self isDataFromCache]) {
        if (data != nil) {
            @try {
                // 1. Save the responseData of the request to cacheFilePath
                [data writeToFile:[self cacheFilePath] atomically:YES];

                // 2. Save the metadata of the request to cacheMetadataFilePath
                YTKCacheMetadata *metadata = [[YTKCacheMetadata alloc] init];
                metadata.version = [self cacheVersion];
                metadata.sensitiveDataString = ((NSObject *)[self cacheSensitiveData]).description;
                metadata.stringEncoding = [YTKNetworkUtils stringEncodingWithRequest:self];
                metadata.creationDate = [NSDate date];
                metadata.appVersionString = [YTKNetworkUtils appVersionString];
                [NSKeyedArchiver archiveRootObject:metadata toFile:[self cacheMetadataFilePath]];

            } @catch (NSException *exception) {
                YTKLog(@"Save cache failed, reason = %@", exception.reason);
            }
        }
    }
}

First, let's look at the conditions under which the write cache operation is performed: when the cacheTimeInSeconds method returns greater than 0 and isDataFromCache is NO.

The cacheTimeInSeconds method returns the time the cache was saved. It was originally defined in YTKBaseRquest, and the default return is -1:

//YTKBaseRequest.m
- (NSInteger)cacheTimeInSeconds {
    return -1;
}

So YTKNetwork does not cache by default. If users need to cache, they need to return an integer greater than 0 in the custom request class, which is in seconds.

The isDataFromCache property is described in the Query Cache steps in the Send Request section above.Here's one more thing to emphasize: the default value of isDataFromCache is NO.Before the request is initiated, -
When querying the cache:

  • If the cache is found to be unavailable (or the cache is ignored), the request is sent immediately, at which point the isDataFromCache value remains NO without changing.
  • If you find that the cache is available (without ignoring the cache), set the isDataFromCache property to YES, which means you won't need to send a request to get the data directly there.

That is, if a request is sent, isDataFromCache must be NO, then (!isDataFromCache) must be YES in the above judgment.

Therefore, if the user sets a time for the cache to be saved, the cache will be written after the request returns successfully.

Let's move on to see that YTKNetwork holds two caches for caching:
The first is a pure instance of the NSDAata type.The second is an instance of the metadata YTKCacheMetadata describing the current NSData instance, which can be divided into these categories in terms of its properties:

  1. Cached version, returned to 0 by default, user can customize.
  2. Sensitive data, type id, returns nil by default, user can customize.
  3. Encoding format for NSString, implemented in YTKNetworkUtils within YTKNetworkPrivate.
  4. Creation time of metadata.
  5. Version number of app, implemented in YTKNetworkUtils within YTKNetworkPrivate.

After assigning these properties to instances of metadata, the metadata instances are serialized and written to disk.The saved path is obtained by the cacheMetadataFilePath method.

Now that you know the contents of the YTKRequest cache, let's look at the location of both caches:

//YTKRequest.m
//File name of pure NSData data cache
- (NSString *)cacheFileName {
    NSString *requestUrl = [self requestUrl];
    NSString *baseUrl = [YTKNetworkConfig sharedConfig].baseUrl;
    id argument = [self cacheFileNameFilterForRequestArgument:[self requestArgument]];
    NSString *requestInfo = [NSString stringWithFormat:@"Method:%ld Host:%@ Url:%@ Argument:%@",
                             (long)[self requestMethod], baseUrl, requestUrl, argument];
    NSString *cacheFileName = [YTKNetworkUtils md5StringFromString:requestInfo];
    return cacheFileName;
}

//Cache location for pure NSData data
- (NSString *)cacheFilePath {
    NSString *cacheFileName = [self cacheFileName];
    NSString *path = [self cacheBasePath];
    path = [path stringByAppendingPathComponent:cacheFileName];
    return path;
}

//Cache location of metadata
- (NSString *)cacheMetadataFilePath {
    NSString *cacheMetadataFileName = [NSString stringWithFormat:@"%@.metadata", [self cacheFileName]];
    NSString *path = [self cacheBasePath];
    path = [path stringByAppendingPathComponent:cacheMetadataFileName];
    return path;
}

//Create a folder where users save all YTKNetwork caches
- (NSString *)cacheBasePath {

    //Get full path
    NSString *pathOfLibrary = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *path = [pathOfLibrary stringByAppendingPathComponent:@"LazyRequestCache"];

    // YTKCacheDirPathFilterProtocol s define proxy methods that users can customize storage locations
    NSArray<id<YTKCacheDirPathFilterProtocol>> *filters = [[YTKNetworkConfig sharedConfig] cacheDirPathFilters];
    if (filters.count > 0) {
        for (id<YTKCacheDirPathFilterProtocol> f in filters) {
            path = [f filterCacheDirPath:path withRequest:self];
        }
    }

    //create folder
    [self createDirectoryIfNeeded:path];
    return path;
}

As you can see, the file name of the pure NSData data cache contains the request method (GET,POST..), the baseURL, the request URL, and the string spliced by the request parameters, encrypted by md5.

The metadata file name is appended with the.Metadata suffix to the file name of the pure NSData data cache.

To get a better view of these two caches, I set the cache save time to 200 seconds and request it again, then open the folder to find them:


Files that cache and cache metadata

We also confirmed that the name of the folder that holds all the YTKNetwork caches is LazyRequestCache.

OK, now that we know the cache write after a successful callback is requested, let's see how the callback works:

//YTKNetworkAgent.m
- (void)handleRequestResult:(NSURLSessionTask *)task responseObject:(id)responseObject error:(NSError *)error {

    ...
    YTKRequest *strongSelf = self;

    //6. Execute callback
    //6.1 Agent Requesting Completion
    [strongSelf.delegate requestFinished:strongSelf];

    //6.2 Successful block request
    if (strongSelf.successCompletionBlock) {
        strongSelf.successCompletionBlock(strongSelf);
    }

    //7. Set both successful and failed block s to nil to avoid circular references
    [strongSelf clearCompletionBlock];
}

As we can see, the callback of the proxy precedes that of the block.And immediately after the block callback ends, the clearCompletionBlock method is called to empty the block.This method is implemented in YTKBaseRequest:

//YTKBaseRequest.m
- (void)clearCompletionBlock {
    // Clear the block at the end of the request to avoid circular references
    self.successCompletionBlock = nil;
    self.failureCompletionBlock = nil;
}

Now that we know that the request was successfully processed, let's take a look at the processing when the request failed:

//YTKNetworkAgent.m
//request was aborted
- (void)requestDidFailWithRequest:(YTKBaseRequest *)request error:(NSError *)error {

    request.error = error;
    YTKLog(@"Request %@ failed, status code = %ld, error = %@",
           NSStringFromClass([request class]), (long)request.responseStatusCode, error.localizedDescription);

    // Store incomplete download data
    NSData *incompleteDownloadData = error.userInfo[NSURLSessionDownloadTaskResumeData];
    if (incompleteDownloadData) {
        [incompleteDownloadData writeToURL:[self incompleteDownloadTempPathForDownloadPath:request.resumableDownloadPath] atomically:YES];
    }

    // Load response from file and clean up if download task failed.
    //If the download task fails, take out the corresponding response file and empty it
    if ([request.responseObject isKindOfClass:[NSURL class]]) {
        NSURL *url = request.responseObject;

        //IsFileURL: Is it a file, and if so, it can be retrieved from isFileURL; &&Then confirm again whether there is a file corresponding to the url change?
        if (url.isFileURL && [[NSFileManager defaultManager] fileExistsAtPath:url.path]) {

            //Assign url's data and string to request
            request.responseData = [NSData dataWithContentsOfURL:url];
            request.responseString = [[NSString alloc] initWithData:request.responseData encoding:[YTKNetworkUtils stringEncodingWithRequest:request]];

            [[NSFileManager defaultManager] removeItemAtURL:url error:nil];
        }

        //Empty request
        request.responseObject = nil;
    }


    @autoreleasepool {
        //Request failed preprocessing, YTK undefined, user defined
        [request requestFailedPreprocessor];
    }

    dispatch_async(dispatch_get_main_queue(), ^{

        //Tell Accessories that the request is stopping
        [request toggleAccessoriesWillStopCallBack];

        //Processing before true callback
        [request requestFailedFilter];

        //If there is a proxy, call the proxy
        if (request.delegate != nil) {
            [request.delegate requestFailed:request];
        }

        //If a block code with a failed callback is passed in, the block is called
        if (request.failureCompletionBlock) {
            request.failureCompletionBlock(request);
        }

        //Tell Accessories that the request has stopped
        [request toggleAccessoriesDidStopCallBack];
    });
}

In this method, it first determines whether the current task is a download task, and if so, stores the currently downloaded data into resumableDownloadPath.If the download task fails, empty the file corresponding to the locally saved path.

At this point, I've explained the steps for configuring, sending, responding, and callback individual requests.To help you understand the whole process, here is the whole process diagram:


YTKNetwork Flowchart

We say that YTKNetworkAgent is the sender of the request. Since there is a send, there will be cancellations and so on, so we have to mention its other two interfaces:

//YTKNetworkAgent.h
///Cancel a request
- (void)cancelRequest:(YTKBaseRequest *)request;

///Cancel all added request s
- (void)cancelAllRequests;

First, let's look at the implementation of cancelling a request:

//YTKNetworkAgent.m
///Cancel a request
- (void)cancelRequest:(YTKBaseRequest *)request {

    NSParameterAssert(request != nil);
    //Get the task of the request and cancel
    [request.requestTask cancel];
    //Remove current request from dictionary
    [self removeRequestFromRecord:request];
    //Clean up all block s
    [request clearCompletionBlock];
}

//Remove a request from the dictionary
- (void)removeRequestFromRecord:(YTKBaseRequest *)request {

    //Locking
    Lock();
    [_requestsRecord removeObjectForKey:@(request.requestTask.taskIdentifier)];
    YTKLog(@"Request queue size = %zd", [_requestsRecord count]);
    Unlock();
}

Cancel all request s added to the dictionary:

//YTKNetworkAgent.m
- (void)cancelAllRequests {
    Lock();
    NSArray *allKeys = [_requestsRecord allKeys];
    Unlock();
    if (allKeys && allKeys.count > 0) {
        NSArray *copiedKeys = [allKeys copy];
        for (NSNumber *key in copiedKeys) {
            Lock();
            YTKBaseRequest *request = _requestsRecord[key];
            Unlock();
            //stop per request
            [request stop];
        }
    }
}

This stop method is defined in YTKBaseRequest:

//YTKBaseRequest.m
- (void)stop {

    //Tell Accessories that a callback is coming
    [self toggleAccessoriesWillStopCallBack];

    //Empty agent
    self.delegate = nil;

    //Call agent's method of canceling a request
    [[YTKNetworkAgent sharedAgent] cancelRequest:self];

    //Tell Accessories that the callback is complete
    [self toggleAccessoriesDidStopCallBack];
}

OK, see here, I'm sure you have a better understanding of the YTKNetwork single request process. Let's take a look at the advanced features of YTKNetwork: batch and chain requests.

3.3 Bulk and Chain Requests

There are two types of bulk requests supported by YTKNetwork:

  1. Bulk requests: Multiple requests are initiated almost simultaneously.
  2. Chained request: The next request cannot be initiated until the current request is complete.

Whether it's a batch request or a chain request, it's conceivable that these requests are most likely managed with an array.So how exactly is this done?

Let's first look at how YTKNetwork implements bulk requests.

3.31 Bulk Requests

YTKNetwork uses the YTKBatchRequest class to send out an unordered batch request, which needs to be initialized with an array containing a subclass of YTKRequest and saved to its _requestArray instance variable:

//YTKBatchRequest.m
- (instancetype)initWithRequestArray:(NSArray<YTKRequest *> *)requestArray {
    self = [super init];
    if (self) {

        //Save as Attribute
        _requestArray = [requestArray copy];

        //Initialize the number of batch requests completed to 0
        _finishedCount = 0;

        //Type check, all elements must be YTKRequest or its subclasses, otherwise force initialization fails
        for (YTKRequest * req in _requestArray) {
            if (![req isKindOfClass:[YTKRequest class]]) {
                YTKLog(@"Error, request item must be YTKRequest instance.");
                return nil;
            }
        }
    }
    return self;
}

After initialization, we can call the start method to initiate all requests managed by the current YTKBatchRequest instance:

//YTKBatchRequest.m
//Bach Request Start
- (void)startWithCompletionBlockWithSuccess:(void (^)(YTKBatchRequest *batchRequest))success
                                    failure:(void (^)(YTKBatchRequest *batchRequest))failure {
    [self setCompletionBlockWithSuccess:success failure:failure];
    [self start];
}

//Set successful and failed block s
- (void)setCompletionBlockWithSuccess:(void (^)(YTKBatchRequest *batchRequest))success
                              failure:(void (^)(YTKBatchRequest *batchRequest))failure {
    self.successCompletionBlock = success;
    self.failureCompletionBlock = failure;
}

- (void)start {

    //If the first request in the batch has successfully ended, you cannot start again
    if (_finishedCount > 0) {
        YTKLog(@"Error! Batch request has already started.");
        return;
    }

    //Initially set failed request to nil
    _failedRequest = nil;

    //Use YTKBatchRequestAgent to manage current bulk requests
    [[YTKBatchRequestAgent sharedAgent] addBatchRequest:self];
    [self toggleAccessoriesWillStartCallBack];

    //Traverse all requests and start requests
    for (YTKRequest * req in _requestArray) {
        req.delegate = self;
        [req clearCompletionBlock];
        [req start];
    }
}

Here we can see:
1. After at least one of these requests has been completed, the start method that calls the current YTKBatchRequest instance returns immediately, otherwise you can start without restriction.
2.An example of YTKBatchRequest needs to be added to the array in the YTKBatchRequestAgent before the request can be made:

//YTKBatchRequestAgent.m
- (void)addBatchRequest:(YTKBatchRequest *)request {
    @synchronized(self) {
        [_requestArray addObject:request];
    }
}

3. Since requests are sent in bulk, here is _requestArray traversing the YTKBatchRequest instance and sending requests one by one.Since individual requests are already encapsulated, start directly here.

After the request has been initiated, the proxy method for each request callback determines whether the bulk request was successful.

Successful callbacks for the YTKRequest subclass:

//YTKBatchRequest.m
#pragma mark - Network Request Delegate
- (void)requestFinished:(YTKRequest *)request {

    //After a request succeeds, first let _finishedCount + 1
    _finishedCount++;

    //If _finishedCount equals the number of _requestArray, determine that the current batch request is successful
    if (_finishedCount == _requestArray.count) {

        //Call the closing proxy
        [self toggleAccessoriesWillStopCallBack];

        //Invoke successful proxy
        if ([_delegate respondsToSelector:@selector(batchRequestFinished:)]) {
            [_delegate batchRequestFinished:self];
        }

        //Invoke successful block for batch request
        if (_successCompletionBlock) {
            _successCompletionBlock(self);
        }

        //Empty successful and failed block s
        [self clearCompletionBlock];

        //Invoke the end of request proxy
        [self toggleAccessoriesDidStopCallBack];

        //Remove current batch from YTKBatchRequestAgent
        [[YTKBatchRequestAgent sharedAgent] removeBatchRequest:self];
    }
}

We can see that after a call back to a request succeeds, it counts the success by + 1.After +1, if the success count is equal to the number of elements in the current batch request array, the current batch request is determined to be successful and a successful callback for the current batch request is made.

Next let's look at the processing of a request that failed:

Failed callbacks for the YTKReques subclass:

//YTKBatchRequest.m
- (void)requestFailed:(YTKRequest *)request {

    _failedRequest = request;

    //Call the closing proxy
    [self toggleAccessoriesWillStopCallBack];

    //Stop all requests in batch
    for (YTKRequest *req in _requestArray) {
        [req stop];
    }

    //Invoke failed proxy
    if ([_delegate respondsToSelector:@selector(batchRequestFailed:)]) {
        [_delegate batchRequestFailed:self];
    }

    //block with failed call request
    if (_failureCompletionBlock) {
        _failureCompletionBlock(self);
    }

    //Empty successful and failed block s
    [self clearCompletionBlock];

    //Invoke the end of request proxy
    [self toggleAccessoriesDidStopCallBack];

    //Remove current batch from YTKBatchRequestAgent
    [[YTKBatchRequestAgent sharedAgent] removeBatchRequest:self];
}

It is not difficult to see here that if only one request in the current batch request fails, the current batch request fails.
Callbacks (proxies and block s) that fail the current bulk request pass in an instance of the failed request.And this failed request is first assigned to the instance variable _failedRequest.

Overall, the YTKBatchRequest class uses an array to hold all request instances to be processed by the current bulk request.A success count is also used to determine whether the current batch request was successful as a whole.The failure of the current batch request is caused by the first failed instance in these request instances: as long as one request callback fails, all other requests are immediately stopped and the failed callback of the current batch request is invoked.

Now that we have finished processing the batch requests, let's look at processing the chain requests.

3.32 Chain Request

Similar to bulk requests, the class that handles chain requests is YTKChainRequest and manages instances of YTKChainRequest using the YTKChainRequestAgent singleton.

Unlike bulk requests, however, the initialization of the YTKChainRequest instance does not require passing in an array containing requests:

//YTKChainRequest.m
- (instancetype)init {

    self = [super init];
    if (self) {

        //index of Next Request
        _nextRequestIndex = 0;

        //Save an array of chain requests
        _requestArray = [NSMutableArray array];

        //Array to save callbacks
        _requestCallbackArray = [NSMutableArray array];

        //Empty callback, used to fill callback block s that are not defined by the user
        _emptyCallback = ^(YTKChainRequest *chainRequest, YTKBaseRequest *baseRequest) {
            // do nothing
        };
    }
    return self;
}

But it provides an interface to add and remove request s:

//YTKChainRequest.m
//Add request and callback to the current chain
- (void)addRequest:(YTKBaseRequest *)request callback:(YTKChainCallback)callback {

    //Save the current request
    [_requestArray addObject:request];

    if (callback != nil) {
        [_requestCallbackArray addObject:callback];
    } else {
        //An empty callback was purposely made to avoid asymmetry between the request array and the callback array if the user did not pass a value to the callback of the current request
        [_requestCallbackArray addObject:_emptyCallback];
    }
}

Note that while adding a request instance to the YTKChainRequest instance, you can also pass in a callback block.Of course, it can be left untranslated, but in order to maintain the symmetry between the request array and the callback array (since callbacks are made by retrieving the corresponding callback from the index in the request array), YTKNetwork provides us with an empty block.

Let's next look at the initiation of a chain request:

//YTKChainRequest.m
- (void)start {
    //If the first request has ended, start is no longer repeated
    if (_nextRequestIndex > 0) {
        YTKLog(@"Error! Chain request has already started.");
        return;
    }
    //If there is a request in the request queue array, take it out and start
    if ([_requestArray count] > 0) {
        [self toggleAccessoriesWillStartCallBack];
        //Take out the current request and start
        [self startNextRequest];
        //Add the current chain to the current_requestArray (YTKChainRequestAgent allows multiple chains)
        [[YTKChainRequestAgent sharedAgent] addChainRequest:self];
    } else {
        YTKLog(@"Error! Chain request array is empty.");
    }
}

As we can see, YTKChainRequest uses _nextRequestIndex to save the index of the next request, and its default value is 0.Its value is added up after the current request ends and before the following request is made.So if you have completed the first request in the request queue, you cannot start the current request queue and return immediately.

Here, the startNextRequest method is important: it is called if you decide there is a request in the request queue array:

//YTKChainRequest.m
- (BOOL)startNextRequest {
    if (_nextRequestIndex < [_requestArray count]) {
        YTKBaseRequest *request = _requestArray[_nextRequestIndex];
        _nextRequestIndex++;
        request.delegate = self;
        [request clearCompletionBlock];
        [request start];
        return YES;
    } else {
        return NO;
    }
}

This method serves two purposes:

  1. The first is to determine if the next request can be made (if the index is greater than or equal to the count of the request array, the request cannot be fetched from the request array because it would cause the array to go out of bounds)
  2. The second purpose is to initiate the next request if it can be made.And _nextRequestIndex+1.

So unlike bulk requests, the request queue for chain requests is variable and users can add requests without restrictions.YTKChainRequest will continue to send requests as long as they exist in the request queue.

Now that we know the sending of YTKChainRequest, let's take a look at the callback section:

Like YTKBatchRequest, YTKChainRequest also implements the proxy for YTKRequest:

//Implementation of a successful proxy for a request request
//YTKChainRequest.m
- (void)requestFinished:(YTKBaseRequest *)request {

    //1. Take out the current request and callback and call it back
    NSUInteger currentRequestIndex = _nextRequestIndex - 1;
    YTKChainCallback callback = _requestCallbackArray[currentRequestIndex];
    callback(self, request);//Note: This callback is only a callback to the current request, not a callback that the current chain has completely completed.The callback for the current chain is below

    //2. If the request cannot be continued, the current successful request is already the last request in the chain, that is, all callbacks in the current chain are successful, that is, the chain request is successful.
    if (![self startNextRequest]) {
        [self toggleAccessoriesWillStopCallBack];
        if ([_delegate respondsToSelector:@selector(chainRequestFinished:)]) {
            [_delegate chainRequestFinished:self];
            [[YTKChainRequestAgent sharedAgent] removeChainRequest:self];
        }
        [self toggleAccessoriesDidStopCallBack];
    }
}

We can see that after a request callback succeeds, its block is retrieved and invoked based on the index (_nextRequestIndex-1) of the current request.Next, the startNextRequest method is called to determine if there are any other requests in the current request queue for YTKChainRequest:

  • If not, the final successful callback for the current YTKChainRequest is invoked.
  • If so, initiate the next request (in order).

Next, let's look at the implementation of a proxy that failed a request:

//YTKChainRequest.m
//A proxy that failed a reqeust request
- (void)requestFailed:(YTKBaseRequest *)request {

    //If a request in the current chain fails, the current chain fails.Callback that failed to invoke the current chain
    [self toggleAccessoriesWillStopCallBack];
    if ([_delegate respondsToSelector:@selector(chainRequestFailed:failedBaseRequest:)]) {
        [_delegate chainRequestFailed:self failedBaseRequest:request];
        [[YTKChainRequestAgent sharedAgent] removeChainRequest:self];
    }
    [self toggleAccessoriesDidStopCallBack];
}

If the current request request fails, the failure callback for the current chain request is invoked immediately if it is determined that the current chain request failed.

Now that we know the requests and callbacks for chain requests, let's take a look at the termination of chain requests:

//YTKChainRequest.m
//Terminate the current chain
- (void)stop {

    //First call the callback that is about to stop
    [self toggleAccessoriesWillStopCallBack];

    //Then stop the current request, empty all requests in the chain, and drop the block
    [self clearRequest];

    //Remove the current chain in YTKChainRequestAgent
    [[YTKChainRequestAgent sharedAgent] removeChainRequest:self];

    //Final call back that has ended
    [self toggleAccessoriesDidStopCallBack];
}

This stop method can be called externally, so the user can terminate the current chain request at any time.It first calls the clearReuqest method, stops the current request, and empties the request queue array and callback array.

//YTKChainRequest.m
- (void)clearRequest {
    //Get the index of the current request
    NSUInteger currentRequestIndex = _nextRequestIndex - 1;
    if (currentRequestIndex < [_requestArray count]) {
        YTKBaseRequest *request = _requestArray[currentRequestIndex];
        [request stop];
    }
    [_requestArray removeAllObjects];
    [_requestCallbackArray removeAllObjects];
}

Then in the YTKChainRequestAgent singleton, remove yourself.

4. Last words

Reading the source code for this framework has helped me understand command patterns, blocks, what elements are required for a network request, how to design a network cache, how to design chain requests, and so on.

I remember hearing that YTKNetwork had no idea when it started making a chain request, but now it should be OK.

So reading more source code is very helpful to improve your technical level. In addition to increasing your knowledge of the API in this language, it is more meaningful that it can expose you to some new design and problem solving methods, which are things separate from a language itself and essential to being a programmer.

I hope this article will help your readers ~

Topics: JSON network encoding iOS