探秘AFNetworking

该文章属于刘小壮原创,转载请注明:刘小壮


AFNetworkingiOS最常用的网络框架,虽然系统也有NSURLSession,但是我们一般不会直接用它。AFNetworking经过了三个大版本,现在用的大多数都是3.x的版本。

AFNetworking经历了下面三个阶段的发展:

  • 1.0版本 : 基于NSURLConnection的封装。
  • 2.0版本 : 两套实现,分别基于NSURLConnectionNSURLSession,是转向NSURLSession的过渡版。
  • 3.0版本 : 基于NSURLSession的封装。

文件构成

文件构成

AFNetworking3.X的构成很简单,主要就四部分,除此之外还有一些基于UIKitCategory,但这些并不是标配。

  • Manager : 负责处理网络请求的两个Manager,主要实现都在AFURLSessionManager中。
  • Reachability : 网络状态监控。
  • Security : 处理网络安全和HTTPS相关的。
  • Serialization : 请求和返回数据的格式化器。

AFURLSessionManager


AFN3.0中,网络请求的manager主要有AFHTTPSessionManagerAFURLSessionManager构成,二者为父子关系。这两个类职责划分很清晰,父类负责处理一些基础的网络请求代码,并且接受NSURLRequest对象,而子类则负责处理和http协议有关的逻辑。

AFN的这套设计很便于扩展,如果以后想增加FTP协议的处理,则基于AFURLSessionManager创建子类即可。子类中只需要进行很少的代码处理,创建一个NSURLRequest对象后调用父类代码,由父类去完成具体的请求操作。

创建sessionManager

AFHTTPSessionManager类的初始化方法中并没有太多实现代码,其内部调用的都是父类AFURLSessionManagerinitWithSessionConfiguration方法,下面是此方法内部的一些关键代码。

在初始化方法中包含一个参数sessionConfiguration,如果没有传入的话默认是使用系统的defaultConfiguration,我们创建是一般都不会自定义configuration,所以大多数都是系统的。

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

随后是NSURLSession的初始化代码,关于NSOperationQueue后面详细进行讲解。NSURLSession的初始化方式有两种,一种是使用系统的共享session,另一种是自己创建sessionAFN选择的是创建自己的session,并且每个请求都会创建一个独立的session

可以通过NSURLSession进行连接复用,这样可以避免很多握手和挥手的过程,提高网络请求速度,苹果允许iOS设备上一个域名可以有四个连接同时存在。但是由于AFN的实现是每个请求都创建一个session,所以就不能进行连接复用。

所以可以通过在外面对AFN进行二次封装,将AFHTTPSessionManager复用为单例对象,通过复用sessionManager的方式,来进行连接的复用。但是这种方案对于不同的requestSerializerresponseSerializer等情况,还是要做特殊兼容,所以最好建立一个sessionManager池,对于同类型的sessionManager直接拿出来复用,否则就创建新的。

// 共享session连接池
[NSURLSession sharedSession];
// 创建新session,则不能使用共享session连接池
[NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];

由于当前AFURLSessionManager对象的所有sessionTask请求任务,都是共享同一个回调代理的,所以AFN为了区分每个sessionTask,通过下面的可变字典,将所有taskDelegatetask.taskIdentifier的进行了一一对应,以便于很容易的对每个请求task进行操作。

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

在初始化方法中,可以发现AFN在创建session后,调用了getTasksWithCompletionHandler方法来获取当前所有的task。但是现在刚创建session,理论上来说是不应该有task的。但从AFNissues中找到了答案issues 3499

这是因为,在completionHandler回调中,为了防止进入前台时,通过session id恢复的task导致一些崩溃问题,所以这里将之前的task进行遍历,并将回调都置nil

[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
    for (NSURLSessionDataTask *task in dataTasks) {
        [self addDelegateForDataTask:task uploadProgress:nil downloadProgress:nil completionHandler:nil];
    }

    for (NSURLSessionUploadTask *uploadTask in uploadTasks) {
        [self addDelegateForUploadTask:uploadTask progress:nil completionHandler:nil];
    }

    for (NSURLSessionDownloadTask *downloadTask in downloadTasks) {
        [self addDelegateForDownloadTask:downloadTask progress:nil destination:nil completionHandler:nil];
    }
}];

创建task

AFURLSessionManager中进行task的创建,task的类型总共分为三种,dataTaskuploadTaskdownloadTaskAFN并没有对streamTask进行处理。

AFHTTPSessionManager在创建GETPOST等请求时,本质上都是调用了下面的方法或其类似的方法,方法内部会创建一个task对象,并调用addDelegateForDataTask将后面的处理交给AFURLSessionManagerTaskDelegate来完成。随后会将task返回给调用方,调用方获取到task对象后,也就是子类AFHTTPSessionManager,会调用resume方法开始请求。

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

除了普通请求外,uploaddownload都有类似的处理。

相关API

addDelegateForDataTask方法中,会调用sessionManagersetDelegate:forTask:方法,此方法内部将tasktaskDelegate进行了注册。由于AFN可以通过通知让外界监听请求状态,所以在此方法中还监听了taskresumesuspend事件,并在实现代码中将事件广播出去。

- (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] initWithTask:dataTask];
    delegate.manager = self;
    delegate.completionHandler = completionHandler;

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

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

如果从AFHTTPSessionManager的创建任务开始,按代码逻辑跟到这里,发现其实AFN3.0的请求代码真的很简单,主要都集中在创建NSMutableURLRequest那里,其他都依赖于NSURLSession,因为确实NSURLSessionAPI封装程度比较好,也很好使用。

AFN3.0的作用就是对NSURLSession的封装性比较好,你不用去写太多重复性的代码,并且可以很容易的通过block得到回调结果。

AFURLSessionManagerTaskDelegate

NSURLSession的回调方法比较多,这里只针对一些关键代码进行讲解,以及梳理整体回调逻辑,不一一列举每个回调方法的作用,详细源码各位可以直接下载AFN代码查看。

AFURLSessionManager中,有一个AFURLSessionManagerTaskDelegate类比较重要,这个类和sessionTask是一一对应的,负责处理sessionTask请求的很多逻辑,NSURLSessionDelegate的回调基本都转发给taskDelegate去处理了。在NSURLSession回调中处理了HTTPS证书验证、下载进度之类的,没有太复杂的处理。

taskDelegate的设计很不错,可以将代理回调任务处理对象化,也可以给AFURLSessionManager类瘦身。比较理想的是直接将代理设置为taskDelegate,但是由于会涉及一些AFURLSessionManager自身的处理逻辑,所以才设计为消息传递的方式。

taskDelegate的功能很简单,主要是NSData数据的处理,NSProgress上传下载进度的处理,以及通知参数的处理。在进行AFN的下载处理时,NSData的数据拼接、事件回调,及文件处理,都是由taskDelegate来完成的。

