The original link

Note: It is recommended to read iOS Networking — NSURLSession and iOS Networking — AFNetworking before reading this article.

In the paper “iOS Networking — AFNetworking”, we introduced the core functions and principles of AFNetworking encapsulated based on NSURLSession. In this paper, we further introduce the YTKNetwork open source framework encapsulated based on AFNetworking. In this article, we read YTKNetwork source code (version number: 2.0.4).

YTKNetwork overview

YTKNetwork is an open source network request framework, which encapsulates AFNetworking internally. YTKNetwork implements a set of high-level apis to provide a higher level of network access abstraction. At present, YTKNetwork is used in iOS client of all products of Ape Question Bank company, including: Ape question bank, little ape search question, ape counseling, little Ape oral calculation, zebra series and so on.

YTKNetwork architecture

The YTKNetwork open source framework mainly consists of three parts:

  • Core functions of YTKNetwork
  • YTKNetwork Chain request
  • YTKNetwork Batch request

Among them, chain request and batch request are based on the core functions of YTKNetwork. Let’s introduce them respectively.

Core functions of YTKNetwork

The diagram above shows the reference relation diagram of YTKNetwork core function classes. The basic idea of the core functions of YTKNetwork is as follows:

  • Encapsulate each network request into an object, and each request object inherits from the YTKBaseRequest class.
  • Use the YTKNetworkAgent singleton to hold an AFHTTPSessionManager object to manage all request objects.

The core functions of YTKNetwork mainly involve three classes:

  • YTKBaseRequest
  • YTKNetworkConfig
  • YTKNetworkAgent

Let’s introduce them respectively.

YTKBaseRequest

The YTKBaseRequest class is used to represent a request object, which provides a set of properties to adequately represent a network request. We can look at the attributes it defines:

@interface YTKBaseRequest : NSObject
/// request related attributes
@property (nonatomic.strong.readonly) NSURLSessionTask *requestTask;
@property (nonatomic.strong.readonly) NSURLRequest *currentRequest;
@property (nonatomic.strong.readonly) NSURLRequest *originalRequest;
@property (nonatomic.strong.readonly) NSHTTPURLResponse *response;

/// response related attributes
@property (nonatomic.readonly) NSInteger responseStatusCode;
@property (nonatomic.strong.readonly.nullable) NSDictionary *responseHeaders;
@property (nonatomic.strong.readonly.nullable) NSData *responseData;
@property (nonatomic.strong.readonly.nullable) NSString *responseString;
@property (nonatomic.strong.readonly.nullable) id responseObject;
@property (nonatomic.strong.readonly.nullable) id responseJSONObject;

/ / /
@property (nonatomic.strong.readonly.nullable) NSError *error;

/ / / state
@property (nonatomic.readonly.getter=isCancelled) BOOL cancelled;
@property (nonatomic.readonly.getter=isExecuting) BOOL executing;

/// Identifier, default is 0
@property (nonatomic) NSInteger tag;

/// Additional information, default is nil
@property (nonatomic.strong.nullable) NSDictionary *userInfo;

/ / / agent
@property (nonatomic.weak.nullable) id<YTKRequestDelegate> delegate;

/// Successful/failed callback
@property (nonatomic.copy.nullable) YTKRequestCompletionBlock successCompletionBlock;
@property (nonatomic.copy.nullable) YTKRequestCompletionBlock failureCompletionBlock;

/// Used to build the HTTP body on POST requests. The default is nil
@property (nonatomic.copy.nullable) AFConstructingBlock constructingBodyBlock;

/// Used to specify the local download path when downloading tasks
@property (nonatomic.strong.nullable) NSString *resumableDownloadPath;

/// a callback to track download progress
@property (nonatomic.copy.nullable) AFURLSessionTaskProgressBlock resumableDownloadProgressBlock;

/// Request priority
@property (nonatomic) YTKRequestPriority requestPriority;

/// YTKRequestAccessory is a protocol that states three methods that developers are allowed to call separately during the three phases of the request execution (start, willStop, didStop).
@property (nonatomic.strong.nullable) NSMutableArray<id<YTKRequestAccessory>> *requestAccessories;

@end
Copy the code

In fact, the YTKBaseRequest class is wrapped around the NSURLSessionTask class, and requestTask is its most important attribute. The other YTKBaseRequest attributes are derived from requestTask attributes. Such as:

  • currentRequest: that is,requestTask.currentRequest
  • originalRequest: that is,requestTask.originalRequest
  • response: that is,requestTask.response
  • responseHeaders: that is,requestTask.allHeaderFields
  • responseStatusCode: that is,requestTask.statusCode

