iOS 移动开发网络 part3:AFNetworking

本文是我对AFNetworking代码模块的总结,想做到有问题知道找谁(找AFNetworking的哪个模块),而非完完整整的源码解析,要看源码解析我推荐辉哥解析AFNetworking源码的系列文章.本文大体基于AFNetworking3.0进行学习.后面也会有对比AFNetworking3.0AFNetworking2.0的内容.

1.概观

AFNetworking大概可以分为:

主干:
请求组织模块
管理者
返回数据解析模块

补充:
网络通畅检测模块
安全模块
UI关联模块

请求组织模块,管理者,返回数据解析模块分为主干,是因为这三者连起来说可以清晰的描绘出AFNetworking网络请求的具体流程.
这样一分方便自己写总结,不是强调谁比较重要,都重要, .

2.主干

- (void)request_delegate{
    _allData = [NSMutableData data];
    
    NSURLSessionConfiguration *configura = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:configura delegate:self delegateQueue:nil];
    NSURL *url = [NSURL URLWithString:@"http://c.3g.163.com/photo/api/list/0096/4GJ60096.json"];
    NSURLRequest * request = [[NSURLRequest alloc]initWithURL:url];
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request];
    [dataTask resume];
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    if (error) {
        NSLog(@"%@",[error localizedDescription]);
        return;
    }
    
    NSError *er = nil;
    id result = [NSJSONSerialization JSONObjectWithData:_allData options:NSJSONReadingMutableContainers error:&er];
    NSLog(@"result: %@", result);
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    [_allData appendData:data];
}
- (void)afn_request{
    AFHTTPSessionManager * manger = [AFHTTPSessionManager manager];
    manger.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript",@"text/html",@"text/plain", nil];
    [manger GET:@"http://c.3g.163.com/photo/api/list/0096/4GJ60096.json" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, NSDictionary * responseData) {
        NSLog(@"responseData-----%@",responseData);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        
    }];
}

以上是一份用NSURLSession请求数据的代码和一份用AFNetworking请求数据的代码.AFNetworking的代码很简洁是因为很多操作让封装好的工具类给做了.

2.1 请求组织模块

我们的请求组织

NSURL *url = [NSURL URLWithString:@"http://c.3g.163.com/photo/api/list/0096/4GJ60096.json"];
NSURLRequest * request = [[NSURLRequest alloc]initWithURL:url];

我们的请求组织就上面两句,那是因为我们做的请求本身比较简单.NSURLRequest内部其实有很多参数.

NSURLRequest包含请求信息有:
请求方法:GET,POST..
URL
参数
超时时间
HTTP头信息:USER-PARAM...
等等

AFNetworking负责请求参数组织的是AFHTTPRequestSerializer.
AFHTTPRequestSerializer的工作就是收集我们通过简单方法传入的参数,生成一个NSURLRequest.

- GET:parameters:progress:success:failure:
   - dataTaskWithHTTPMethod:URLString:parameters:uploadProgressdownloadProgress:success:failure:
      - requestWithMethod:URLString:parameters:error:
         - requestBySerializingRequest:withParameters:error:
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
                                 URLString:(NSString *)URLString
                                parameters:(id)parameters
                                     error:(NSError *__autoreleasing *)error
{
    NSParameterAssert(method);
    NSParameterAssert(URLString);

    NSURL *url = [NSURL URLWithString:URLString];

    NSParameterAssert(url);

    NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url];
    mutableRequest.HTTPMethod = method;

    //zc read:改mutableRequest的部分属性
    //[allowsCellularAccess,cachePolicy,HTTPShouldHandleCookies,HTTPShouldUsePipelining,networkServiceType,timeoutInterval];
    //外围改变AFHTTPRequestSerializer的任何属性都会被记录下来,而当以上五个属性被修改,则需要将属性同步到生成的mutableRequest上
    for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
        if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
            [mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
        }
    }

    mutableRequest = [[self requestBySerializingRequest:mutableRequest withParameters:parameters error:error] mutableCopy];

    return mutableRequest;
}
- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
                               withParameters:(id)parameters
                                        error:(NSError *__autoreleasing *)error
{
    NSParameterAssert(request);

    NSMutableURLRequest *mutableRequest = [request mutableCopy];

    //zc read:改mutableRequest.HTTPRequestHeaders内键值
    [self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) {
        if (![request valueForHTTPHeaderField:field]) {
            [mutableRequest setValue:value forHTTPHeaderField:field];
        }
    }];

    //zc read:parameters由字典(@{@"kk1":@"vv1",@"kk2":@"vv2"})转成字符串(kk1=vv1&kk2=vv2)
    NSString *query = nil;
    if (parameters) {
        if (self.queryStringSerialization) {
            NSError *serializationError;
            query = self.queryStringSerialization(request, parameters, &serializationError);

            if (serializationError) {
                if (error) {
                    *error = serializationError;
                }

                return nil;
            }
        } else {
            switch (self.queryStringSerializationStyle) {
                case AFHTTPRequestQueryStringDefaultStyle:
                    query = AFQueryStringFromParameters(parameters);
                    break;
            }
        }
    }

    //zc read:self.HTTPMethodsEncodingParametersInURI==>{GET,HEAD,DELETE},只有这三个方法会将参数拼接到url后面
    if ([self.HTTPMethodsEncodingParametersInURI containsObject:[[request HTTPMethod] uppercaseString]]) {
        if (query && query.length > 0) {
            mutableRequest.URL = [NSURL URLWithString:[[mutableRequest.URL absoluteString] stringByAppendingFormat:mutableRequest.URL.query ? @"&%@" : @"?%@", query]];
        }
    } else {
        // #2864: an empty string is a valid x-www-form-urlencoded payload
        if (!query) {
            query = @"";
        }
        if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) {
            [mutableRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
        }
        [mutableRequest setHTTPBody:[query dataUsingEncoding:self.stringEncoding]];
    }

    return mutableRequest;
}