下面是downloadTask任务完成时的处理代码,其他回调代码就不一一列举了。

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

    if (self.downloadTaskDidFinishDownloading) {
        self.downloadFileURL = self.downloadTaskDidFinishDownloading(session, downloadTask, location);
        if (self.downloadFileURL) {
            NSError *fileManagerError = nil;

            if (![[NSFileManager defaultManager] moveItemAtURL:location toURL:self.downloadFileURL error:&fileManagerError]) {
                [[NSNotificationCenter defaultCenter] postNotificationName:AFURLSessionDownloadTaskDidFailToMoveFileNotification object:downloadTask userInfo:fileManagerError.userInfo];
            }
        }
    }
}

taskDelegate中有一个很好的设计,taskDelegate并不直接在NSURLSession的代理方法中做进度拼接和回调。而是对于上传和下载任务分别对应不同的NSProgress,并通过KVO来监听fractionCompleted属性,并且实现cancelsuspend等状态回调。任务的状态和进度处理交给NSProgress,在回调方法中直接拼接NSProgress的进度,从而回调KVO方法。

NSProgress内部的cancelpauseresume方法,正好可以对应到sessionTask的方法调用。但是从代码角度来看,AFN好像并没有进行相关的调用,但这个设计思路很好。

_uploadProgress = [[NSProgress alloc] initWithParent:nil userInfo:nil];
_downloadProgress = [[NSProgress alloc] initWithParent:nil userInfo:nil];
    
__weak __typeof__(task) weakTask = task;
for (NSProgress *progress in @[ _uploadProgress, _downloadProgress ])
{
    progress.totalUnitCount = NSURLSessionTransferSizeUnknown;
    progress.cancellable = YES;
    progress.cancellationHandler = ^{
        [weakTask cancel];
    };
    progress.pausable = YES;
    progress.pausingHandler = ^{
        [weakTask suspend];
    };
#if AF_CAN_USE_AT_AVAILABLE
    if (@available(iOS 9, macOS 10.11, *))
#else
    if ([progress respondsToSelector:@selector(setResumingHandler:)])
#endif
    {
        progress.resumingHandler = ^{
            [weakTask resume];
        };
    }
    
    [progress addObserver:self
               forKeyPath:NSStringFromSelector(@selector(fractionCompleted))
                  options:NSKeyValueObservingOptionNew
                  context:NULL];
}

_AFURLSessionTaskSwizzling

看过源码的话,可以发现AFURLSessionManager中还有一个_AFURLSessionTaskSwizzling类,这里我们简称taskSwizzling类。我认为此类的设计实在是冗余,此类的主要功能就是在+load方法中进行一个swizzling,将dataTaskresumesuspend方法进行替换,并且在替换后的方法中发出对应的通知,并没有太多实际的功能。

只不过taskSwizzling类中还是有一些不错的代码设计值得借鉴的,由于sessionTask存在一系列继承链,所以直接对其进行swizzling对其他子类并不生效,因为每个子类都有自己的实现,而写一大堆swizzling又没有什么技术含量。

iOS7iOS8上,sessionTask的继承关系并不一样,最好进行一个统一的处理。AFN采取的方式是创建一个dataTask对象,并对这个对象进行swizzling,并且遍历其继承链一直进行swizzling,这样保证集成继承链的正确性。

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

+ (void)swizzleResumeAndSuspendMethodForClass:(Class)theClass {
    Method afResumeMethod = class_getInstanceMethod(self, @selector(af_resume));
    Method afSuspendMethod = class_getInstanceMethod(self, @selector(af_suspend));

    if (af_addMethod(theClass, @selector(af_resume), afResumeMethod)) {
        af_swizzleSelector(theClass, @selector(resume), @selector(af_resume));
    }

    if (af_addMethod(theClass, @selector(af_suspend), afSuspendMethod)) {
        af_swizzleSelector(theClass, @selector(suspend), @selector(af_suspend));
    }
}

clang预编译指令

AFN为了避免发生编译器警告,采取了预编译指令对代码进行修饰,预编译指令基本由三部分组成,pushpopignored类型。Github上有人维护了一份clang warning清单,如果想进行对应的预编译处理可以上去找找有没有合适的。

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
#pragma clang diagnostic pop

线程问题

NSURLSessioniOS8以下会并发创建多个task,但并发设置task identifier的时候会存在identifier重复的问题。为了解决这个问题,在iOS8以下,系统将所有sessionTask的创建都放在一个同步的串行队列中进行,保证创建及赋值操作是串行进行的。

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

url_session_manager_create_task_safely(^{
    uploadTask = [self.session uploadTaskWithRequest:request fromData:bodyData];
});

// 如果Foundation版本小于iOS8,则把block任务放在一个同步队列中执行。这个问题是由于在iOS8以下并发创建任务,可能会有多个相同的identifier
static void url_session_manager_create_task_safely(dispatch_block_t block) {
    if (NSFoundationVersionNumber < NSFoundationVersionNumber_With_Fixed_5871104061079552_bug) {
        dispatch_sync(url_session_manager_creation_queue(), block);
    } else {
        block();
    }
}

一个比较有意思的是,AFN为了让开发者明白为什么要加这个判断,对iOS8系统的判断定义成了一个宏,并且用Apple Supportid作为宏定义命名,很见名知意。

#define NSFoundationVersionNumber_With_Fixed_5871104061079552_bug NSFoundationVersionNumber_iOS_8_0

AFN在回调didCompleteWithError方法,并处理返回数据时,会切换到其他线程和group去处理,处理完成后再切换到主线程并通知调用方。

AFN提供了两个属性,用来设置请求结束后进行回调的dispatch queuedispatch group,如果不设置的话,AFN会有默认的实现来处理请求结束的操作。下面是groupqueue的实现,AFN对于返回数据的处理,采用的是并发处理。

static dispatch_queue_t url_session_manager_processing_queue() {
    static dispatch_queue_t af_url_session_manager_processing_queue;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        af_url_session_manager_processing_queue = dispatch_queue_create("com.alamofire.networking.session.manager.processing", DISPATCH_QUEUE_CONCURRENT);
    });

    return af_url_session_manager_processing_queue;
}

static dispatch_group_t url_session_manager_completion_group() {
    static dispatch_group_t af_url_session_manager_completion_group;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        af_url_session_manager_completion_group = dispatch_group_create();
    });

    return af_url_session_manager_completion_group;
}

NSOperationQueue

AFN在创建AFURLSessionManageroperationQueue时,将其最大并发数设置为1。这是因为在创建NSURLSSession时,苹果要求网络请求回来的数据顺序执行,为了保证代理方法的执行顺序,所以需要串行的调用NSURLSSession的代理方法。

AFHTTPSessionManager