YTKBaseRequest provides high-level network abstraction by providing some high-level configuration methods and allowing users to override these methods to customize the configuration. Some common configuration methods are as follows:

/// BaseURL, because the BaseURL of network requests in an application is almost always the same.
- (NSString *)baseUrl {
    return @ "";
}

/// The requested URL path
- (NSString *)requestUrl {
    return @ "";
}

/// Network request timeout interval. The default 60 seconds
- (NSTimeInterval)requestTimeoutInterval {
    return 60;
}

/// HTTP request method. The default is GET
- (YTKRequestMethod)requestMethod {
    return YTKRequestMethodGET;
}

/// Request serializer type. The default is the HTTP
- (YTKRequestSerializerType)requestSerializerType {
    return YTKRequestSerializerTypeHTTP;
}

/// Response serializer type. The default is JSON
- (YTKResponseSerializerType)responseSerializerType {
    return YTKResponseSerializerTypeJSON;
}

/// The request parameter object is encoded according to the configured request serializer.
- (id)requestArgument {
    return nil;
}

// Whether cellular networks are allowed. The default YES
- (BOOL)allowsCellularAccess {
    return YES;
}

// Whether to use CDN. The default NO
- (BOOL)useCDN {
    return NO;
}

/ / / CDN urls. The useCDN determines whether to use it or not.
- (NSString *)cdnUrl {
    return @ ""; }...Copy the code

It also provides several simple methods for the execution of the YTKBaseRequest object that developers can use, as shown below. Using the start method, we can see that YTKBaseRequest is added to the YTKNetworkAgent singleton. YTKNetworkAgent manages multiple YTKBaseRequest objects.

// YTKBaseRequest starts executing
- (void)start {
    Execute the requestWillStart: method defined in the YTKRequestAccessory agreement.
    [self toggleAccessoriesWillStartCallBack];
    // Add the request object to the YTKNetworkAgent singleton
    [[YTKNetworkAgent sharedAgent] addRequest:self];
}

/// YTKBaseRequest stops execution
- (void)stop {
    Execute the requestWillStop: method defined in the YTKRequestAccessory agreement.
    [self toggleAccessoriesWillStopCallBack];
    self.delegate = nil;
    [[YTKNetworkAgent sharedAgent] cancelRequest:self];
    Execute the requestDidStop: method defined in the YTKRequestAccessory agreement.
    [self toggleAccessoriesDidStopCallBack];
}

/// a convenient method. Perform YTKBaseRequest.
- (void)startWithCompletionBlockWithSuccess:(YTKRequestCompletionBlock)success
                                    failure:(YTKRequestCompletionBlock)failure {
    [self setCompletionBlockWithSuccess:success failure:failure];
    [self start];
}
Copy the code

YTKNetworkConfig

YTKNetworkConfig is a singleton used for YTKNetworkAgent initialization.

YTKNetworkConfig contains the following attributes:

@interface YTKNetworkConfig : NSObject

/// The requested Base URL. The default is ""
@property (nonatomic.strong) NSString *baseUrl;

/// CDN URL. Default is ""
@property (nonatomic.strong) NSString *cdnUrl;

/// URL filter. The filterUrl:withRequest: method declared by YTKUrlFilterProtocol returns the URL that was ultimately used
@property (nonatomic.strong.readonly) NSArray<id<YTKUrlFilterProtocol>> *urlFilters;

/// Cache path filter. YTKCacheDirPathFilterProtocol filterCacheDirPath statement: withRequest: method returns was eventually use cache path.
@property (nonatomic.strong.readonly) NSArray<id<YTKCacheDirPathFilterProtocol>> *cacheDirPathFilters;

/// Security policy.
@property (nonatomic.strong) AFSecurityPolicy *securityPolicy;

/// Whether to print debug logs. The default is NO
@property (nonatomic) BOOL debugLogEnabled;

/// Session configuration object
@property (nonatomic.strong) NSURLSessionConfiguration* sessionConfiguration;

@end
Copy the code

YTKNetworkConfig holds a NSURLSessionConfiguration type attribute sessionConfiguration, Used to initialize AFHTTPSessionManager in YTKNetworkAgent (essentially used to initialize NSURLSession).

