AFNetworking3.0后为什么不再需要常驻线程?

2018 年 10 月 21 日 CocoaChina

本公众号内容均为本号转发,已尽可能注明出处。因未能核实来源或转发内容图片有权利瑕疵的,请及时联系本号,本号会第一时间进行修改或删除。 QQ : 3442093904 


最近在补源码阅读方面的短板,第一个选择的就是AFNetworking,一方面AF的编码思路、代码质量都属于开源框架的上乘;另一方面也可以借机温习一下网络方面的东西。


AF源码解析的系列文章有很多(文末有我看过的一些推荐给大家),本文不对AF作全面的解析,仅从常驻线程这个角度解析一下2.0和3.0的差异。


AF2.x为什么需要常驻线程?


NSURLConnection


先来看看 NSURLConnection 发送请求时的线程情况,NSURLConnection 是被设计成异步发送的,调用了start方法后,NSURLConnection 会新建一些线程用底层的 CFSocket 去发送和接收请求,在发送和接收的一些事件发生后通知原来线程的Runloop去回调事件。


大概有三种方法使用NSURLConnection


A.在主线程异步回调


若直接在主线程异步回调会存在两个问题:


[[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES


1、当在主线程调用上面的初始化方法时,监听回调的任务会加入到主线程的 Runloop 下,主线程的Runloop默认的 RunloopMode 是 NSDefaultRunLoopMode。当用户滑动 scrollview 时,RunloopMode会切换到 NSEventTrackingRunLoopMode 模式,这个时候回调函数就不会执行了,直到用户停止滑动。


这个问题可以通过如下方法来解决:


NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO]; 
//设置 RunloopMode 为 NSRunLoopCommonModes(即使用户滑动 scrollview 也能即时执行回调函数)
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; 
[connection start];


2、作为网络层框架,在 NSURLConnection 回调回来之后,必定要对 Response 做一些诸如序列化、错误处理的操作的。这些通用操作势必要放在子线程去做掉,接着回到主线程,框架的使用者只需要拿到处理后的 Response 进行UI 刷新即可。(PASS)


B.一个请求一条线程


来一个请求开辟一条线程,设置runloop保活线程,等待结果回调。这种方式理论上是可行的,但是你也看到了,线程开销太大了。(PASS)


C.一条常驻线程


只开辟一条子线程,设置runloop使线程常驻。所有的请求在这个线程上发起、同时也在这个线程上回调。


那有人会问:那网络请求岂不是变成了单线程?


//networkRequestThread即常驻线程
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];


- (void)operationDidStart {
    [self.lock lock];
    if (![self isCancelled]) {
        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];
    }
    [self.lock unlock];
    dispatch_async(dispatch_get_main_queue(), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingOperationDidStartNotification object:self];
    });
}


首先,每一个请求对应一个AFHTTPRequestOperation实例对象(以下简称operation),每一个operation在初始化完成后都会被添加到一个NSOperationQueue中。


由这个NSOperationQueue来控制并发,系统会根据当前可用的核心数以及负载情况动态地调整最大的并发 operation 数量,我们也可以通过setMaxConcurrentoperationCount:方法来设置最大并发数。注意:并发数并不等于所开辟的线程数。具体开辟几条线程由系统决定。


也就是说此处执行operation是并发的、多线程的。



经过上面ABC方案的分析,最后再来小结一下为什么AF2.x需要一条常驻线程:


首先需要在子线程去start connection,请求发送后,所在的子线程需要保活以保证正常接收到 NSURLConnectionDelegate 回调方法。如果每来一个请求就开一条线程,并且保活线程,这样开销太大了。所以只需要保活一条固定的线程,在这个线程里发起请求、接收回调。


AF3.x为什么不再需要常驻线程?


标题写的是“AFNetworking3.0后为什么不再需要常驻线程?”,然而却花了大半的篇幅解析了AF2.x为什么需要常驻线程?EXO ME??


其实明白了AF2.x为什么需要常驻线程之后,再看一下AF3.x,很快就能知道答案了~


NSURLConnection的一大痛点就是:发起请求后,这条线程并不能随风而去,而需要一直处于等待回调的状态。