AFHTTPSessionManager本质上是对父类AFURLSessionManager的封装,主要实现都在父类中,自己内部代码实现很简单。在创建AFHTTPSessionManager时会传入一个baseURL,以及指定requestSerializerresponseSerializer对象。

从代码实现来看,AFN的请求并不是单例形式的,每个请求都会创建一个新的请求对象。平时调用的GETPOST等网络请求方法,都定义在AFHTTPSessionManager中。AFHTTPSessionManager内部则调用父类方法,发起响应的请求并获取到task对象,调用taskresume后返回给调用方。

- (NSURLSessionDataTask *)GET:(NSString *)URLString
                   parameters:(id)parameters
                     progress:(void (^)(NSProgress * _Nonnull))downloadProgress
                      success:(void (^)(NSURLSessionDataTask * _Nonnull, id _Nullable))success
                      failure:(void (^)(NSURLSessionDataTask * _Nullable, NSError * _Nonnull))failure
{

    NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"GET"
                                                        URLString:URLString
                                                       parameters:parameters
                                                   uploadProgress:nil
                                                 downloadProgress:downloadProgress
                                                          success:success
                                                          failure:failure];

    [dataTask resume];

    return dataTask;
}

AFURLRequestSerialization


AFURLRequestSerialization负责创建NSMutableURLRequest请求对象,并对request进行请求的参数拼接、设置缓存策略、设置请求头等关于请求相关的配置。

AFURLRequestSerialization并不是一个类,而是一个文件,其中包含三个requestSerializer请求对象,分别对应着不同的请求序列化器。

  • AFHTTPRequestSerializer:普通请求。
  • AFJSONRequestSerializer:JSON请求。
  • AFPropertyListRequestSerializer:一种特殊的xml格式请求。

这三个类区别就在于Content-Type不同,其他基本都是一样的。AFN默认是HTTP的。

AFURLRequestSerialization协议

在文件中定义了同名的AFURLRequestSerialization协议,不同的requestSerializer会对协议方法有不同的实现,下面是AFHTTPRequestSerializer的实现代码。其核心代码实现也比较直观,就是在创建requestSerializer的时候,设置请求头的公共参数,以及将请求参数通过NSJSONSerialization转换为NSData,并将其赋值给request对象的httpBody,下面是精简后的核心代码。

- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
                               withParameters:(id)parameters
                                        error:(NSError *__autoreleasing *)error
{
    NSMutableURLRequest *mutableRequest = [request mutableCopy];
    [self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) {
        if (![request valueForHTTPHeaderField:field]) {
            [mutableRequest setValue:value forHTTPHeaderField:field];
        }
    }];

    if (parameters) {
        if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) {
            [mutableRequest setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
        }

        NSData *jsonData = [NSJSONSerialization dataWithJSONObject:parameters options:self.writingOptions error:error];
        [mutableRequest setHTTPBody:jsonData];
    }

    return mutableRequest;
}

如果想给网络请求设置请求参数的话,需要通过requestSerializer对外暴露的API添加参数,AFNrequestManager并不直接对外提供设置请求头的代码。通过requestSerializer可以对请求头进行添加和删除、以及清空的操作。

从创建AFURLRequestSerialization对象到最后返回NSURLRequest对象,中间的过程并不复杂,主要是设置请求头和拼接参数,逻辑很清晰。

AFQueryStringPair

AFURLRequestSerialization有一个很重要的功能就是参数处理,AFQueryStringPair就是负责处理这些参数的。pair类中定义了两个属性,分别对应请求参数的keyvalue。除此之外,还定义了一些非常实用的C语言函数。

@interface AFQueryStringPair : NSObject
@property (readwrite, nonatomic, strong) id field;
@property (readwrite, nonatomic, strong) id value;

- (id)initWithField:(id)field value:(id)value;

- (NSString *)URLEncodedStringValue;
@end

AFQueryStringFromParameters函数负责将请求参数字典,转成拼接在URL后面的参数字符串,这个函数是AFQueryStringPair类中定义的一个关键函数。函数内部通过AFQueryStringPairsFromDictionary函数将参数字典,转为存储pair对象的数组并进行遍历,遍历后调用URLEncodedStringValue方法对参数进行拼接,最后成为字符串参数。

URLEncodedStringValue方法实现很简单,就是进行一个keyvalue的拼接,并且在中间加上“=”。

static NSString * AFQueryStringFromParameters(NSDictionary *parameters) {
    NSMutableArray *mutablePairs = [NSMutableArray array];
    for (AFQueryStringPair *pair in AFQueryStringPairsFromDictionary(parameters)) {
        [mutablePairs addObject:[pair URLEncodedStringValue]];
    }

    return [mutablePairs componentsJoinedByString:@"&"];
}

- (NSString *)URLEncodedStringValue {
    if (!self.value || [self.value isEqual:[NSNull null]]) {
        return AFPercentEscapedStringFromString([self.field description]);
    } else {
        return [NSString stringWithFormat:@"%@=%@", AFPercentEscapedStringFromString([self.field description]), AFPercentEscapedStringFromString([self.value description])];
    }
}

下面是参数拼接的代码,函数内部会将原有的参数,转换为AFQueryStringPair对象的类型,但之前的层级结构不变。这句话是什么意思呢,就是说对原有传入的对象进行逐层递归调用,并且将最后一层字典的keyvalue参数,转成pair类型的对象,并且将嵌套有pair对象的数组返回给调用方。

对象层级不变,但字典、集合都会被转换为数组结构,也就是之前传入字典、数组、字典的嵌套结构,返回的时候就是数组、数组、pair的结构返回。

NSArray * AFQueryStringPairsFromDictionary(NSDictionary *dictionary) {
    return AFQueryStringPairsFromKeyAndValue(nil, dictionary);
}

NSArray * AFQueryStringPairsFromKeyAndValue(NSString *key, id value) {
    NSMutableArray *mutableQueryStringComponents = [NSMutableArray array];

    NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"description" ascending:YES selector:@selector(compare:)];

    if ([value isKindOfClass:[NSDictionary class]]) {
        NSDictionary *dictionary = value;
        for (id nestedKey in [dictionary.allKeys sortedArrayUsingDescriptors:@[ sortDescriptor ]]) {
            id nestedValue = dictionary[nestedKey];
            if (nestedValue) {
                [mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue((key ? [NSString stringWithFormat:@"%@[%@]", key, nestedKey] : nestedKey), nestedValue)];
            }
        }
    } else if ([value isKindOfClass:[NSArray class]]) {
        NSArray *array = value;
        for (id nestedValue in array) {
            [mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue([NSString stringWithFormat:@"%@[]", key], nestedValue)];
        }
    } else if ([value isKindOfClass:[NSSet class]]) {
        NSSet *set = value;
        for (id obj in [set sortedArrayUsingDescriptors:@[ sortDescriptor ]]) {
            [mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue(key, obj)];
        }
    } else {
        [mutableQueryStringComponents addObject:[[AFQueryStringPair alloc] initWithField:key value:value]];
    }

    return mutableQueryStringComponents;
}