YTKNetworkAgent

The following figure shows the internal structure of YTKNetworkAgent. We will use this figure as a guide for the introduction.

Initialize the

The YTKNetworkAgent initialization process uses the YTKNetworkConfig singleton object (configuration object). Initialize the session manager AFHTTPSessionManager with the sessionConfiguration object sessionConfiguration of the configuration object.

The YTKNetwork framework can only use the YTKNetworkAgent singleton by default.

Add and execute the request

YTKNetworkAgent provides the addRequest: method to add and execute the request object. We can look at the internal implementation.

- (void)addRequest:(YTKBaseRequest *)request {
    NSParameterAssert(request ! =nil);

    NSError * __autoreleasing requestSerializationError = nil;

    // Initialize the key attribute of the request object, requestTask, that is, the task object
    NSURLRequest *customUrlRequest= [request buildCustomUrlRequest];
    if (customUrlRequest) {
        __block NSURLSessionDataTask *dataTask = nil;
        dataTask = [_manager dataTaskWithRequest:customUrlRequest completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
            // Complete the callback
            [self handleRequestResult:dataTask responseObject:responseObject error:error];
        }];
        request.requestTask = dataTask;
    } else {
        // Default mode
        request.requestTask = [self sessionTaskForRequest:request error:&requestSerializationError];
    }

    // Request serialization exception handling
    if (requestSerializationError) {
        [self requestDidFailWithRequest:request error:requestSerializationError];
        return;
    }

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

    // Set the request priority
    / /!!!!! 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;
        }
    }

    YTKLog(@"Add request: %@".NSStringFromClass([request class]));
    // Add the request object to the record table
    [self addRequestToRecord:request];
    // Execute the request, that is, execute the task object
    [request.requestTask resume];
}
Copy the code

AddRequest: The method does several steps inside:

  1. Initialize key attributes of the request objectrequestTask, the task object.
  2. Set the request priority
  3. To the task objecttaskIdentifierIs the key, the request object is the value, establish the mapping relationship, saverecord(As shown above_requestRecord, more on that later).
  4. Executing a request is essentially executing a task object.

Let’s focus on step 1. This step is the default calling the sessionTaskForRequest: error: method to initialize. The internal implementation of this method is as follows:

- (NSURLSessionTask *)sessionTaskForRequest:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error {
    // Get the request method
    YTKRequestMethod method = [request requestMethod];
    // Get the request URL
    NSString *url = [self buildRequestUrl:request];
    // Get the request parameters
    id param = request.requestArgument;
    // Get the HTTP body
    AFConstructingBlock constructingBlock = [request constructingBodyBlock];
    // Get the request serializer
    AFHTTPRequestSerializer *requestSerializer = [self requestSerializerForRequest:request];

    // Initializes the corresponding task object according to the request method and the download path value
    switch (method) {
        case YTKRequestMethodGET:
            if (request.resumableDownloadPath) {
                return [self downloadTaskWithDownloadPath:request.resumableDownloadPath requestSerializer:requestSerializer URLString:url parameters:param progress:request.resumableDownloadProgressBlock error:error];
            } else {
                return [self dataTaskWithHTTPMethod:@"GET" requestSerializer:requestSerializer URLString:url parameters:param error:error];
            }
        case YTKRequestMethodPOST:
            return [self dataTaskWithHTTPMethod:@"POST" requestSerializer:requestSerializer URLString:url parameters:param constructingBodyWithBlock:constructingBlock error:error];
        case YTKRequestMethodHEAD:
            return [self dataTaskWithHTTPMethod:@"HEAD" requestSerializer:requestSerializer URLString:url parameters:param error:error];
        case YTKRequestMethodPUT:
            return [self dataTaskWithHTTPMethod:@"PUT" requestSerializer:requestSerializer URLString:url parameters:param error:error];
        case YTKRequestMethodDELETE:
            return [self dataTaskWithHTTPMethod:@"DELETE" requestSerializer:requestSerializer URLString:url parameters:param error:error];
        case YTKRequestMethodPATCH:
            return [self dataTaskWithHTTPMethod:@"PATCH"requestSerializer:requestSerializer URLString:url parameters:param error:error]; }}Copy the code

SessionTaskForRequest: error: method will be initialized according to the request object requestMethod corresponding task object. In a POST request, for example, here will call dataTaskWithHTTPMethod: requestSerializer: URLString: parameters: constructingBodyWithBlock: error: method. Its internal implementation is as follows:

- (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;

    // Initializes a URLRequest object
    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];
    }

    // Use the URLRequest object to initialize the task object and return it
    __block NSURLSessionDataTask *dataTask = nil;
    dataTask = [_manager dataTaskWithRequest:request
                           completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *_error) {
                                // Set the callback to complete
                               [self handleRequestResult:dataTask responseObject:responseObject error:_error];
                           }];

    return dataTask;
}
Copy the code

dataTaskWithHTTPMethod:requestSerializer:URLString:parameters:constructingBodyWithBlock:error: The URLRequest () method initializes a URLRequest object from the input parameter, uses the object to initialize a task object, and returns the task object.

Complete the callback

The dataTaskWithHTTPMethod: requestSerializer: URLString: parameters: constructingBodyWithBlock: error: method, the initialization task object will set up the callback.

Let’s take a look at what’s done to complete the callback.

- (void)handleRequestResult:(NSURLSessionTask *)task responseObject:(id)responseObject error:(NSError *)error {
    Lock();
    // Get the request object from the record table according to the task object's taskIdentifier.
    YTKBaseRequest *request = _requestsRecord[@(task.taskIdentifier)];
    Unlock();

    if(! request) {return;
    }

    YTKLog(@"Finished Request: %@".NSStringFromClass([request class]));

    NSError * __autoreleasing serializationError = nil;
    NSError * __autoreleasing validationError = nil;

    NSError *requestError = nil;
    BOOL succeed = NO;

    // Serialize the response data according to the different response serializer
    request.responseObject = responseObject;
    if ([request.responseObject isKindOfClass:[NSData class]]) {
        request.responseData = responseObject;
        request.responseString = [[NSString alloc] initWithData:responseObject encoding:[YTKNetworkUtils stringEncodingWithRequest:request]];

        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; }}// Check whether the request was successful and get the request exception
    if (error) {
        succeed = NO;
        requestError = error;
    } else if (serializationError) {
        succeed = NO;
        requestError = serializationError;
    } else {
        succeed = [self validateResult:request error:&validationError];
        requestError = validationError;
    }

    // Call request processing successfully or call request processing failed
    if (succeed) {
        [self requestDidSucceedWithRequest:request];
    } else{[self requestDidFailWithRequest:request error:requestError];
    }

    // Delete the request object from the record table
    dispatch_async(dispatch_get_main_queue(), ^{
        [self removeRequestFromRecord:request];
        [request clearCompletionBlock];
    });
}
Copy the code

In this callback, the following work is done:

  1. According to the task objecttaskIdentifierFrom the form_requestRecordGets the request object from.
  2. For the obtained request object, the response data is serialized according to the different response serializer.
  3. Check that the request was successful and get the request exception.
  4. Call request successful processing or call request failed processing
  5. Deletes the request object from the record table.

Step 4, either a successful callback or a failed callback, calls the requestFinished implementation of the proxy object in turn: Or requestFailed, and successCompletionBlock or failureCompletionBlock for the request object.

Download tasks and cache

About download task, let’s take a look at the above sessionTaskForRequest: error: Method, when the request object’s request type is YTKRequestMethodGET and its resumableDownloadPath property is set, Invokes the downloadTaskWithDownloadPath: requestSerializer: URLString: parameters: progress: error: method. The concrete implementation of this method is as follows:

- (NSURLSessionDownloadTask *)downloadTaskWithDownloadPath:(NSString *)downloadPath
                                         requestSerializer:(AFHTTPRequestSerializer *)requestSerializer
                                                 URLString:(NSString *)URLString
                                                parameters:(id)parameters
                                                  progress:(nullable void(^) (NSProgress *downloadProgress))downloadProgressBlock
                                                     error:(NSError * _Nullable __autoreleasing *)error {
    // Initializes the URLRequest object with request parameters, request URL, and request type
    NSMutableURLRequest *urlRequest = [requestSerializer requestWithMethod:@"GET" URLString:URLString parameters:parameters error:error];

    NSString *downloadTargetPath;
    // Check whether the download storage path specified by a resumableDownloadPath is a directory
    BOOL isDirectory;
    if(! [[NSFileManager defaultManager] fileExistsAtPath:downloadPath isDirectory:&isDirectory]) {
        isDirectory = NO;
    }
    // Preprocess the download storage path to ensure that it is not a directory, but a file
    if (isDirectory) {
        NSString *fileName = [urlRequest.URL lastPathComponent];
        downloadTargetPath = [NSString pathWithComponents:@[downloadPath, fileName]];
    } else {
        downloadTargetPath = downloadPath;
    }
    
    // Clear the original files in this path
    if ([[NSFileManager defaultManager] fileExistsAtPath:downloadTargetPath]) {
        [[NSFileManager defaultManager] removeItemAtPath:downloadTargetPath error:nil];
    }

    // Check whether there is any data in the pending download path and read the data temporarily stored in this path
    BOOL resumeDataFileExists = [[NSFileManager defaultManager] fileExistsAtPath:[self incompleteDownloadTempPathForDownloadPath:downloadPath].path];
    NSData *data = [NSData dataWithContentsOfURL:[self incompleteDownloadTempPathForDownloadPath:downloadPath]];
    BOOL resumeDataIsValid = [YTKNetworkUtils validateResumeData:data];

    BOOL canBeResumed = resumeDataFileExists && resumeDataIsValid;
    BOOL resumeSucceeded = NO;
    __block NSURLSessionDownloadTask *downloadTask = nil;
    if (canBeResumed) {
        // For recoverable download requests, initialize a download task with the downloaded data and proceed with the download request.
        @try {
            downloadTask = [_manager downloadTaskWithResumeData:data progress:downloadProgressBlock destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
                return [NSURL fileURLWithPath:downloadTargetPath isDirectory:NO];
            } completionHandler:
                            ^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
                                [self handleRequestResult:downloadTask responseObject:filePath error:error];
                            }];
            resumeSucceeded = YES;
        } @catch (NSException *exception) {
            YTKLog(@"Resume download failed, reason = %@", exception.reason);
            resumeSucceeded = NO; }}if(! resumeSucceeded) {// If the attempt to continue the download fails, create a download task and restart the download request.
        downloadTask = [_manager downloadTaskWithRequest:urlRequest progress:downloadProgressBlock destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
            // Specify the path to store the download
            return [NSURL fileURLWithPath:downloadTargetPath isDirectory:NO];
        } completionHandler:
                        ^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
                            [self handleRequestResult:downloadTask responseObject:filePath error:error];
                        }];
    }
    return downloadTask;
}
Copy the code

There are three key steps in creating a download task:

  1. Make sure the download is stored in a file path, not a directory path.
  2. Read the data in the pending download path and determine whether the download can continue.
  3. If the download can continue, create a request to continue the download; Otherwise, create a request to re-download.

From the above code, we can see that there are two possible download storage paths:

  1. resumableDownloadPath
  2. resumableDownloadPath + filename

So what is the pending download staging path? Let’s look at the code:

- (NSString *)incompleteDownloadTempCacheFolder {
    NSFileManager *fileManager = [NSFileManager new];
    static NSString *cacheFolder;

    if(! cacheFolder) {NSString *cacheDir = NSTemporaryDirectory(a); cacheFolder = [cacheDir stringByAppendingPathComponent:kYTKNetworkIncompleteDownloadFolderName]; }NSError *error = nil;
    if(! [fileManager createDirectoryAtPath:cacheFolder withIntermediateDirectories:YES attributes:nil error:&error]) {
        YTKLog(@"Failed to create cache directory at %@", cacheFolder);
        cacheFolder = nil;
    }
    return cacheFolder;
}

- (NSURL *)incompleteDownloadTempPathForDownloadPath:(NSString *)downloadPath {
    NSString *tempPath = nil;
    NSString *md5URLString = [YTKNetworkUtils md5StringFromString:downloadPath];
    tempPath = [[self incompleteDownloadTempCacheFolder] stringByAppendingPathComponent:md5URLString];
    return [NSURL fileURLWithPath:tempPath];
}
Copy the code

From the above code, we can see that the incomplete download temporary path is actually:

  • NSTemporaryDirectory()+ Download the MD5 value of the directory stored in the path

Note that the NSTemporaryDirectory() directory is the/TMP directory in UNIX, and the files in this directory will be emptied after the system restarts.

YTKNetwork Chain request

Chain request is mainly realized through two classes provided by YTKNetwork and combined with the core functions of YTKNetwork. The two categories are:

  • YTKChainRequest
  • YTKChainRequestAgent

Next, we introduce YTKChainRequest and YTKChainRequestAgent respectively.