我们通过- GET:parameters:progress:success:failure:传入与生成NSURLRequest有关的只有:

URL
请求方法
parameters

AFHTTPRequestSerializer会在组织生成NSURLRequest的时候加入许多其他的参数,还会帮我们调整URL(如:GET请求的parameters,会帮我们拼在原来得URL后面).

当然简单的网络请求我们并不会直接用到AFHTTPRequestSerializer.我们如果要自定义AFNetworking的请求参数,可以改AFHTTPSessionManager->requestSerializer的相应属性.

NSURLRequest * request生成后,当然是要拿request去生成NSURLSessionDataTask,然后resume.代码如下(代码中有一些其他操作是用来绑定tasktask自己的代理(AFURLSessionManagerTaskDelegate)的,暂时先可以略过,后面会说):

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

2.2 管理者

管理者主要负责网络请求流程中的代理方法.

我们的代理方法实现

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    if (error) {
        NSLog(@"%@",[error localizedDescription]);
        return;
    }
    
    NSError *er = nil;
    id result = [NSJSONSerialization JSONObjectWithData:_allData options:NSJSONReadingMutableContainers error:&er];
    NSLog(@"result: %@", result);
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    [_allData appendData:data];
}
AFN的代理方法实现(这些方法由AFHTTPSessionManager的父类AFURLSessionManager实现)

- (void)URLSession:(NSURLSession *)session
              task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
    NSLog(@"zc 3 AFN NSURLSessionDataDelegate %s",__func__);
    //zc read:请求完成
    AFURLSessionManagerTaskDelegate *delegate = [self delegateForTask:task];
    
    // delegate may be nil when completing a task in the background
    if (delegate) {
        [delegate URLSession:session task:task didCompleteWithError:error];
        
        [self removeDelegateForTask:task];
    }
    
    if (self.taskDidComplete) {
        self.taskDidComplete(session, task, error);
    }
}

- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveData:(NSData *)data
{
    NSLog(@"zc 2 AFN NSURLSessionDataDelegate %s",__func__);
    //zc read:请求中途
    AFURLSessionManagerTaskDelegate *delegate = [self delegateForTask:dataTask];
    [delegate URLSession:session dataTask:dataTask didReceiveData:data];
}

以上AFHTTPSessionManager的两个代理方法里都出现了AFURLSessionManagerTaskDelegate.

由[iOS 移动开发网络 part2]我们已经知道,一个NSURLSession可以包含多个NSURLSessionTask.
NSURLSession可以设置代理,NSURLSessionTask是没有代理的,
多个NSURLSessionTask的情况通知一个'NSURLSession的代理',这样的交互是十分凌乱的.
最好的方式就是每个NSURLSessionTask都有自己的代理,
那么AFURLSessionManagerTaskDelegate就应运而生了.

NSURLSession>NSURLSessionTask 1对多
NSURLSession>AFHTTPSessionManager 1对1

AFHTTPSessionManager>AFURLSessionManagerTaskDelegate 1对多
AFURLSessionManagerTaskDelegate>NSURLSessionDataTask 1对1

那么AFHTTPSessionManager又是如何持有多个AFURLSessionManagerTaskDelegate的呢?
AFURLSessionManagerTaskDelegateNSURLSessionDataTask又是怎么绑定的呢?

iOS 移动开发网络 part3:AFNetworking_第1张图片
AFURLSessionManagerTaskDelegate_NSURLSessionDataTask.png

如图很清晰:

[AFURLSessionManager * manager].mutableTaskDelegatesKeyedByTaskIdentifier
以task的taskIdentifier为键保存着与task对应的AFURLSessionManagerTaskDelegate.

绑定方法taskAFURLSessionManagerTaskDelegate的具体方法如下:

这个方法前面已经提过.那会让略过,现在这说

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

当然对网络请求流程的掌控不止上面提到的两个代理方法,
AFHTTPSessionManager的父类AFURLSessionManager向外暴露了相应的block来掌控对应的代理方法.如:

重定向

@property (readwrite, nonatomic, copy) AFURLSessionTaskWillPerformHTTPRedirectionBlock taskWillPerformHTTPRedirection;

- (void)URLSession:(NSURLSession *)session
              task:(NSURLSessionTask *)task
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
        newRequest:(NSURLRequest *)request
 completionHandler:(void (^)(NSURLRequest *))completionHandler
{
    NSURLRequest *redirectRequest = request;

    if (self.taskWillPerformHTTPRedirection) {
        redirectRequest = self.taskWillPerformHTTPRedirection(session, task, response, request);
    }

    if (completionHandler) {
        completionHandler(redirectRequest);
    }
}

2.3 返回数据解析模块

我们的数据解析

NSError *er = nil;
id result = [NSJSONSerialization JSONObjectWithData:_allData options:NSJSONReadingMutableContainers error:&er];
NSLog(@"result: %@", result);

AFNetworking负责对返回数据解析的是AFHTTPResponseSerializer.

AFHTTPResponseSerializer
AFJSONResponseSerializer : AFHTTPResponseSerializer
AFXMLParserResponseSerializer : AFHTTPResponseSerializer
AFXMLDocumentResponseSerializer : AFHTTPResponseSerializer
AFPropertyListResponseSerializer : AFHTTPResponseSerializer
AFImageResponseSerializer : AFHTTPResponseSerializer
AFCompoundResponseSerializer : AFHTTPResponseSerializer
@protocol AFURLResponseSerialization 
- (nullable id)responseObjectForResponse:(nullable NSURLResponse *)response data:(nullable NSData *)data error:(NSError * _Nullable __autoreleasing *)error;
@end

AFHTTPResponseSerializer并不做解析数据的具体的事情,子类根据自己支持的数据类型来实现解析数据的协议方法.==>传说中的面向协议编程.

AFHTTPSessionManager默认会配备一个[AFJSONResponseSerializer serializer].
以下的方法调用栈看的也是AFJSONResponseSerializer内的实现.

responseObject = [manager.responseSerializer responseObjectForResponse:task.response data:data error:&serializationError];
- (id)responseObjectForResponse:(NSURLResponse *)response
                           data:(NSData *)data
                          error:(NSError *__autoreleasing *)error
{
 //验证不通过
    if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) {
  //没有error 或者 确定是上层校验报的错
        if (!error || AFErrorOrUnderlyingErrorHasCodeInDomain(*error, NSURLErrorCannotDecodeContentData, AFURLResponseSerializationErrorDomain)) {
            return nil;
        }
    }

    id responseObject = nil;
    NSError *serializationError = nil;
    // Workaround for behavior of Rails to return a single space for `head :ok` (a workaround for a bug in Safari), which is not interpreted as valid input by NSJSONSerialization.
    // See https://github.com/rails/rails/issues/1742
    BOOL isSpace = [data isEqualToData:[NSData dataWithBytes:" " length:1]];
    if (data.length > 0 && !isSpace) {
        responseObject = [NSJSONSerialization JSONObjectWithData:data options:self.readingOptions error:&serializationError];
    } else {
        return nil;
    }

    if (self.removesKeysWithNullValues && responseObject) {
        responseObject = AFJSONObjectByRemovingKeysWithNullValues(responseObject, self.readingOptions);
    }

    if (error) {
        *error = AFErrorWithUnderlyingError(serializationError, *error);
    }

    return responseObject;
}
//zc read:检验正常的数据类型和网络状态码
- (BOOL)validateResponse:(NSHTTPURLResponse *)response
                    data:(NSData *)data
                   error:(NSError * __autoreleasing *)error
{
    BOOL responseIsValid = YES;
    NSError *validationError = nil;

    if (response && [response isKindOfClass:[NSHTTPURLResponse class]]) {
        if (self.acceptableContentTypes && ![self.acceptableContentTypes containsObject:[response MIMEType]] &&
            !([response MIMEType] == nil && [data length] == 0)) {

            if ([data length] > 0 && [response URL]) {
                NSMutableDictionary *mutableUserInfo = [@{
                                                          NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"Request failed: unacceptable content-type: %@", @"AFNetworking", nil), [response MIMEType]],
                                                          NSURLErrorFailingURLErrorKey:[response URL],
                                                          AFNetworkingOperationFailingURLResponseErrorKey: response,
                                                        } mutableCopy];
                if (data) {
                    mutableUserInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] = data;
                }

                validationError = AFErrorWithUnderlyingError([NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorCannotDecodeContentData userInfo:mutableUserInfo], validationError);
            }

            responseIsValid = NO;
        }

        if (self.acceptableStatusCodes && ![self.acceptableStatusCodes containsIndex:(NSUInteger)response.statusCode] && [response URL]) {
            NSMutableDictionary *mutableUserInfo = [@{
                                               NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"Request failed: %@ (%ld)", @"AFNetworking", nil), [NSHTTPURLResponse localizedStringForStatusCode:response.statusCode], (long)response.statusCode],
                                               NSURLErrorFailingURLErrorKey:[response URL],
                                               AFNetworkingOperationFailingURLResponseErrorKey: response,
                                       } mutableCopy];

            if (data) {
                mutableUserInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] = data;
            }

            validationError = AFErrorWithUnderlyingError([NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorBadServerResponse userInfo:mutableUserInfo], validationError);

            responseIsValid = NO;
        }
    }

    if (error && !responseIsValid) {
        *error = validationError;
    }

    return responseIsValid;
}