设置NSMutableURLRequest

AFHTTPRequestSerializer在创建NSMutableURLRequest时,需要为request设置属性。serializer对外提供了和request同名的一些属性,外界直接调用serializer即可设置request的属性。

AFHTTPRequestSerializer内部创建request时,并不是根据设置request的属性按个赋值,而是通过一个属性数组AFHTTPRequestSerializerObservedKeyPaths,将serializer需要赋值给request的属性,都放在数组中。

static NSArray * AFHTTPRequestSerializerObservedKeyPaths() {
    static NSArray *_AFHTTPRequestSerializerObservedKeyPaths = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _AFHTTPRequestSerializerObservedKeyPaths = @[NSStringFromSelector(@selector(allowsCellularAccess)), NSStringFromSelector(@selector(cachePolicy)), NSStringFromSelector(@selector(HTTPShouldHandleCookies)), NSStringFromSelector(@selector(HTTPShouldUsePipelining)), NSStringFromSelector(@selector(networkServiceType)), NSStringFromSelector(@selector(timeoutInterval))];
    });

    return _AFHTTPRequestSerializerObservedKeyPaths;
}

在初始化AFHTTPRequestSerializer时,遍历keyPath数组并通过KVO的方式,监听serializer的赋值。如果外界对serializer对应的属性进行赋值,则将其添加到mutableObservedChangedKeyPaths数组中。在创建request对象是,遍历mutableObservedChangedKeyPaths数组并将值赋值给request对象。

for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
    if ([self respondsToSelector:NSSelectorFromString(keyPath)]) {
        [self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:AFHTTPRequestSerializerObserverContext];
    }
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(__unused id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if (context == AFHTTPRequestSerializerObserverContext) {
        if ([change[NSKeyValueChangeNewKey] isEqual:[NSNull null]]) {
            [self.mutableObservedChangedKeyPaths removeObject:keyPath];
        } else {
            [self.mutableObservedChangedKeyPaths addObject:keyPath];
        }
    }
}
    
for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
    if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
        [mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
    }
}

表单提交

当进行POST表单提交时,需要用到AFMultipartFormData协议。调用POST方法后,会回调一个遵守此协议的对象,可以通过此对象进行表单提交操作。

[manager POST:requestURL parameters:params constructingBodyWithBlock:^(id<AFMultipartFormData>  _Nonnull formData) {
    [formData appendPartWithFileData:params[@"front_img"]
                                name:@"front_img"
                            fileName:frontImgfileName
                            mimeType:@"multipart/form-data"];
    [formData appendPartWithFileData:params[@"reverse_img"]
                                name:@"reverse_img"
                            fileName:reverseImgfileName
                            mimeType:@"multipart/form-data"];
    [formData appendPartWithFileData:params[@"face_img"]
                                name:@"face_img"
                            fileName:faceImgfileName
                            mimeType:@"multipart/form-data"];

} progress:^(NSProgress * _Nonnull uploadProgress) {
    // nothing
} success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
    // nothing
} failure:nil];

进行表单提交时,可以直接传入文件,也可以传入路径。表单提交可以同时提交多个文件,理论上数量不受限制。

缓存策略

AFN的缓存策略和NSURLCache的缓存策略一致,并且直接使用系统的枚举,这对iOS开发者是非常友好的。下面是枚举定义,忽略掉一些unimplemented的,和一些重定向到已有枚举的,可用的都在这。

typedef NS_ENUM(NSUInteger, NSURLRequestCachePolicy)
{
    NSURLRequestUseProtocolCachePolicy = 0,
    NSURLRequestReloadIgnoringLocalCacheData = 1,
    NSURLRequestReturnCacheDataElseLoad = 2,
    NSURLRequestReturnCacheDataDontLoad = 3,
    NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4,
    NSURLRequestReloadRevalidatingCacheData = 5,
};
  • NSURLRequestUseProtocolCachePolicy,使用协议指定的缓存策略。
  • NSURLRequestReloadIgnoringLocalCacheData,忽略缓存,直接发起请求。
  • NSURLRequestReturnCacheDataElseLoad,不验证缓存过期时间,如果有则使用缓存数据,如果不存在则请求服务器。
  • NSURLRequestReturnCacheDataDontLoad,不验证缓存过期时间,如果有则使用缓存数据,如果不存在则请求失败。
  • NSURLRequestReloadIgnoringLocalAndRemoteCacheData,忽略本地缓存,以及代理等中间介质的缓存。
  • NSURLRequestReloadRevalidatingCacheData,和数据的源服务器验证数据合法性,如果可以用就直接使用缓存数据,否则从服务器请求数据。

AFURLResponseSerialization


AFURLResponseSerialization负责处理response相关的逻辑,其功能主要是设置acceptType、编码格式和处理服务器返回数据。同样的,AFURLResponseSerialization也有同名的协议,每个子类都遵循代理方法并实现不同的返回值处理代码。

- (nullable id)responseObjectForResponse:(nullable NSURLResponse *)response
                           data:(nullable NSData *)data
                          error:(NSError * _Nullable __autoreleasing *)error;

AFURLRequestSerialization一样,AFURLResponseSerialization由一个父类和六个子类构成,子类中有一个是Mac的,所以这里不做分析,子类的职责只是对acceptType做修改以及处理具体的返回数据。

  • AFHTTPResponseSerializer:公共父类,处理返回值类型为NSData二进制。
  • AFJSONResponseSerializer:JSON返回数据,也是默认类型。
  • AFXMLParserResponseSerializer,处理XML返回数据,由系统NSXMLParser负责处理。
  • AFPropertyListResponseSerializer:处理特殊XML返回数据,也就是plist数据。
  • AFImageResponseSerializer:处理图片返回数据,这个类型用的也比较多。
  • AFCompoundResponseSerializer:处理复杂数据,返回结果类型有多种。

容错处理

由于服务器有时候会返回null的情况,系统会将其转换为NSNull对象,而对NSNull对象发送不正确的消息,就会导致崩溃。从服务器接收到返回值后,AFN会对返回值进行一个递归查找,找到所有NSNull对象并将其移除,防止出现向NSNull对象发送消息导致的崩溃。