YTKChainRequest

YTKChainRequest inherits from NSObject and mainly contains these properties.

/// Expose attributes
@interface YTKChainRequest : NSObject

/// proxy object
@property (nonatomic.weak.nullable) id<YTKChainRequestDelegate> delegate;

/// YTKRequestAccessory is a protocol that states three methods that developers are allowed to call separately during the three phases of the request execution (start, willStop, didStop).
@property (nonatomic.strong.nullable) NSMutableArray<id<YTKRequestAccessory>> *requestAccessories;

@end

/// ------------------------------------------

/// Private attributes
@interface YTKChainRequest()"YTKRequestDelegate>

// queued requests
@property (strong.nonatomic) NSMutableArray<YTKBaseRequest *> *requestArray;

/// the callback queue is chained
@property (strong.nonatomic) NSMutableArray<YTKChainCallback> *requestCallbackArray;

/// 
@property (assign.nonatomic) NSUInteger nextRequestIndex;
@property (strong.nonatomic) YTKChainCallback emptyCallback;

@end
Copy the code

YTKChainRequest offers four methods.

/// get the chained request queue
- (NSArray<YTKBaseRequest *> *)requestArray;

// add an object that implements the YTKRequestAccessory agreement
- (void)addAccessory:(id<YTKRequestAccessory>)accessory;

/// start executing the chain request
- (void)start;

/// stop executing the chain request
- (void)stop;

// add a request to the chained request queue
- (void)addRequest:(YTKBaseRequest *)request callback:(nullable YTKChainCallback)callback;
Copy the code

Let’s take a look at the key start method in the source code.

- (void)start {
    // Determine if the chain request has been started
    if (_nextRequestIndex > 0) {
        YTKLog(@"Error! Chain request has already started.");
        return;
    }

    // If the chained request queue is not empty, the request is executed
    if ([_requestArray count] > 0) {[self toggleAccessoriesWillStartCallBack];
        [self startNextRequest];
        [[YTKChainRequestAgent sharedAgent] addChainRequest:self];
    } else {
        YTKLog(@"Error! Chain request array is empty."); }}Copy the code

Internally, the start method first determines whether the chained request has been started, using the request index _nextRequestIndex. If the chained request is not started, the chained request is executed, where a key method, startNextRequest, is invoked.

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

Each time startNextRequest is called, the request index is moved, the request broker is set, and executed.

The proxy for each request YTKBaseRequest in a chain request is a chain request YTKChainRequest. YTKChainRequest implements the YTKRequestDelegate protocol. After each request is completed, the next request is executed. If one request fails, the entire chain request fails.

- (void)requestFinished:(YTKBaseRequest *)request {
    NSUInteger currentRequestIndex = _nextRequestIndex - 1;
    YTKChainCallback callback = _requestCallbackArray[currentRequestIndex];
    callback(self, request);
    // Execute the next request
    if(! [self startNextRequest]) {
        [self toggleAccessoriesWillStopCallBack];
        if ([_delegate respondsToSelector:@selector(chainRequestFinished:)]) {
            // Call the proxy method chainRequestFinished when all requests are completed:
            [_delegate chainRequestFinished:self];
            [[YTKChainRequestAgent sharedAgent] removeChainRequest:self];
        }
        [selftoggleAccessoriesDidStopCallBack]; }} - (void)requestFailed:(YTKBaseRequest *)request {
    [self toggleAccessoriesWillStopCallBack];
    if ([_delegate respondsToSelector:@selector(chainRequestFailed:failedBaseRequest:)]) {
        // There was a request that failed, i.e. ChainRequestFailed:
        [_delegate chainRequestFailed:self failedBaseRequest:request];
        [[YTKChainRequestAgent sharedAgent] removeChainRequest:self];
    }
    [self toggleAccessoriesDidStopCallBack];
}
Copy the code

YTKChainRequestAgent

The purpose of the YTKChainRequestAgent is very simple, as a singleton, holding multiple chained requests. YTKChainRequestAgent provides the following methods:

+ (YTKChainRequestAgent *)sharedAgent;

/// add a chained request
- (void)addChainRequest:(YTKChainRequest *)request;

/// remove the chained request
- (void)removeChainRequest:(YTKChainRequest *)request;
Copy the code

YTKNetwork Batch request

The implementation principle of YTKNetwork batch request is actually the same as the implementation principle of chain request, and also provides two classes:

  • YTKBatchRequest
  • YTKBatchRequestAgent

