AFURLSession Manager, the Core of AFNetworking (II)

Keywords: Session network iOS

AFURLSession Manager is absolutely the core of AFNetworking.

  1. Responsible for creating and managing NSURLSession
  2. Managing NSURLSession Task
  3. Implementation of proxy methods in NSURLSession Delegate and other protocols
  4. Managing progress using AFURLSession Manager Task Delegate
  5. Using _AFURLSession TaskSwizzling Adjustment Method
  6. Introducing AFSecurity Policy to Ensure the Security of Requests
  7. Introducing AFNetwork Reachability Manager to monitor network status

Here we will focus on the first five of the above seven functions and analyze how they are packaged. NSURLSession and many proxy methods.


Create and manage NSURLSession

When using AFURLSession Manager, the first thing to do is to initialize:


- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration {
    self = [super init];
    if (!self) {
        return nil;
    }

    if (!configuration) {
        configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
    }

    self.sessionConfiguration = configuration;

    self.operationQueue = [[NSOperationQueue alloc] init];
    self.operationQueue.maxConcurrentOperationCount = 1;

    self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];

    self.responseSerializer = [AFJSONResponseSerializer serializer];

    self.securityPolicy = [AFSecurityPolicy defaultPolicy];

    self.reachabilityManager = [AFNetworkReachabilityManager sharedManager];

    self.mutableTaskDelegatesKeyedByTaskIdentifier = [[NSMutableDictionary alloc] init];

    self.lock = [[NSLock alloc] init];
    self.lock.name = AFURLSessionManagerLockName;

    #1: Set up agents for existing task s.

    return self;
}



In the initialization method, we need to complete the initialization of some instances we own:

  1. Initialize session configuration (NSURLSession Configuration), default Session Configuration
  2. Initialize the session and set up the session's proxy and proxy queue
  3. Examples of AFJSONResponse Serializer, AFSecurity Policy, and AFNetwork Reachability Manager
  4. Initialize the dictionary that saves data task (mutable Task Delegates KeyedByTaskIdentifier)

Managing NSURLSession Task

Next, after obtaining an example of AFURLSession Manager, we can create an instance of NSURLSession DataTask by:

- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request
                               uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgressBlock
                             downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgressBlock
                            completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject,  NSError * _Nullable error))completionHandler;

- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request
                                         fromFile:(NSURL *)fileURL
                                         progress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgressBlock
                                completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject, NSError  * _Nullable error))completionHandler;

...

- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request
                                             progress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgressBlock
                                          destination:(nullable NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination
                                    completionHandler:(nullable void (^)(NSURLResponse *response, NSURL * _Nullable filePath, NSError * _Nullable error))completionHandler;

...


Some methods for returning NSURLSessionTask are omitted here, because these interfaces are in the same form.

We will take the implementation of the -[AFURLSession Manager dataTaskWithRequest: uploadProgress: download Progress: completionHandler:] method as an example to analyze how it instantiates and returns a Examples of NSURLSessionTask:


- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request
                               uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgressBlock
                             downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgressBlock
                            completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject,  NSError * _Nullable error))completionHandler {

    __block NSURLSessionDataTask *dataTask = nil;
    url_session_manager_create_task_safely(^{
        dataTask = [self.session dataTaskWithRequest:request];
    });

    [self addDelegateForDataTask:dataTask uploadProgress:uploadProgressBlock downloadProgress:downloadProgressBlock completionHandler:completionHandler];

    return dataTask;
}


Note: url_session_manager_create_task_safe is called because of a bug in the Apple Framework #2093 If you're interested in it, it's not explained here.


  1. Invoke the - [NSURLSession dataTaskWithRequest:] method to pass in NSURLRequest
  2. Calling the method - [AFURLSession Manager addDelegate ForDataTask: uploadProgress: downloadProgress: completionHandler:] returns an AFURLSession Manager TaskDelegate object
  3. Pass completionHandler uploadProgressBlock and download ProgressBlock into the object and call back when the corresponding event occurs