static id AFJSONObjectByRemovingKeysWithNullValues(id JSONObject, NSJSONReadingOptions readingOptions) {
    if ([JSONObject isKindOfClass:[NSArray class]]) {
        NSMutableArray *mutableArray = [NSMutableArray arrayWithCapacity:[(NSArray *)JSONObject count]];
        for (id value in (NSArray *)JSONObject) {
            [mutableArray addObject:AFJSONObjectByRemovingKeysWithNullValues(value, readingOptions)];
        }

        return (readingOptions & NSJSONReadingMutableContainers) ? mutableArray : [NSArray arrayWithArray:mutableArray];
    } else if ([JSONObject isKindOfClass:[NSDictionary class]]) {
        NSMutableDictionary *mutableDictionary = [NSMutableDictionary dictionaryWithDictionary:JSONObject];
        for (id <NSCopying> key in [(NSDictionary *)JSONObject allKeys]) {
            id value = (NSDictionary *)JSONObject[key];
            if (!value || [value isEqual:[NSNull null]]) {
                [mutableDictionary removeObjectForKey:key];
            } else if ([value isKindOfClass:[NSArray class]] || [value isKindOfClass:[NSDictionary class]]) {
                mutableDictionary[key] = AFJSONObjectByRemovingKeysWithNullValues(value, readingOptions);
            }
        }

        return (readingOptions & NSJSONReadingMutableContainers) ? mutableDictionary : [NSDictionary dictionaryWithDictionary:mutableDictionary];
    }

    return JSONObject;
}

AFNetworking的设计技巧


bundleForClass

在使用NSBundle对象时,我们最常用的就是mainBundle或者bundleWithPath这种方式获取bundle,这种对于都是从app二进制读取的时候是没有问题的。但是如果涉及到framework动态库,就不是那么易于使用。

framework中可以包含资源文件,例如.bundle文件。如果是动态库形式的framework(framework也有静态形式),其会以一个独立二进制的形式表现,并且会分配独立的二进制空间。在读取bundle的时候,就可以考虑使用bundleForClass的方式读取。

bundleForClass表示从当前类定义的二进制,所在的程序包中读取NSBundle文件。例如.app就是从main bundle中读取,如果是framework就从其所在的二进制中读取。

网络指示器

AFN提供了一些UIKitCategory,例如网络请求发起时,网络指示器转菊花,则由AFNetworkActivityIndicatorManager类负责。开启网络指示器很简单,添加下面代码即可,网络指示器默认是关闭的。

[[AFNetworkActivityIndicatorManager sharedManager] setEnabled:YES];

这里不对AFNetworkActivityIndicatorManager的代码进行过多的分析,只是调其中比较重要的点来分析,下面统称为indicatorManager

之前在_AFURLSessionTaskSwizzling类中写了很多代码,就是为了发出resumesuspend两个通知,这两个通知在indicatorManager中就用到了。网络指示器监听了下面的三个通知,并且完全由通知来驱动。

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkRequestDidStart:) name:AFNetworkingTaskDidResumeNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkRequestDidFinish:) name:AFNetworkingTaskDidSuspendNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkRequestDidFinish:) name:AFNetworkingTaskDidCompleteNotification object:nil];

如果看indicatorManager中的源码,你会发现为什么里面还有timer,完全不需要啊,有网络请求就转菊花,没网络请求就停止不就行了吗?

这是因为AFN考虑,如果一个网络请求很快的话,会导致菊花出现转一下很快就消失的情况,如果网络请求比较多会多次闪现。所以对于这个问题,indicatorManager通过Timer的方式实现,如果在指定的区间内网络请求已经结束,则不在显示菊花,如果有多次请求则在请求之间也不进行中断。

对于开始转圈设置的是1.0秒,结束转圈设置的是0.17秒。也就是当菊花开始旋转时,需要有1.0秒的延时,这个时间足以保证之前的菊花停止转动。结束转圈则会在0.17秒之后进行,可以保证菊花的旋转至少会有0.17秒。

static NSTimeInterval const kDefaultAFNetworkActivityManagerActivationDelay = 1.0;
static NSTimeInterval const kDefaultAFNetworkActivityManagerCompletionDelay = 0.17;

- (void)startActivationDelayTimer {
    self.activationDelayTimer = [NSTimer
                                 timerWithTimeInterval:self.activationDelay target:self selector:@selector(activationDelayTimerFired) userInfo:nil repeats:NO];
    [[NSRunLoop mainRunLoop] addTimer:self.activationDelayTimer forMode:NSRunLoopCommonModes];
}

- (void)startCompletionDelayTimer {
    [self.completionDelayTimer invalidate];
    self.completionDelayTimer = [NSTimer timerWithTimeInterval:self.completionDelay target:self selector:@selector(completionDelayTimerFired) userInfo:nil repeats:NO];
    [[NSRunLoop mainRunLoop] addTimer:self.completionDelayTimer forMode:NSRunLoopCommonModes];
}

由于indicatorManager是采用通知的方式进行回调,所有的网络请求通知都会调到这。所以当多个网络请求到来时,会通过一个_activityCount来进行计数,可以将其理解为一个队列,这样更容易理解。在显示网络指示器的时候,就是基于_activityCount来进行判断的,如果队列中有请求则显示网络指示器,无论有多少请求。

这种设计思路比较好,在项目中很多地方都可以用到。例如有些方法需要成对进行调用,例如播放开始和暂停,如果某一个方法调用多次就会造成bug。这种方式就比较适合用count的方式进行容错,内部针对count做一些判断操作。

- (void)incrementActivityCount {
    [self willChangeValueForKey:@"activityCount"];
    @synchronized(self) {
        _activityCount++;
    }
    [self didChangeValueForKey:@"activityCount"];

    dispatch_async(dispatch_get_main_queue(), ^{
        [self updateCurrentStateForNetworkActivityChange];
    });
}

- (void)decrementActivityCount {
    [self willChangeValueForKey:@"activityCount"];
    @synchronized(self) {
        _activityCount = MAX(_activityCount - 1, 0);
    }
    [self didChangeValueForKey:@"activityCount"];

    dispatch_async(dispatch_get_main_queue(), ^{
        [self updateCurrentStateForNetworkActivityChange];
    });
}

indicatorManager是多线程安全的,在一些关键地方都通过synchronized的方式加锁,防止从各个线程调用过来的通知造成资源抢夺的问题。

AFSecurityPolicy


验证处理

AFN支持https请求,并通过AFSecurityPolicy类来处理https证书及验证,但其https请求的执行还是交给NSURLSession去完成的。

下面是NSURLSession的一个代理方法,当需要进行证书验证时,可以重写此方法并进行自定义的验证处理。验证完成后通过completionHandlerblock来告知处理结果,并且将验证结果disposition和公钥credential传入。

AFN通过AFSecurityPolicy类提供了验证逻辑,并且在内部可以进行证书的管理。也可以不使用AFN提供的验证逻辑,重写sessionDidReceiveAuthenticationChallengeblock即可自定义验证逻辑,不走AFN的逻辑。

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

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

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