AFHTTPRequestSerializer组织请求参数;
AFHTTPSessionManager做为NSURLSession的代理掌控请求的中间流程,并为每一个task保有对应的AFURLSessionManagerTaskDelegate,方便对每一个task做到细致处理;
AFHTTPResponseSerializer做请求到数据的解析.

3.补充

3.1 网络通畅检测模块

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

这个模块很简单,关键代码就是上面这个方法.设置回调给系统的方法,并加入主线程的runloop内,实时监测网络的状态.
而且AFNetworkReachabilityManager全局是一个单例,我们直接用AFNetworkReachabilityManager,还是AFNetworking内部用的都是同一个.

3.2 安全模块

安全模块,因为AFNetworking与HTTPS挂钩,要大篇幅的介绍HTTPS的验证流程,所以单成一篇==>iOS 移动开发网络 part4:HTTPS.

3.3 UI关联模块

3.3.1 AFNetworkActivityIndicatorManager

首先说说AFNetworkActivityIndicatorManager,AFNetworking封装的提示用户网络状态的工具类.
AFNetworkActivityIndicatorManager有如下状态:

typedef NS_ENUM(NSInteger, AFNetworkActivityManagerState) {
  //没有请求
  AFNetworkActivityManagerStateNotActive,
  //请求延迟开始
  AFNetworkActivityManagerStateDelayingStart,
  //请求进行中
  AFNetworkActivityManagerStateActive,
  //请求延迟结束
  AFNetworkActivityManagerStateDelayingEnd
};

请求延迟开始:小菊花没有开始转,请求开始到请求结束的时间间隔小于延迟时间-->小菊花不转.
请求延迟结束:小菊花已经开始转,开始转到请求结束时间间隔小于延迟时间-->小菊花不立马结束.
有这样的延迟操作,小菊花就不会闪烁个不停.

AFNetworkActivityIndicatorManager内代码十分清晰,总结成示意图大致是:

iOS 移动开发网络 part3:AFNetworking_第2张图片
network_AFN_AFNetworkActivityIndicatorManager.png
  • step1:替换系统方法的实现,发广播

AFURLSessionManager.m_AFURLSessionTaskSwizzlingload方法内:

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

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

- (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];
    }
}
  • step2:收广播,更改网络请求活跃数
[[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];

- (void)networkRequestDidStart:(NSNotification *)notification {
    if ([AFNetworkRequestFromNotification(notification) URL]) {
        [self incrementActivityCount];
    }
}

- (void)networkRequestDidFinish:(NSNotification *)notification {
    if ([AFNetworkRequestFromNotification(notification) URL]) {
        [self decrementActivityCount];
    }
}
  • step3:根据现有状态和新的活跃数设置新的状态
- (void)updateCurrentStateForNetworkActivityChange {
    if (self.enabled) {
        switch (self.currentState) {
            case AFNetworkActivityManagerStateNotActive:
                if (self.isNetworkActivityOccurring) {
                    [self setCurrentState:AFNetworkActivityManagerStateDelayingStart];
                }
                break;
            case AFNetworkActivityManagerStateDelayingStart:
                //No op. Let the delay timer finish out.
                break;
            case AFNetworkActivityManagerStateActive:
                if (!self.isNetworkActivityOccurring) {
                    [self setCurrentState:AFNetworkActivityManagerStateDelayingEnd];
                }
                break;
            case AFNetworkActivityManagerStateDelayingEnd:
                if (self.isNetworkActivityOccurring) {
                    [self setCurrentState:AFNetworkActivityManagerStateActive];
                }
                break;
        }
    }
}
  • step4:用新的状态决定小菊花的显示与否,并决定延时定时器(完成延迟+活跃延迟)的开启或者关闭
- (void)setCurrentState:(AFNetworkActivityManagerState)currentState {
    @synchronized(self) {
        if (_currentState != currentState) {
            [self willChangeValueForKey:@"currentState"];
            _currentState = currentState;
            switch (currentState) {
                case AFNetworkActivityManagerStateNotActive:
                    [self cancelActivationDelayTimer];
                    [self cancelCompletionDelayTimer];
                    [self setNetworkActivityIndicatorVisible:NO];
                    break;
                case AFNetworkActivityManagerStateDelayingStart:
                    [self startActivationDelayTimer];
                    break;
                case AFNetworkActivityManagerStateActive:
                    [self cancelCompletionDelayTimer];
                    [self setNetworkActivityIndicatorVisible:YES];
                    break;
                case AFNetworkActivityManagerStateDelayingEnd:
                    [self startCompletionDelayTimer];
                    break;
            }
        }
        [self didChangeValueForKey:@"currentState"];
    }
}
  • step5:定时器到时间点再重新设置新的状态
- (void)activationDelayTimerFired {
    if (self.networkActivityOccurring) {
        [self setCurrentState:AFNetworkActivityManagerStateActive];
    } else {
        [self setCurrentState:AFNetworkActivityManagerStateNotActive];
    }
}
- (void)completionDelayTimerFired {
    [self setCurrentState:AFNetworkActivityManagerStateNotActive];
}
3.3.2 AFImageDownloader

AFImageDownloader主要用于图片下载.

UIImageView+AFNetworking.h

- (void)setImageWithURL:(NSURL *)url;
- (void)setImageWithURL:(NSURL *)url placeholderImage:(nullable UIImage *)placeholderImage;
- (void)setImageWithURLRequest:(NSURLRequest *)urlRequest placeholderImage:(nullable UIImage *)placeholderImage success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *image))success failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure;