- (void)addDelegateForDataTask:(NSURLSessionDataTask *)dataTask
                uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgressBlock
              downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgressBlock
             completionHandler:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler
{
    AFURLSessionManagerTaskDelegate *delegate = [[AFURLSessionManagerTaskDelegate alloc] init];
    delegate.manager = self;
    delegate.completionHandler = completionHandler;

    dataTask.taskDescription = self.taskDescriptionForSessionTasks;
    [self setDelegate:delegate forTask:dataTask];

    delegate.uploadProgressBlock = uploadProgressBlock;
    delegate.downloadProgressBlock = downloadProgressBlock;
}

In this method, another method - [AFURLSession Manager setDelegate: forTask:] is called to set up the agent at the same time:


- (void)setDelegate:(AFURLSessionManagerTaskDelegate *)delegate
            forTask:(NSURLSessionTask *)task
{

    #1: Check parameters, Brief

    [self.lock lock];
    self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)] = delegate;
    [delegate setupProgressForTask:task];
    [self addNotificationObserverForTask:task];
    [self.lock unlock];
}


As mentioned above, AFNRUSession Manager stores and manages each NSURLSession Task through the dictionary mutable Task Delegates KeyedByTaskIdentifier, which uses task Identifier. Store task s for keys.

This method uses NSLock to ensure the use of different threads When mutable Task Delegates KeyedByTaskIdentifier, there is no thread competition problem.

Simultaneous invocation - setupProgressForTask: We will introduce this method in the body below.


Implementation of proxy methods in NSURLSession Delegate and other protocols

As you can see in the header file of AFURLSession Manager, it follows a number of protocols, including:

  • NSURLSessionDelegate
  • NSURLSessionTaskDelegate
  • NSURLSessionDataDelegate
  • NSURLSessionDownloadDelegate

It points the agent of NSURLSession to self in the initialization method - [AFURLSession Manager initWithSession Configuration:], and then implements these methods to provide more concise block interface:


- (void)setSessionDidBecomeInvalidBlock:(nullable void (^)(NSURLSession *session, NSError *error))block;
- (void)setSessionDidReceiveAuthenticationChallengeBlock:(nullable NSURLSessionAuthChallengeDisposition (^)(NSURLSession *session, NSURLAuthenticationChallenge *challenge, NSURLCredential * _Nullable __autoreleasing * _Nullable credential))block;
...

It provides the corresponding block interface for all proxy protocols. The idea of implementation is similar.- [AFNRLSession Manager setSession Did BecomeInvalid Block:] For example.

First, the setter method is called and the block is stored in the session DidBecomeInvalid property:

- (void)setSessionDidBecomeInvalidBlock:(void (^)(NSURLSession *session, NSError *error))block {
    self.sessionDidBecomeInvalid = block;
}

When a proxy method is called, if there is a corresponding block, the corresponding block is executed:


- (void)URLSession:(NSURLSession *)session
didBecomeInvalidWithError:(NSError *)error
{
    if (self.sessionDidBecomeInvalid) {
        self.sessionDidBecomeInvalid(session, error);
    }

    [[NSNotificationCenter defaultCenter] postNotificationName:AFURLSessionDidInvalidateNotification object:session];
}

Other similar interface implementations are similar, skipped here directly.

Managing progress using AFURLSession Manager Task Delegate

We mentioned the AFURLSession Manager TaskDelegate class above, which provides schedule management for task and calls back at the end of task, that is to say, call back at the end of task.- [AFURLSession Manager data Task WithRequest: uploadProgress: download Progress: completionHandler:] and other methods passed in the completionHandler.


Let's first analyze how AFURLSession Manager Task Delegate tracks progress:


- (void)setupProgressForTask:(NSURLSessionTask *)task {

    #1: Set up callbacks when upload progress or download progress status changes

    #2: KVO

}

The implementation of this method has two parts, one is to set callbacks to the two attributes uploadProgress and download Progress held by the agent.


__weak __typeof__(task) weakTask = task;