除了进行NSURLSession请求验证的回调,对于每个task也有对应的代理方法。两个代理方法内部实现基本一样,区别在于对于每个taskAFN提供了taskDidReceiveAuthenticationChallenge回调block,可以由外界自定义证书验证过程。

验证结果是通过一个枚举回调给NSURLSession的,参数是一个NSURLSessionAuthChallengeDisposition类型的枚举,表示证书验证的情况,此枚举包含下面几个具体值。

  • NSURLSessionAuthChallengeUseCredential
    使用当前证书建立SSL连接,并处理后续请求
  • NSURLSessionAuthChallengePerformDefaultHandling
    使用默认的处理方式,当前证书被忽略
  • NSURLSessionAuthChallengeCancelAuthenticationChallenge
    验证不通过,取消整个网络请求
  • NSURLSessionAuthChallengeRejectProtectionSpace
    这次验证被忽略,但不取消网络请求

Security


HTTPS请求的密钥管理等安全相关的处理,都放在Security.framework框架中。在AFSecurityPolicy中经常可以看到SecTrustRef类型的变量,其表示的就是密钥对象,其中包含了公钥等信息。

我们可以通过下面的命令获取到公钥,具体格式这里不做过多介绍,详细的可以Google一下公钥格式。

// 获取公钥命令
SecTrustCopyPublicKey(serverTrust)

// 打印的公钥(公钥已做脱敏)
<SecKeyRef algorithm id: 1, key type: RSAPublicKey, version: 4, block size: 2048 bits, exponent: {hex: 10001, decimal: 65537}, modulus: A51E89C5FFF2748A6F70C4D701D29A39723C3BE495CABC5487B05023D957CD287839F6B5E53F90B963438417547A369BBA5818D018B0E98F2449442DFBD7F405E18D5A827B6F6F0A5A5E5585C6C0F342DDE727681902021B7A7CE0947EFCFDDC4CCF8E100D94A454156FD3E457F4719E3C6B9E408CD4316B976A6C44BD91A057FEA4A115BEB1FE28E71005D2198E3B79B8942779B434A0B08F82B3D390A7B94B958BB71D9B69564C84A1B03FE22D4C4E24BBA2D5ED3F660F1EBC757EF0E52A7CF13A8C167B0E6171B2CD6678CC02EAF1E59147F53B3671C33107D9ED5238CBE33DB617931FFB44DE70043B2A618D8F43608D6F494360FFDEE83AD1DCE120D6F1, addr: 0x280396dc0>

AFSecurityPolicy概述

AFSecurityPolicy的职责比较单一,只处理公钥和验证的逻辑,其定义是一个单例对象。此类主要由四个属性和一个方法构成。

// 证书验证方式
@property (readonly, nonatomic, assign) AFSSLPinningMode SSLPinningMode;
// 本地自签名证书集合
@property (nonatomic, strong, nullable) NSSet <NSData *> *pinnedCertificates;
// 是否验证证书的合法性(是否允许自签名证书)
@property (nonatomic, assign) BOOL allowInvalidCertificates;
// 是否验证域名是否有效
@property (nonatomic, assign) BOOL validatesDomainName;

如果进行细分的话,AFSecurityPolicy的功能基本就两个。一个是通过CA的方式进行验证,另一个就是进行SSL Pinning自签名验证。evaluateServerTrust:forDomain:AFSecurityPolicy最主要的方法,用来进行证书的合法性验证。

SSL Pinning

AFSecurityPolicy进行SSL Pinning验证的方式分为以下三种,如果是None则会执行正常CA验证的流程,其他两种都是自签名的流程。AFN中默认的调用是defaultPolicy方法,其内部设置的是AFSSLPinningModeNone模式。

  • AFSSLPinningModeNone
    正常流程,通过CA机构颁发的公钥,对服务器下发的证书验证数字签名,并且获得公钥。
  • AFSSLPinningModeCertificate
    不通过CA的流程进行验证,而是通过本地内置的服务端证书进行验证,验证过程分为两步。首先验证证书是否过期或失效,其次验证本地是否包含此证书。
  • AFSSLPinningModePublicKey
    不进行CA的验证,也不验证证书,只验证公钥是否有效。

对于本地自签名证书的管理有两种方式,一种是默认会在本地查找遍历所有.cer的文件,并存在一个自签名证书的集合中。也可以在创建AFSecurityPolicy对象时传入SSLPinningMode,下面是查找本地.cer文件的逻辑。

+ (NSSet *)certificatesInBundle:(NSBundle *)bundle {
    NSArray *paths = [bundle pathsForResourcesOfType:@"cer" inDirectory:@"."];

    NSMutableSet *certificates = [NSMutableSet setWithCapacity:[paths count]];
    for (NSString *path in paths) {
        NSData *certificateData = [NSData dataWithContentsOfFile:path];
        [certificates addObject:certificateData];
    }

    return [NSSet setWithSet:certificates];
}

自签名证书

HTTPS在进行握手时,需要通过CA的公钥进行验证,保证服务器公钥的合法性,没有被篡改为有问题的公钥。如果使用CA机构颁发的证书,无论使用NSURLSession还是AFNetworking都不需要修改代码,这些都会自动完成。如果不想使用CA的证书验证,例如自签名证书在CA证书验证时就会失败。

这种情况可以使用自签名的方式进行验证,也就是在客户端本地内置一份证书,服务器进行四次握手时,通过保存在本地的证书和服务器进行对比,证书相同则表示验证成功,不走CA的验证方式。

AFN为我们提供了自签名证书的验证方法,通过SSLPinningMode设置验证方式为自签名,并且传入证书集合。如果没有传入证书集合,则AFN默认会遍历整个沙盒,查找所有.cer的证书。

进行沙盒验证时,需要将AFSecurityPolicyallowInvalidCertificates设置为YES,默认是NO,表示允许无效的证书,也就是自签名的证书。

NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"12306" ofType:@"cer"];
NSData *certData = [NSData dataWithContentsOfFile:cerPath];
NSSet *set = [NSSet setWithObject:certData];

AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate withPinnedCertificates:set];
securityPolicy.allowInvalidCertificates = YES;

AFNetworking 2.x


AFNetworking2.xNSURLSessionNSURLConnection两部分组成,并且分别对应不同的类,这里主要介绍NSURLConnection部分的源码实现。

相关类

总体概览

NSURLConnection实现由下面三个类构成,从源码构成可以看出,无论是session还是connection方案,都具有很好的扩展性。例如这里AFHTTPRequestOperation是基于AFURLConnectionOperation实现的,如果需要实现FTP协议,则可以创建一个继承自AFURLConnectionOperationAFFPTConnectionOperation类并重写对应方法即可。

  • AFURLConnectionOperation
    继承自NSOperation,负责网络请求的逻辑实现,每个网络请求就是一个Operation对象。
  • AFHTTPRequestOperation
    继承自AFURLConnectionOperation,处理HTTP相关网络请求。
  • AFHTTPRequestOperationManager
    内部持有一个NSOperationQueue,负责管理所有Operation网络请求。