UIImageView会用上面这些方法来加载网络图片,方法内部就调用了AFImageDownloader.

iOS 移动开发网络 part3:AFNetworking_第3张图片
network_AFN_AFImageDownloader.png

AFImageDownloader内部结构大略如上图所示.是有些凌乱,层级也多.不要怕,我们会分模块来说说:
AFImageDownloader下辖的属性:

@property (nonatomic, strong) NSMutableDictionary *mergedTasks;

@property (nonatomic, assign) AFImageDownloadPrioritization downloadPrioritizaton;
@property (nonatomic, strong) NSMutableArray *queuedMergedTasks;
@property (nonatomic, assign) NSInteger maximumActiveDownloads;
@property (nonatomic, assign) NSInteger activeRequestCount;

@property (nonatomic, strong, nullable) id  imageCache;

以上三个属性分区分别对应一个模块:创建下载任务与保存下载任务,多个下载任务执行顺序控制,图片缓存

  • 创建下载任务与保存下载任务

UIImageView+AFNetworking.h内各个设置网路图片方法最终会调用以下方法:

- (nullable AFImageDownloadReceipt *)downloadImageForURLRequest:(NSURLRequest *)request
                                                  withReceiptID:(nonnull NSUUID *)receiptID
                                                        success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse  * _Nullable response, UIImage *responseObject))success
                                                        failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure;

方法内部会创建AFImageDownloader中最小的下载任务AFImageDownloaderMergedTask.

作为单例的AFImageDownloader持有NSMutableDictionary *mergedTasks.
NSMutableDictionary *mergedTasksurl做键保存着AFImageDownloaderMergedTask.

那么AFImageDownloaderMergedTask又是什么?
AFImageDownloaderMergedTask是对NSURLSessionDataTask *task封装了一层(NSURLSessionDataTask利用AFHTTPSessionManager下载图片的过程就不展开说了,与请求十分类似).
AFImageDownloaderMergedTask还持有一个保存回调的数组==>NSMutableArray *responseHandlers.因为一个相同的url有可能出现多个地方的UIImageView都在加载.

AFImageDownloaderResponseHandler保存了成功的回调和失败的回调,还有一个NSUUID *uuid.这个NSUUID *uuid就是用于匹配对应的UIImageView的.

UIImageView调用AFNetworking加载图片的方法,最终会为UIImageView绑定一个AFImageDownloadReceipt *af_activeImageDownloadReceipt.这个AFImageDownloadReceipt持有:

@property (nonatomic, strong) NSURLSessionDataTask *task;
@property (nonatomic, strong) NSUUID *receiptID;

有了这两个属性就可以让AFImageDownloader取消对一个UIImageView的加载回调.

[imageView cancelImageDownloadTask];

- (void)cancelImageDownloadTask {
    if (self.af_activeImageDownloadReceipt != nil) {
        [[self.class sharedImageDownloader] cancelTaskForImageDownloadReceipt:self.af_activeImageDownloadReceipt];
        [self clearActiveDownloadInformation];
     }
}