苹果也是明白了这一痛点,从iOS9.0开始 deprecated 了NSURLConnection。 替代方案就是NSURLSession。当然NSURLSession还解决了很多其他的问题,这里不作赘述。


self.operationQueue = [[NSOperationQueue alloc] init];
self.operationQueue.maxConcurrentOperationCount = 1;
self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];


为什么说NSURLSession解决了NSURLConnection的痛点,从上面的代码可以看出,NSURLSession发起的请求,不再需要在当前线程进行代理方法的回调!可以指定回调的delegateQueue,这样我们就不用为了等待代理回调方法而苦苦保活线程了。


同时还要注意一下,指定的用于接收回调的Queue的maxConcurrentOperationCount设为了1,这里目的是想要让并发的请求串行的进行回调。


为什么要串行回调?


- (AFURLSessionManagerTaskDelegate *)delegateForTask:(NSURLSessionTask *)task {
    NSParameterAssert(task);
    AFURLSessionManagerTaskDelegate *delegate = nil;
    [self.lock lock];
    //给所要访问的资源加锁,防止造成数据混乱
    delegate = self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)];
    [self.lock unlock];
    return delegate;
}


这边对 self.mutableTaskDelegatesKeyedByTaskIdentifier 的访问进行了加锁,目的是保证多线程环境下的数据安全。既然加了锁,就算maxConcurrentOperationCount不设为1,当某个请求正在回调时,下一个请求还是得等待一直到上个请求获取完所要的资源后解锁,所以这边并发回调也是没有意义的。相反多task回调导致的多线程并发,还会导致性能的浪费。



补充1:


AF3.x会给每个 NSURLSessionTask 绑定一个 AFURLSessionManagerTaskDelegate ,这个TaskDelegate相当于把NSURLSessionDelegate进行了一层过滤,最终只保留类似didCompleteWithError这样对上层调用者输出的回调。


- (void)URLSession:(__unused NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
//此处代码进行了大量删减,只是为了让大家清楚的看到这个方法做的最重要的事
dispatch_group_async(manager.completionGroup ?: url_session_manager_completion_group(), manager.completionQueue ?: dispatch_get_main_queue(), ^{
            if (self.completionHandler) {
                self.completionHandler(task.response, responseObject, error);
            }
    }
}


补充2:


面试官可能会问你:为什么AF3.0中需要设置


self.operationQueue.maxConcurrentOperationCount = 1;


而AF2.0却不需要?


这个问题不难,但是却可以帮助面试官判断面试者是否真的认真研读了AF的两个大版本的源码。


解答:功能不一样:AF3.0的operationQueue是用来接收NSURLSessionDelegate回调的,鉴于一些多线程数据访问的安全性考虑,设置了maxConcurrentOperationCount = 1来达到串行回调的效果。


而AF2.0的operationQueue是用来添加operation并进行并发请求的,所以不要设置为1。


- (AFHTTPRequestOperation *)POST:(NSString *)URLString
                      parameters:(id)parameters
                         success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
                         failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
    AFHTTPRequestOperation *operation = [self HTTPRequestOperationWithHTTPMethod:@"POST" URLString:URLString parameters:parameters success:success failure:failure];
    [self.operationQueue addOperation:operation];
    return operation;
}


补充3:AF中常驻线程的实现(经典案例,不作赘述)


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


首先用NSThread创建了一个线程,并且这个线程是个单例。


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

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


新建的子线程默认是没有添加Runloop的,因此给这个线程添加了一个runloop,并且加了一个NSMachPort,来防止这个新建的线程由于没有活动直接退出。


参考资料


AFNetworking到底做了什么?涂耀辉

AFNetworking2.0源码解析<一> bang


作者:dj_rose

链接:https://www.jianshu.com/p/b5c27669e2c1


相关推荐:


登录查看更多
0

相关内容