AFURLConnectionOperation

下面是AFURLConnectionOperation的初始化方法,和AFURLSessionManager有些不一样。其内部增加了状态的概念,以及RunloopMode的概念,这两个我们后面会详细讲解。shouldUseCredentialStorage表示是否由系统做证书验证,后面设置了securityPolicy,和sessionManager一样也是使用默认方案。

- (instancetype)initWithRequest:(NSURLRequest *)urlRequest {
    _state = AFOperationReadyState;

    self.lock = [[NSRecursiveLock alloc] init];
    self.lock.name = kAFNetworkingLockName;
    self.runLoopModes = [NSSet setWithObject:NSRunLoopCommonModes];
    self.request = urlRequest;
    self.shouldUseCredentialStorage = YES;
    self.securityPolicy = [AFSecurityPolicy defaultPolicy];
}

AFURLConnectionOperation继承自NSOperation,由于NSOperation和网络请求的过程很像,有开始、暂停、完成等,并且很好的支持KVO监听,所以AFN将每个网络请求都当做一个Operation任务。AFURLConnectionOperation可以设置任务优先级,也可以通过AFHTTPRequestOperationManager设置最大并发数,基本上NSOperationQueue提供的功能都能用。

AFHTTPRequestOperationManager中定义了NSOperationQueue,创建网络请求任务后,都会被加入到queue中,随后由系统调用queue中的operation任务,执行operationstart方法发起请求。AFURLConnectionOperation只需要在内部实现startpauseresume等父类方法即可,其他都由系统去进行调用。这种设计可以很好的将manageroperation进行解耦,二者不用直接发生调用关系。

NSURLConnection中,每个网络请求对应一个AFHTTPRequestOperation,所有网络请求都共用一个manager来管理operation。而AFHTTPSessionManager则不同,每个网络请求对应一个manager以及一个task

state设计

AFURLConnectionOperation支持KVO的方式,让外界监听网络请求的变化,并通过重写setState方法,在内部加入willChangeValueForKey触发KVO回调。AFN通过AFOperationState来管理网络请求状态,下面是AFN对其的状态定义。

  • AFOperationPausedState
    请求暂停
  • AFOperationReadyState
    请求已准备好
  • AFOperationExecutingState
    请求正在执行中
  • AFOperationFinishedState
    请求完成

当网络请求状态发生改变时,都会调用setState方法进行赋值,例如下面是请求完成时的处理代码。除此之外,当判断AFN请求状态时,也是通过这个属性作为判断依据的。

- (void)finish {
    [self.lock lock];
    self.state = AFOperationFinishedState;
    [self.lock unlock];
}

常驻线程

AFURLConnectionOperation中设计了常驻线程,并且重写了operationstart等方法,网络请求的startcancelpause等操作,都是在常驻线程中完成的。网络请求结束后,数据回调的处理也是在这个线程中完成的。

这是因为在哪个线程创建NSURLConnection对象并发出请求,则数据返回时也默认从那个线程接受数据。如果请求都是从主线程发出的,请求返回时如果屏幕正在滑动,runloopModeUITrackingRunLoopMode则不能处理返回数据。而如果把网络请求都加到主线程的NSRunLoopCommonModes中,在大量网络请求返回时,处理返回数据会影响屏幕滑动FPS

所以为了保证网络请求数据可以正常返回并被处理,而又不影响屏幕FPS,则用一个单独的线程来处理。如果每个请求都对应一个线程来处理返回任务,会造成大量线程的占用,所以用一个常驻线程来处理所有网络请求,来保证线程资源的最小占用。常驻线程实际上是一个单例线程,并且这个单例线程被加入了一个Port进行保活,保证线程可以不被退出。

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });

    return _networkRequestThread;
}

通过AFURLConnectionOperation发起网络请求时,实际创建connection对象的代码在下面的方法中。在创建connection对象后,并没有立即发出网络请求,而是将startImmediately设置为NO。随后会设置NSURLConnectionNSOutputStreamRunloopMode,网络请求会从单例线程的runLoopModes中发出,这样当网络请求返回时,回调代码也会在runLoopModes中去执行。

operationDidStart方法中会调用NSURLConnectionscheduleInRunLoop:forMode:方法,将网络请求任务派发到Runloop指定的Mode中。我觉得给Operation设置runLoopModes其实意义不大,因为常驻线程基本上只会有一个Mode,也就是NSRunloopDefaultMode,基本上不会有其他Mode,所以这里设置runLoopModes没什么意义。

- (void)operationDidStart {
    self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];

    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    for (NSString *runLoopMode in self.runLoopModes) {
        [self.connection scheduleInRunLoop:runLoop forMode:runLoopMode];
        [self.outputStream scheduleInRunLoop:runLoop forMode:runLoopMode];
    }

    [self.outputStream open];
    [self.connection start];
}

代理方法

AFURLConnectionOperation是通过NSURLConnection实现网络请求的,这里简单讲一下operation中代理方法的实现。

AFN实现了https证书验证的代码,具体实现和AFURLSessionManager基本类似,并且也是通过AFSecurityPolicy来处理具体的证书验证逻辑。

- (void)connection:(NSURLConnection *)connection
willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;

关于请求服务器数据这块值得讲一下,在NSURLConnection接收服务器数据时,AFN通过创建了一个outputStream,来承载和组织具体的数据,并且在内存中进行存储。当没有可用空间或发生其他错误时,会通过streamError的方式进行体现。

当网络请求结束时,会调用didFinishLoading方法,AFN会从outputStream中拿出数据并赋值给responseData,当做返回值数据使用。

- (void)connection:(NSURLConnection __unused *)connection
    didReceiveData:(NSData *)data
{
    NSUInteger length = [data length];
    while (YES) {
        NSInteger totalNumberOfBytesWritten = 0;
        if ([self.outputStream hasSpaceAvailable]) {
            const uint8_t *dataBuffer = (uint8_t *)[data bytes];

            NSInteger numberOfBytesWritten = 0;
            while (totalNumberOfBytesWritten < (NSInteger)length) {
                numberOfBytesWritten = [self.outputStream write:&dataBuffer[(NSUInteger)totalNumberOfBytesWritten] maxLength:(length - (NSUInteger)totalNumberOfBytesWritten)];
                if (numberOfBytesWritten == -1) {
                    break;
                }

                totalNumberOfBytesWritten += numberOfBytesWritten;
            }

            break;
        } else {
            [self.connection cancel];
            if (self.outputStream.streamError) {
                [self performSelector:@selector(connection:didFailWithError:) withObject:self.connection withObject:self.outputStream.streamError];
            }
            return;
        }
    }

    if (self.downloadProgress) {
        self.downloadProgress(length, self.totalBytesRead, self.response.expectedContentLength);
    }
}