- (void)cancelTaskForImageDownloadReceipt:(AFImageDownloadReceipt *)imageDownloadReceipt {
    dispatch_sync(self.synchronizationQueue, ^{
        NSString *URLIdentifier = imageDownloadReceipt.task.originalRequest.URL.absoluteString;
        AFImageDownloaderMergedTask *mergedTask = self.mergedTasks[URLIdentifier];
        NSUInteger index = [mergedTask.responseHandlers indexOfObjectPassingTest:^BOOL(AFImageDownloaderResponseHandler * _Nonnull handler, __unused NSUInteger idx, __unused BOOL * _Nonnull stop) {
            return handler.uuid == imageDownloadReceipt.receiptID;
        }];

        if (index != NSNotFound) {
            AFImageDownloaderResponseHandler *handler = mergedTask.responseHandlers[index];
            [mergedTask removeResponseHandler:handler];
            NSString *failureReason = [NSString stringWithFormat:@"ImageDownloader cancelled URL request: %@",imageDownloadReceipt.task.originalRequest.URL.absoluteString];
            NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey:failureReason};
            NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:userInfo];
            if (handler.failureBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    handler.failureBlock(imageDownloadReceipt.task.originalRequest, nil, error);
                });
            }
        }
        //zc read UIKit 6:没有回调且任务是暂停状态,取消任务
        if (mergedTask.responseHandlers.count == 0 && mergedTask.task.state == NSURLSessionTaskStateSuspended) {
            [mergedTask.task cancel];
            [self removeMergedTaskWithURLIdentifier:URLIdentifier];
        }
    });
}
字典:[AFImageDownloader]->[NSMutableDictionary *mergedTasks]
键:imageView.af_activeImageDownloadReceipt.task.originalRequest.URL.absoluteString

得到值:AFImageDownloaderMergedTask
数组:[AFImageDownloaderMergedTask]->NSMutableArray  *responseHandlers
用于匹配的UUID:imageView.receiptID

遍历得到值:AFImageDownloaderResponseHandler

AFImageDownloader中最小的下载任务是AFImageDownloaderMergedTask,AFImageDownloaderMergedTask包含NSURLSessionDataTask,NSURLSessionDataTask利用AFHTTPSessionManager进行下载.为UIImageView绑定的AFImageDownloadReceipt使得UIImageView可以找到自己的回调并予以取消.

  • 多个下载任务执行顺序控制

当用于下载的AFImageDownloaderMergedTask创建好之后,就要考虑是立马开始下载还是入队等待.当最大下载任务数大于当前下载任务数则立马开始,否则入队等待.

if ([self isActiveRequestCountBelowMaximumLimit]) {
    [self startMergedTask:mergedTask];
} else {
    [self enqueueMergedTask:mergedTask];
}

- (BOOL)isActiveRequestCountBelowMaximumLimit {
    return self.activeRequestCount < self.maximumActiveDownloads;
}

以下的startMergedTask:方法就是开启一个下载的task.enqueueMergedTask:方法是用来为入队的task做排序的.

- (void)startMergedTask:(AFImageDownloaderMergedTask *)mergedTask {
    [mergedTask.task resume];
    ++self.activeRequestCount;
}

- (void)enqueueMergedTask:(AFImageDownloaderMergedTask *)mergedTask {
    switch (self.downloadPrioritizaton) {
        case AFImageDownloadPrioritizationFIFO:
            [self.queuedMergedTasks addObject:mergedTask];
            break;
        case AFImageDownloadPrioritizationLIFO:
            [self.queuedMergedTasks insertObject:mergedTask atIndex:0];
            break;
    }
}

入队有分为:先进先出,后进先出.

typedef NS_ENUM(NSInteger, AFImageDownloadPrioritization) {
    //先进先出
    AFImageDownloadPrioritizationFIFO,
    //后进先出
    AFImageDownloadPrioritizationLIFO
};

先进先出:加入的task放在数组queuedMergedTasks的最后面
先进先出:加入的task放在数组queuedMergedTasks的最前面

  • 图片缓存

如上图可以看到AFImageDownloader有自己的缓存模块id imageCache(AFAutoPurgingImageCache).
同时AFImageDownloader->AFHTTPSessionManager->NSURLSession也有缓存模块NSURLCache.

AFAutoPurgingImageCacheAFNetworking自己用的,NSURLCache是给NSURLSession用的.那为什么AFNetworking不也用NSURLCache来做缓存呢?因为NSURLCache的诸多限制,例如只支持get请求.没法满足AFNetworking读写缓存的需求.

- (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;
- (void)addImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;

以上是AFAutoPurgingImageCache的读写方法,可以加附加的identifier,没有附加的identifier就用url做键来保存图片.

4.线程调度

本部分内容会有AFNetworking2.0的介绍.

4.1 NSURLSession的线程调度甩NSURLConnection几条街

NSURLConnection

- (void)requestByConnection{
    _allData = [NSMutableData data];
    
    NSURL *url = [NSURL URLWithString:@"http://c.3g.163.com/photo/api/list/0096/4GJ60096.json"];
    NSURLRequest * request = [[NSURLRequest alloc]initWithURL:url];
    NSURLConnection * connection = [[NSURLConnection alloc]initWithRequest:request delegate:self];
    [connection start];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
    [_allData appendData:data];
    NSLog(@"zc NSThread %@",[NSThread currentThread]);
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
    NSError *er = nil;
    id result = [NSJSONSerialization JSONObjectWithData:_allData options:NSJSONReadingMutableContainers error:&er];
    NSLog(@"zc NSThread %@",[NSThread currentThread]);
}

打印:
zc NSThread {number = 1, name = (null)}
zc NSThread {number = 1, name = (null)}
NSURLSession

- (void)requestBySession{
    _allData = [NSMutableData data];
    
    NSURLSessionConfiguration *configura = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:configura delegate:self delegateQueue:nil];
    NSURL *url = [NSURL URLWithString:@"http://c.3g.163.com/photo/api/list/0096/4GJ60096.json"];
    NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url];
    [dataTask resume];
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    NSError *er = nil;
    id result = [NSJSONSerialization JSONObjectWithData:_allData options:NSJSONReadingMutableContainers error:&er];
    NSLog(@"zc NSThread %@",[NSThread currentThread]);
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    [_allData appendData:data];
    NSLog(@"zc NSThread %@",[NSThread currentThread]);
}