【2020新书】实战R语言4,323页pdf
专知会员服务
102+阅读 · 2020年7月1日
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
60+阅读 · 2020年6月26日
【实用书】Python技术手册,第三版767页pdf
专知会员服务
239+阅读 · 2020年5月21日
【实用书】Python爬虫Web抓取数据,第二版,306页pdf
专知会员服务
122+阅读 · 2020年5月10日
【干货书】流畅Python,766页pdf,中英文版
专知会员服务
226+阅读 · 2020年3月22日
TensorFlow Lite指南实战《TensorFlow Lite A primer》,附48页PPT
专知会员服务
70+阅读 · 2020年1月17日
转岗产品经理,花了3个月都做不好需求工作
人人都是产品经理
10+阅读 · 2019年9月16日
msf实现linux shell反弹
黑白之道
49+阅读 · 2019年8月16日
Kali Linux 渗透测试:密码攻击
计算机与网络安全
18+阅读 · 2019年5月13日
PHP使用Redis实现订阅发布与批量发送短信
安全优佳
7+阅读 · 2019年5月5日
Pupy – 全平台远程控制工具
黑白之道
43+阅读 · 2019年4月26日
R工程化—Rest API 之plumber包
R语言中文社区
11+阅读 · 2018年12月25日
一天精通无人中级篇:遥控器协议 S-BUS
无人机
52+阅读 · 2018年12月20日
比Selenium快100倍的方法爬东方财富网财务报表
程序人生
8+阅读 · 2018年10月31日
Android P正式发布,你需要尽快做适配了
前端之巅
3+阅读 · 2018年8月7日
前端高性能计算(4):GPU加速计算
前端大全
7+阅读 · 2017年10月26日
Directions for Explainable Knowledge-Enabled Systems
Arxiv
26+阅读 · 2020年3月17日
Neural Approaches to Conversational AI
Arxiv
8+阅读 · 2018年12月13日
Attend More Times for Image Captioning
Arxiv
6+阅读 · 2018年12月8日
Arxiv
3+阅读 · 2018年10月25日
Arxiv
4+阅读 · 2018年5月10日
Arxiv
8+阅读 · 2018年4月8日
VIP会员
相关VIP内容
【2020新书】实战R语言4,323页pdf
专知会员服务
102+阅读 · 2020年7月1日
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
60+阅读 · 2020年6月26日
【实用书】Python技术手册,第三版767页pdf
专知会员服务
239+阅读 · 2020年5月21日
【实用书】Python爬虫Web抓取数据,第二版,306页pdf
专知会员服务
122+阅读 · 2020年5月10日
【干货书】流畅Python,766页pdf,中英文版
专知会员服务
226+阅读 · 2020年3月22日
TensorFlow Lite指南实战《TensorFlow Lite A primer》,附48页PPT
专知会员服务
70+阅读 · 2020年1月17日
相关资讯
转岗产品经理,花了3个月都做不好需求工作
人人都是产品经理
10+阅读 · 2019年9月16日
msf实现linux shell反弹
黑白之道
49+阅读 · 2019年8月16日
Kali Linux 渗透测试:密码攻击
计算机与网络安全
18+阅读 · 2019年5月13日
PHP使用Redis实现订阅发布与批量发送短信
安全优佳
7+阅读 · 2019年5月5日
Pupy – 全平台远程控制工具
黑白之道
43+阅读 · 2019年4月26日
R工程化—Rest API 之plumber包
R语言中文社区
11+阅读 · 2018年12月25日
一天精通无人中级篇:遥控器协议 S-BUS
无人机
52+阅读 · 2018年12月20日
比Selenium快100倍的方法爬东方财富网财务报表
程序人生
8+阅读 · 2018年10月31日
Android P正式发布,你需要尽快做适配了
前端之巅
3+阅读 · 2018年8月7日
前端高性能计算(4):GPU加速计算
前端大全
7+阅读 · 2017年10月26日
相关论文
Directions for Explainable Knowledge-Enabled Systems
Arxiv
26+阅读 · 2020年3月17日
Neural Approaches to Conversational AI
Arxiv
8+阅读 · 2018年12月13日
Attend More Times for Image Captioning
Arxiv
6+阅读 · 2018年12月8日
Arxiv
3+阅读 · 2018年10月25日
Arxiv
4+阅读 · 2018年5月10日
Arxiv
8+阅读 · 2018年4月8日
Top
微信扫码咨询专知VIP会员