- (void)connectionDidFinishLoading:(NSURLConnection __unused *)connection {
    self.responseData = [self.outputStream propertyForKey:NSStreamDataWrittenToMemoryStreamKey];

    [self.outputStream close];
    if (self.responseData) {
       self.outputStream = nil;
    }

    self.connection = nil;

    [self finish];
}

outputStream,也会有与之对应的inputStreaminputStream实现很简单,就是修改NSMutableURLRequestHTTPBodyStream

- (NSInputStream *)inputStream {
    return self.request.HTTPBodyStream;
}

- (void)setInputStream:(NSInputStream *)inputStream {
    NSMutableURLRequest *mutableRequest = [self.request mutableCopy];
    mutableRequest.HTTPBodyStream = inputStream;
    self.request = mutableRequest;
}

内存管理


在创建AFHTTPRequestOperation时会将successfailureblock传给operation,并且在operation执行完成并回调completionBlock时,执行这两个block代码。但是由于completionBlock中直接使用了self,导致了循环引用的问题。

- (void)setCompletionBlockWithSuccess:(void (^)(AFHTTPRequestOperation *operation, id __nullable responseObject))success
                              failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure {
      self.completionBlock = ^{
          // something code ...
      };
}

completionBlock的循环引用是AFN有意而为之的,为的就是保持operation的生命周期,以保证请求处理完成并接收返回的block回调。

对于循环引用的生命周期,AFN采取的是主动打破循环引用的方式,也就是重写父类的completionBlock,并且在调用block结束后,主动将completionBlock赋值为nil,从而主动打破循环引用。

- (void)setCompletionBlock:(void (^)(void))block {
    if (!block) {
        [super setCompletionBlock:nil];
    } else {
        __weak __typeof(self)weakSelf = self;
        [super setCompletionBlock:^ {
            __strong __typeof(weakSelf)strongSelf = weakSelf;

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
            dispatch_group_t group = strongSelf.completionGroup ?: url_request_operation_completion_group();
            dispatch_queue_t queue = strongSelf.completionQueue ?: dispatch_get_main_queue();
#pragma clang diagnostic pop

            dispatch_group_async(group, queue, ^{
                block();
            });

            dispatch_group_notify(group, url_request_operation_completion_queue(), ^{
                [strongSelf setCompletionBlock:nil];
            });
        }];
    }
}

AFNetworkReachabilityManager

AFNetworking中还有很重要的一部分,就是Reachability,用来做网络状态监控的。AFNetworkingYYKit、苹果官方都提供有ReachabilityAPI使用,内部实现原理基本差不多。

代码实现也很简单,主要依赖SystemConfiguration.framework框架的SCNetworkReachability,注册一个Callback然后等着回调就可以。这里讲一下核心逻辑,一些细枝末节的就忽略了。

Reachability提供了两种初始化方法,一种是通过域名初始化的managerForDomain:方法,传入一个域名,基于这个域名的访问情况来判断当前网络状态。另一种是通过地址初始化的managerForAddress:方法,创建一个sockaddr_in对象,并基于这个对象来判断网络状态。

+ (instancetype)managerForAddress:(const void *)address {
    SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr *)address);
    AFNetworkReachabilityManager *manager = [[self alloc] initWithReachability:reachability];

    CFRelease(reachability);
    
    return manager;
}

+ (instancetype)manager {
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_len = sizeof(address);
    address.sin_family = AF_INET;
    return [self managerForAddress:&address];
}

下面startMonitoring中是开启网络监测的核心代码,主要逻辑是设置了两个Callback,一个是block的一个是函数的,并添加到Runloop中开始监控。由此可以推测,Reachability的代码实现主要依赖Runloop的事件循环,并且在事件循环中判断网络状态。

当网络发生改变时,就会回调AFNetworkReachabilityCallback函数,回调有三个参数。targetSCNetworkReachabilityRef对象,flags是网络状态,info是我们设置的block回调参数。回调Callback函数后,内部会通过block以及通知的形式,对外发出回调。

- (void)startMonitoring {
    [self stopMonitoring];

    if (!self.networkReachability) {
        return;
    }

    __weak __typeof(self)weakSelf = self;
    AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
        __strong __typeof(weakSelf)strongSelf = weakSelf;

        strongSelf.networkReachabilityStatus = status;
        if (strongSelf.networkReachabilityStatusBlock) {
            strongSelf.networkReachabilityStatusBlock(status);
        }

    };

    SCNetworkReachabilityContext context = {0, (__bridge void *)callback, AFNetworkReachabilityRetainCallback, AFNetworkReachabilityReleaseCallback, NULL};
    SCNetworkReachabilitySetCallback(self.networkReachability, AFNetworkReachabilityCallback, &context);
    SCNetworkReachabilityScheduleWithRunLoop(self.networkReachability, CFRunLoopGetMain(), kCFRunLoopCommonModes);

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0),^{
        SCNetworkReachabilityFlags flags;
        if (SCNetworkReachabilityGetFlags(self.networkReachability, &flags)) {
            AFPostReachabilityStatusChange(flags, callback);
        }
    });
}

- (void)stopMonitoring {
    if (!self.networkReachability) {
        return;
    }

    SCNetworkReachabilityUnscheduleFromRunLoop(self.networkReachability, CFRunLoopGetMain(), kCFRunLoopCommonModes);
}

static void AFNetworkReachabilityCallback(SCNetworkReachabilityRef __unused target, SCNetworkReachabilityFlags flags, void *info) {
    AFPostReachabilityStatusChange(flags, (__bridge AFNetworkReachabilityStatusBlock)info);
}

AFNetworking总结


AFNetworking对请求数据的序列化,以及返回数据的反序列化做了很多处理。使开发者只需要传入一个字典即可构建请求参数,无需处理拼接到URL后面或参数转换为body二进制的细节,这些都由AFNetworking内部处理并进行容错,开发者只需要指定请求方式即可。

通过AFNetworking实现https也很方便,AFSecurityPolicy可以很好的管理CA以及自签名证书,以及处理https请求过程中的一些逻辑,我们只需要告诉AFNetworking怎么处理即可,如果不指定处理方式则使用默认CA证书的方式处理。

AFNetworking对于后台下载以及断点续传有很好的支持,我们可以在AFNetworking的基础上,很简单的就完成一个下载模块的设计。如果自己写后台下载和断点续传的代码,工作量还是不小的。

并且AFNetworking在网络库的设计上还提供了很强的自定义性,例如指定证书、URL缓存处理,以及下载过程中不同下载阶段的处理。如果没有提供自定义处理,则使用默认处理方式。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350