打印:
zc NSThread {number = 5, name = (null)}
zc NSThread {number = 5, name = (null)}

以上是最简单的拿NSURLConnectionNSURLSession请求数据时,代理方法调用时的线程信息.NSURLConnection全程都在主线程,NSURLSession全程都在次线程.如此可见一斑==>NSURLSession的线程调度甩NSURLConnection几条街.

AFNetworking2.0封装NSURLConnection.
AFNetworking3.0封装NSURLSession.
而我们用起来却没什么差别,证明AFNetworking2.0AFNetworking3.0的线程调度会更有难度.

4.2 NSURLConnection VS NSRunLoop

一般来说我们直接用NSURLConnection在主线程上开异步请求也没什么大问题,但请求一多,返回数据也在主线程解析对UI的体验就有影响了.

上面的方法可以改进为:主线程发请求,收到数据后,开辟次线程解析.

同时我的第一直觉又想出一招:任意开一个次线程发请求,再任意开一个次线程解析数据.
我还写了代码试了试:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    NSURL *url = [NSURL URLWithString:@"http://c.3g.163.com/photo/api/list/0096/4GJ60096.json"];
    NSURLRequest * request = [[NSURLRequest alloc]initWithURL:url];
    NSURLConnection * connection = [[NSURLConnection alloc]initWithRequest:request delegate:self];
    [connection start];
    NSLog(@"zc NSThread 1 %@",[NSThread currentThread]);
});

搞笑的是代理方法全都没有调用,怎么回事?
点进NSURLConnection,看到这个方法:

- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSRunLoopMode)mode;

才知道NSURLConnection的运行载体是runLoop.NSURLConnection要么跑主线程的runLoop,要么跑次线程的runLoop,反正必须选一个runLoop,而且这个runLoop必须一直跟着请求的流程一直存活(这也就意味着runLoop对应的线程也必须跟着请求的流程一直存活),否则NSURLConnection就无法回调代理方法.

这也是很好的回答下面的两个问题:

为什么NSURLConnection的默认实现只能在主线程运行? 主线程一直存活
为什么我用NSURLConnection在次线程发请求,代理方法不回调? 发请求的次线程早死了
方案1.1:
主线程发请求,收到数据后,主线程解析.
请求过多时,影响UI体验.不好
方案1.2:
主线程发请求,收到数据后,开辟次线程解析.
这样多个请求的数据就对应多个次线程,多个次线程有性能损耗,线程间切换也带来的损耗.有瑕疵
方案2:
多个次线程发多个请求,保证多个次线程存活,收到数据后,对应次线程解析.
阻塞多个次线程.很瞎
方案3:
用特定的次线程发多个请求,保证特定线程存活,特定的次线程用于返回数据的解析.
✔️

AFNetworking2.0也正是用的方案3.保证特定线程存活的代码如下:

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

上面的两个方法给runLoopNSMachPort保证线程存活,早就被奉为传世经典,我就不多做讲解了.

4.3 NSOperation VS NSOperationQueue

iOS 移动开发网络 part3:AFNetworking_第4张图片
AFNetworking2.0.png

AFNetworking2.0内的各大模块的关系大概如上.
最小下载任务AFHTTPRequestOperation继承自NSOperation.我们调用请求方法创建AFHTTPRequestOperation完成后,AFHTTPRequestOperation被加入AFHTTPRequestOperationManager下辖的NSOperationQueue中.NSOperationQueue根据自己的任务并发数来确定任务何时执行,这个任务并发数我们是可以在外围设置的.

AFNetworking2.0的常驻线程完成所有网络请求是AFNetworking线程调度最精彩的一笔;
AFNetworking2.0最小下载任务AFHTTPRequestOperation继承自NSOperation,再加入NSOperationQueue中,也是间接的线程调度.
AFNetworking2.0AFNetworking3.0细小处都有,dispatch_group_t,dispatch_queue_t等等,我就不一一举例了哈.

5.AFNetworking功绩

5.1线程调度

刚说完,看上面.

5.2细化任务跟踪粒度

AFNetworking3.0时候已经说过,多个task走一个NSURLSession的代理方法粒度实在太大了,而AFNetworking3.0就真的做到了为每一task分配一个代理.

5.3请求组装+数据解析 方法协助