The difference is that the individual request in YTKBatchRequest is not a YTKBaseRequest request, but a subclass of it, YTKRequest.

Let’s look at what YTKRequest does based on its parent YTKBaseRequest.

YTKRequest

First, let’s look at the external properties and methods provided by YTKRequest.

@interface YTKRequest : YTKBaseRequest

// Whether to ignore the cache
@property (nonatomic) BOOL ignoreCache;

/// whether the request response data is from the local cache
- (BOOL)loadCacheWithError:(NSError * __autoreleasing *)error;
/// The request does not use cached data
- (void)startWithoutCache;
// save the response data to the cache
- (void)saveResponseDataToCacheFile:(NSData *)data;

#pragma mark - Subclass Override

/// cache time
- (NSInteger)cacheTimeInSeconds;
/// Cache version
- (long long)cacheVersion;
// cache sensitive data, used to verify whether the cache is invalid
- (nullable id)cacheSensitiveData;
// whether to write to cache asynchronously
- (BOOL)writeCacheAsynchronously;

@end
Copy the code

Obviously, YTKRequest supports local caching on top of the parent class.

The cache directory

Let’s focus on the relevant cache directories in YTKRequest. Let’s start with the following methods:


- (NSString *)cacheBasePath {
    NSString *pathOfLibrary = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory.NSUserDomainMask.YES) objectAtIndex:0];
    NSString *path = [pathOfLibrary stringByAppendingPathComponent:@"LazyRequestCache"];

    // Filter cache base path
    NSArray<id<YTKCacheDirPathFilterProtocol>> *filters = [[YTKNetworkConfig sharedConfig] cacheDirPathFilters];
    if (filters.count > 0) {
        for (id<YTKCacheDirPathFilterProtocol> f in filters) {
            path = [f filterCacheDirPath:path withRequest:self]; }} [self createDirectoryIfNeeded:path];
    return path;
}

- (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;
}

- (NSString *)cacheFilePath {
    NSString *cacheFileName = [self cacheFileName];
    NSString *path = [self cacheBasePath];
    path = [path stringByAppendingPathComponent:cacheFileName];
    return path;
}

- (NSString *)cacheMetadataFilePath {
    NSString *cacheMetadataFileName = [NSString stringWithFormat:@"%@.metadata"[self cacheFileName]];
    NSString *path = [self cacheBasePath];
    path = [path stringByAppendingPathComponent:cacheMetadataFileName];
    return path;
}
Copy the code

By default, cacheBasePath method returns: is the basic path of/Library/LazyRequestCache.

The cacheFileName Method generates the file name of the cache based on the basic requested information: Method: XXX Host: XXX Url: XXX Argument: XXX and uses MD5 encoding.

CacheFilePath is the complete request data storage path: / Library/LazyRequestCache / + md5 (Url: Host: Method: XXX XXX XXX Argument: XXX).

CacheMetadataFilePath stores cache metadata in the cacheFilePath +.medata directory.

The cache metadata is represented by the YTKCacheMetaData object, which is defined as follows:

@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
Copy the code

YTKCacheMetaData Primary user verifies that the cache is valid. The verification method is as follows:

- (BOOL)validateCacheWithError:(NSError * _Nullable __autoreleasing *)error {
    // Date
    NSDate *creationDate = self.cacheMetadata.creationDate;
    NSTimeInterval duration = -[creationDate timeIntervalSinceNow];
    if (duration < 0 || duration > [self cacheTimeInSeconds]) {
        // ...
        return NO;
    }
    // Version
    long long cacheVersionFileContent = self.cacheMetadata.version;
    if(cacheVersionFileContent ! = [self cacheVersion]) {
        // ...
        return NO;
    }
    // Sensitive data
    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]) {// ...
            return NO; }}// App version
    NSString *appVersionString = self.cacheMetadata.appVersionString;
    NSString *currentAppVersionString = [YTKNetworkUtils appVersionString];
    if (appVersionString || currentAppVersionString) {
        if(appVersionString.length ! = currentAppVersionString.length || ! [appVersionString isEqualToString:currentAppVersionString]) {// ...
            return NO; }}return YES;
}
Copy the code

conclusion

The design principle of YTKNetwork is very simple. It is just a simple encapsulation of AFNetworking and provides an object-oriented method of use. The downside is that you need to define a class for each request.

reference

  1. YTKNetwork