self.uploadProgress.totalUnitCount = task.countOfBytesExpectedToSend;
[self.uploadProgress setCancellable:YES];
[self.uploadProgress setCancellationHandler:^{
   __typeof__(weakTask) strongTask = weakTask;
   [strongTask cancel];
}];
[self.uploadProgress setPausable:YES];
[self.uploadProgress setPausingHandler:^{
   __typeof__(weakTask) strongTask = weakTask;
   [strongTask suspend];
}];
if ([self.uploadProgress respondsToSelector:@selector(setResumingHandler:)]) {
   [self.uploadProgress setResumingHandler:^{
       __typeof__(weakTask) strongTask = weakTask;
       [strongTask resume];
   }];
}

Only the code to set the callback for uploadProgress is set here, and the setup for download Progress is exactly the same as here.


The main purpose is to call resume when the state of NSProgress changes suspend and other methods change the state of task.


The second part is to observe the key values of task and NSProgress attributes.


[task addObserver:self
      forKeyPath:NSStringFromSelector(@selector(countOfBytesReceived))
         options:NSKeyValueObservingOptionNew
         context:NULL];
[task addObserver:self
      forKeyPath:NSStringFromSelector(@selector(countOfBytesExpectedToReceive))
         options:NSKeyValueObservingOptionNew
         context:NULL];

[task addObserver:self
      forKeyPath:NSStringFromSelector(@selector(countOfBytesSent))
         options:NSKeyValueObservingOptionNew
         context:NULL];
[task addObserver:self
      forKeyPath:NSStringFromSelector(@selector(countOfBytesExpectedToSend))
         options:NSKeyValueObservingOptionNew
         context:NULL];

[self.downloadProgress addObserver:self
                       forKeyPath:NSStringFromSelector(@selector(fractionCompleted))
                          options:NSKeyValueObservingOptionNew
                          context:NULL];
[self.uploadProgress addObserver:self
                     forKeyPath:NSStringFromSelector(@selector(fractionCompleted))
                        options:NSKeyValueObservingOptionNew
                        context:NULL];

Change progress in observeValueForKeypath:ofObject:change:context: method and call block


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
    if ([object isKindOfClass:[NSURLSessionTask class]]) {
        if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesReceived))]) {
            self.downloadProgress.completedUnitCount = [change[@"new"] longLongValue];
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesExpectedToReceive))]) {
            self.downloadProgress.totalUnitCount = [change[@"new"] longLongValue];
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesSent))]) {
            self.uploadProgress.completedUnitCount = [change[@"new"] longLongValue];
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesExpectedToSend))]) {
            self.uploadProgress.totalUnitCount = [change[@"new"] longLongValue];
        }
    }
    else if ([object isEqual:self.downloadProgress]) {
        if (self.downloadProgressBlock) {
            self.downloadProgressBlock(object);
        }
    }
    else if ([object isEqual:self.uploadProgress]) {
        if (self.uploadProgressBlock) {
            self.uploadProgressBlock(object);
        }
    }
}

Update the NSProgress object when some attributes of the object change or pass the NSProgress object self.uploadProgressBlock(object) with a block.

The proxy method URLSession:task:didCompleteWithError:

At the end of each NSURLSession Task, the proxy method URLSession:task:didCompleteWithError:

  1. Call the incoming completionHander block
  2. Issue AF Networking Task Did Complete Notification Notification Notification Notification

- (void)URLSession:(__unused NSURLSession *)session
              task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
    #1: Get data and store `responseSerializer'and `download File URL'.`

    if (error) {
        #2: Call `completionHandler'in case of an error`
    } else {
        #3: Call `completionHandler'`
    }
}

This is the skeleton of the whole proxy method. Let's first look at the simplest part of the code:


__block NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
userInfo[AFNetworkingTaskDidCompleteResponseSerializerKey] = manager.responseSerializer;

//Performance Improvement from #2672
NSData *data = nil;
if (self.mutableData) {
   data = [self.mutableData copy];
   //We no longer need the reference, so nil it out to gain back some memory.
   self.mutableData = nil;
}

if (self.downloadFileURL) {
   userInfo[AFNetworkingTaskDidCompleteAssetPathKey] = self.downloadFileURL;
} else if (data) {
   userInfo[AFNetworkingTaskDidCompleteResponseDataKey] = data;
}