以下是NSURLSession上传多张照片的代码和AFNetworking3.0上传多张照片的代码的对比,请自行感受.

- (void)sendImagesByURLSession
{
    NSString *URLString = @"http://192.168.8.11/upload.php";
    NSString *serverFileName = @"zc[]";
    NSString *filePath1 = [[NSBundle mainBundle] pathForResource:@"on_show_1.png" ofType:nil];
    NSString *filePath2 = [[NSBundle mainBundle] pathForResource:@"on_show_2.png" ofType:nil];
    NSArray *filePaths = @[filePath1,filePath2];
    NSDictionary *textDict = @{@"kkk":@"vvv"};
    [self uploadFilesWithURLString:URLString serverFileName:serverFileName filePaths:filePaths textDict:textDict];
}

- (void)uploadFilesWithURLString:(NSString *)URLString serverFileName:(NSString *)serverFileName filePaths:(NSArray *)filePaths textDict:(NSDictionary *)textDict
{
    NSURL *URL = [NSURL URLWithString:URLString];
    
    NSMutableURLRequest *requestM = [NSMutableURLRequest requestWithURL:URL];
    [requestM setValue:@"multipart/form-data; boundary=itcast" forHTTPHeaderField:@"Content-Type"];
    requestM.HTTPMethod = @"POST";
    requestM.HTTPBody = [self getHTTPBodyWithServerFileName:serverFileName filePaths:filePaths textDict:textDict];
    [[[NSURLSession sharedSession] dataTaskWithRequest:requestM completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (error == nil && data != nil) {
            NSLog(@"%@",[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]);
            NSLog(@"44");
        } else {
            NSLog(@"%@",error);
        }
    }] resume];
}

- (NSData *)getHTTPBodyWithServerFileName:(NSString *)serverFileName filePaths:(NSArray *)filePaths textDict:(NSDictionary *)textDict
{
    NSMutableData *dataM = [NSMutableData data];
    
    [filePaths enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSMutableString *stringM = [NSMutableString string];
        [stringM appendString:@"--itcast\r\n"];
        [stringM appendFormat:@"Content-Disposition: form-data; name=%@; filename=%@\r\n",serverFileName,[obj lastPathComponent]];
        [stringM appendString:@"Content-Type: image/png\r\n"];
        [stringM appendString:@"\r\n"];
        [dataM appendData:[stringM dataUsingEncoding:NSUTF8StringEncoding]];
        [dataM appendData:[NSData dataWithContentsOfFile:obj]];
        [dataM appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
    }];
    
    [textDict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
        NSMutableString *stringM = [NSMutableString string];
        [stringM appendString:@"--itcast\r\n"];
        [stringM appendFormat:@"Content-Disposition: form-data; name=%@\r\n",key];
        [stringM appendString:@"\r\n"];
        [stringM appendFormat:@"%@\r\n",obj];
        
        [dataM appendData:[stringM dataUsingEncoding:NSUTF8StringEncoding]];
    }];
    
    [dataM appendData:[@"--itcast--" dataUsingEncoding:NSUTF8StringEncoding]];
    
    return dataM.copy;
}
- (void)sendImagesByAFN{
    NSString *filePath1 = [[NSBundle mainBundle] pathForResource:@"on_show_1.png" ofType:nil];
    NSString *filePath2 = [[NSBundle mainBundle] pathForResource:@"on_show_2.png" ofType:nil];
    
    NSData * data1 = [NSData dataWithContentsOfFile:filePath1];
    NSData * data2 = [NSData dataWithContentsOfFile:filePath2];
    
    NSArray * attributeDataArr = @[@{@"data":data1,@"name":@"zc[]",@"fileName":@"on_show_1",@"mimeType":@"image/png"},
                                   @{@"data":data2,@"name":@"zc[]",@"fileName":@"on_show_2",@"mimeType":@"image/png"}];
    
    AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
    [manager POST:@"http://192.168.8.11/upload.php" parameters:nil constructingBodyWithBlock:^(id formData) {
        for (NSDictionary * attributeData in attributeDataArr)
        {
            [formData appendPartWithFileData:attributeData[@"data"]
                                        name:attributeData[@"name"]
                                    fileName:attributeData[@"fileName"]
                                    mimeType:attributeData[@"mimeType"]];
        }
    } success:^(AFHTTPRequestOperation *operation, id responseObject) {
        
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        
    }];
}

5.4安全验证 方法协助

以自签名证书为例,我们只需要给AFSecurityPolicy设置一份本地的证书.而如果自己实现证书的遍历+验证等工作,还得与SecPolicy.framework里的API打交道,不是一般人能做到的.

5.5.太多了......

感谢大牛们的贡献,让我(们)可以像白痴一样和网络请求打交道那么久!!! .但现在我不想当白痴了.


文章参考:
AFNetworking到底做了什么?(系列文章)

你可能感兴趣的:(iOS 移动开发网络 part3:AFNetworking)