读AFNetworking 3.0 源码记录

知名的iOS网络框架 AFNetworking 3.0 发布一段时间了,现在来阅读记录一下。(注:我目前阅读的版本是3.1.0)

AFNetworking 3.0 的改动


AFNetworking 3.0 抛弃了基于 NSURLConnection 的API,全力支持 NSURLSession, 在Xcode7中,苹果明确说明废弃了 NSURLConnection, 建议全面使用 NSURLSession:

/*** DEPRECATED: The NSURLConnection class should no longer be used.  NSURLSession is the replacement for NSURLConnection ***/

所以, AFURLConnectionOperation, AFHTTPRequestOperation , AFHTTPRequestOperationManager 这几个2.0版本中极为关键的几个类已被全部移除。

AFURLSessionManager


AFNetworking 3.0 中关键的几个类:AFURLSessionManagerAFHTTPSessionManager,先来看看 AFURLSessionManager 文件:

  1. AFURLSessionManager 的初始化,在 initWithSessionConfiguration 方法:
- (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];

    .....省略......
    return self;
}
  • 创建NSURLSession时需要设置NSURLSessionConfiguration,NSURLSessionConfiguration 有三种模式:

    1. 一般模式(default):工作模式类似于原来的NSURLConnection,可以使用缓存的Cache,Cookie,鉴权。
    2. 及时模式(ephemeral):不保存任何数据到磁盘,不使用缓存的Cache,Cookie,鉴权。
    3. 后台模式(background):支持在后台完成上传下载 (需要iOS 8以上)
  • 创建NSURLSession时还需要设置 delegate , delegate 用来处理请求中的各种事件,可以设置为nil使用系统提供的delegate,但是要想支持后台传输数据必须提供自定义实现的delegate;另外,NSURLSession对象是强引用了delegate,如果app最终没有调用 invalidateAndCancel 方法 来invalidate 该session的话,则会造成内存泄漏。

  • 创建NSURLSession时可以设置相应的 OperationQueue, 决定请求过程中的一系列事件在哪个 OperationQueue 回调,这里是设置了最大并发量为1的队列,也就相当于串行队列了。(AFNetworing 2.0 版本是设置了一条常驻线程来响应所有网络请求的delegate事件)

2 接着 AFURLSessionManager 当然实现了 NSURLSession Delegate的各个接口,挺容易看的,还是看一看它暴露的创建请求任务的方法吧:


- (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;
}
  • 这里使用 url_session_manager_create_task_safely 的方法,是为了解决iOS8以下如果并发创建NSURLSessionTask时会出现的bug(源码里有对应的链接)。这时候获取的 NSURLSessionDataTask 是出于挂起状态的,还不会发起网络请求。NSURLSessionTask 有三个职能不同的子类,

    • NSURLSessionDataTask: 用于一般的请求资源,以NSData对象的方式返回服务器响应的数据,它不支持backround session;
    • NSURLSessionUploadTask: 用于上传,支持backround session;
    • NSURLSessionDownloadTask: 用于下载数据到文件中,也支持backround session。
  • 我们再看看里面设置task代理的方法:

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

针对于每个data task均有对应的delegate,这个一对一关系是保存在一个字典中,以task的唯一标志作为 key,并且取值赋值删除的时候均要上锁,确保线程安全;
再看一看自定义的 AFURLSessionManagerTaskDelegate 代理对象,它实现了不少功能:
1, 通过KVO的方式监听上传下载的进度并回调出去;
2, 请求接收的数据不断追加到 mutableData :

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

3, 设置当前task完成后的回调:

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error;

它是在NSURLSession的代理方法中被调用的,

- (void)URLSession:(NSURLSession *)session
              task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
    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);
    }
}

一开始容易混淆这两个回调,其实是当task完成数据传输后,会回调上面的方法,然后根据task从字典中取出对应的 AFURLSessionManagerTaskDelegate 对象,然后 AFURLSessionManagerTaskDelegate 对象再进行调用完成的callback:

- (void)URLSession:(__unused NSURLSession *)session
              task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{.......省略部分.....
    __strong AFURLSessionManager *manager = self.manager;
    __block id responseObject = nil;
    __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 (error) {
        .......省略........
    } else {
        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];
                });
            });
        });
    }
}

1, 这里有个技巧,将可变的数据拷贝后,马上将mutableData置为nil来释放回收内存,特别是处理大文件的时候效果就会出来了。具体描述请看这里.
2,它的流程是当请求没有出错时,异步调用 block 处理序列化task的响应对象,然后把数据对象再通过group异步回调出去.


其它:
a. NSStringFromSelector(_cmd)
_cmd表示当前方法的selector,正如self代表了当前方法调用的对象

// AFURLSessionManager中的方法:
- (NSArray *)tasks {
    //这里的NSStringFromSelector(_cmd) 与 NSStringFromSelector(@selector(tasks)) 相等
    return [self tasksForKeyPath:NSStringFromSelector(_cmd)];
}

b. Method Swizzle
在AFURLSessionManager文件中,有一段代码用方法混写的方式修复了iOS7 iOS8 NSURLSessionTask 出现的问题,方法混写的知识已经烂大街了,就不写了,这个问题具体再看源码链接吧。

AFHTTPSessionManager


1, AFHTTPSessionManager 实现类就几百行代码,比较容易看,来看看它提供的GET请求方法吧:

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

这里就是根据请求参数序列化创建请求对象设置进度以及请求成功或失败的回调,然后返回dataTask给你并启动,开始真正的网络请求了。

2,按照 AFNetworking 3.0 给出的迁移文档中,可以简单使用 AFHTTPSessionManager 来发起GET请求:

AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager GET:@"http://example.com/resources.json" parameters:nil progress:nil success:^(NSURLSessionTask *task, id responseObject) {
    NSLog(@"JSON: %@", responseObject);
} failure:^(NSURLSessionTask *operation, NSError *error) {
    NSLog(@"Error: %@", error);
}];

很熟悉是吧,跟2.0版本一样,获取manager后调用GET方法,但是来看看AFHTTPSessionManager 类的manager 方法:

+ (instancetype)manager {
    return [[[self class] alloc] initWithBaseURL:nil];
}

问题来了,如果这么用的话,AFHTTPSessionManager对象调用请求GET方法后,一直没有被释放,因为它一直强引用着session即NSURLSession对象,而session一直被session的delegate强引用着(上面有提到),这样就造成了循环引用导致内存泄漏。这是个坑啊😂。这个问题很早以前就有人在Github上提过了,@mattt当时也回复了这里.
然后我看了AFNetworking 3.0 的示例Demo,发现它是这么用的,创建一个继承AFHTTPSessionManager的类,提供获取单例的方法:

+ (instancetype)sharedClient {
    static AFAppDotNetAPIClient *_sharedClient = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _sharedClient = [[AFAppDotNetAPIClient alloc] initWithBaseURL:[NSURL URLWithString:AFAppDotNetAPIBaseURLString]];
        _sharedClient.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];
    });
    return _sharedClient;
}

那么以后再封装AFNetworking 3.0 来用的话,得注意一下这个问题咯。另外swift版本的 Alamofire 的实现是不一样的,有点差别,(呵呵):

public static let sharedInstance: Manager = {
        let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
        configuration.HTTPAdditionalHeaders = Manager.defaultHTTPHeaders

        return Manager(configuration: configuration)
 }()

最后


先写到这里吧,有空再补充。以上均个人见解,欢迎交流。另外,个人简单封装了一下AFNetworking 3.0以便快捷安全使用,地址如下:Githud.

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

推荐阅读更多精彩内容