This part of the code takes data from mutableData and sets userInfo.


userInfo[AFNetworkingTaskDidCompleteErrorKey] = error;

dispatch_group_async(manager.completionGroup ?: url_session_manager_completion_group(), manager.completionQueue ?: dispatch_get_main_queue(), ^{
    if (self.completionHandler) {
        self.completionHandler(task.response, responseObject, error);
    }

    dispatch_async(dispatch_get_main_queue(), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingTaskDidCompleteNotification object:task userInfo:userInfo];
    });
});

Use them if the current manager holds completionGroup or completionQueue. Otherwise, a dispatch_group_t will be created. And call completionHandler in the main thread and send notifications (in the main thread).

If there are no errors in executing the current task, serialize the data first, then call the block again and send notifications.


dispatch_async(url_session_manager_processing_queue(), ^{
    NSError *serializationError = nil;
    responseObject = [manager.responseSerializer responseObjectForResponse:task.response data:data error:&serializationError];

    if (self.downloadFileURL) {
        responseObject = self.downloadFileURL;
    }

    if (responseObject) {
        userInfo[AFNetworkingTaskDidCompleteSerializedResponseKey] = responseObject;
    }

    if (serializationError) {
        userInfo[AFNetworkingTaskDidCompleteErrorKey] = serializationError;
    }

    dispatch_group_async(manager.completionGroup ?: url_session_manager_completion_group(), manager.completionQueue ?: dispatch_get_main_queue(), ^{
        if (self.completionHandler) {
            self.completionHandler(task.response, responseObject, serializationError);
        }

        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingTaskDidCompleteNotification object:task userInfo:userInfo];
        });
    });
});


The proxy method URLSession:dataTask:didReceiveData: and - URLSession: downloadTask: didFinish DownloadingToURL:

These two proxy methods are invoked when the data is received or when the corresponding files are downloaded. The functions of these two proxy methods are to add data to mutableData and process the downloaded files respectively:


- (void)URLSession:(__unused NSURLSession *)session
          dataTask:(__unused NSURLSessionDataTask *)dataTask
    didReceiveData:(NSData *)data
{
    [self.mutableData appendData:data];
}

- (void)URLSession:(NSURLSession *)session
      downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
    NSError *fileManagerError = nil;
    self.downloadFileURL = nil;

    if (self.downloadTaskDidFinishDownloading) {
        self.downloadFileURL = self.downloadTaskDidFinishDownloading(session, downloadTask, location);
        if (self.downloadFileURL) {
            [[NSFileManager defaultManager] moveItemAtURL:location toURL:self.downloadFileURL error:&fileManagerError];

            if (fileManagerError) {
                [[NSNotificationCenter defaultCenter] postNotificationName:AFURLSessionDownloadTaskDidFailToMoveFileNotification object:downloadTask userInfo:fileManagerError.userInfo];
            }
        }
    }
}

Using _AFURLSession TaskSwizzling Adjustment Method

_ The only function of AFURLSession TaskSwizzling is modification. NSURLSession Task The resume and suspend methods replace the original implementation with the following methods


- (void)af_resume {
    NSAssert([self respondsToSelector:@selector(state)], @"Does not respond to state");
    NSURLSessionTaskState state = [self state];
    [self af_resume];

    if (state != NSURLSessionTaskStateRunning) {
        [[NSNotificationCenter defaultCenter] postNotificationName:AFNSURLSessionTaskDidResumeNotification object:self];
    }
}

- (void)af_suspend {
    NSAssert([self respondsToSelector:@selector(state)], @"Does not respond to state");
    NSURLSessionTaskState state = [self state];
    [self af_suspend];

    if (state != NSURLSessionTaskStateSuspended) {
        [[NSNotificationCenter defaultCenter] postNotificationName:AFNSURLSessionTaskDidSuspendNotification object:self];
    }
}

The purpose of this is to notify when the method resume or suspend is called.

The specific method of dispensing is carried out in the + load method.


The load method is called only once when the entire file is introduced


+ (void)load {
    if (NSClassFromString(@"NSURLSessionTask")) {
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
        NSURLSession * session = [NSURLSession sessionWithConfiguration:configuration];
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wnonnull"
        NSURLSessionDataTask *localDataTask = [session dataTaskWithURL:nil];
#pragma clang diagnostic pop
        IMP originalAFResumeIMP = method_getImplementation(class_getInstanceMethod([self class], @selector(af_resume)));
        Class currentClass = [localDataTask class];

        while (class_getInstanceMethod(currentClass, @selector(resume))) {
            Class superClass = [currentClass superclass];
            IMP classResumeIMP = method_getImplementation(class_getInstanceMethod(currentClass, @selector(resume)));
            IMP superclassResumeIMP = method_getImplementation(class_getInstanceMethod(superClass, @selector(resume)));
            if (classResumeIMP != superclassResumeIMP &&
                originalAFResumeIMP != classResumeIMP) {
                [self swizzleResumeAndSuspendMethodForClass:currentClass];
            }
            currentClass = [currentClass superclass];
        }

        [localDataTask cancel];
        [session finishTasksAndInvalidate];
    }
}

  1. First, the NSClassFromString (@ "NSURLSession Task") is used to determine whether the currently deployed iOS version contains the NSURLSession Task class.
  2. Because the implementation of NSURLSessionTask on iOS 7 and iOS 8 is different, a NSURLSession dataTask with URL:] method is used to return a NSURLSessionTask. Example
  3. Get the implementation af_resume in the current class _AFURLSession TaskSwizzling
  4. If the current class currentClass has resume methods
    • True: 5
    • False: 7
  5. Use swizzleResume AndSuspendMethodForClass: Adjust the resume and the class suspend method
  6. currentClass = [currentClass superclass]
The complex implementation here is to solve bug s #2702


Introducing AFSecurity Policy to Ensure the Security of Requests

AFSecurity Policy is a class used by AFNetworking to secure HTTP requests. It is called AFSecurity Policy. AFURLSession Manager holds if you are in Search in the Implementation File of AFURLSession Manager Self. security policy, you only get three results:

  1. Initialize self. security policy = [AFSecurity Policy default policy]
  2. When receiving authentication requests from the connection layer
  3. When a task receives a validation request

On API calls, the latter two call the -[AFSecurity Policy evaluateServer Trust: for Domain:] method to determine whether the current server is trusted or not. We will introduce the implementation of this method in the next article.


- (void)URLSession:(NSURLSession *)session
              task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    __block NSURLCredential *credential = nil;

    if (self.taskDidReceiveAuthenticationChallenge) {
        disposition = self.taskDidReceiveAuthenticationChallenge(session, task, challenge, &credential);
    } else {
        if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
            if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
                disposition = NSURLSessionAuthChallengeUseCredential;
                credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            } else {
                disposition = NSURLSessionAuthChallengeRejectProtectionSpace;
            }
        } else {
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        }
    }

    if (completionHandler) {
        completionHandler(disposition, credential);
    }
}

If no taskDidReceive Authentication Challenge block is passed in, authentication credentials will be obtained only when the above method returns YES. credential.

Introducing AFNetwork Reachability Manager to monitor network status

Like AFSecurity Policy, AFURLSession Manager monitors network status by AFNetwork Reachability Manager To be responsible, it simply holds an object of AFNetwork Reachability Manager.


When we really need to judge the network state, we still need the developer to call the corresponding API to get the network state.

Summary

  1. AFURLSession Manager is the encapsulation of NSURLSession
  2. It creates instances of NSURLSession DataTask through interfaces such as -[AFURLSession Manager dataTaskWithRequest: completionHandler:]
  3. Holding a dictionary mutable Task Delegates KeyedByTaskIdentifier manages these data task instances
  4. Introduce AFURLSession Manager Task Delegate to load the incoming uploadProgress Block download Progress Block completion Handler Call at the right time
  5. Implementing all proxy methods to provide block interface
  6. Notification is given when the data task status changes by way of dispensing

Other articles on other AFNetworking source code analysis:



Posted by tarun on Wed, 19 Jun 2019 17:40:54 -0700