前言

对于iOS开发中的网络请求模块,AFNet的使用应该是最熟悉不过了,但你是否把握了网络请求正确的完成时机?本篇文章涉及线程同步、线程依赖、线程组等专用名词的含义,若对上述名词认识模糊,可先进行查阅理解后阅读本文。如果你也纠结于文中所述问题,可进行阅读希望对你有所帮助。大神无视勿喷。

在真实开发中,我们通常会遇到如下问题:

一、某界面存在多个请求,希望所有请求均结束才进行某操作。

对于这一问题的解决方案很容易想到通过线程组进行实现。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //请求1
        NSLog(@"Request_1");
    });
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //请求2
        NSLog(@"Request_2");
    });
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //请求3
        NSLog(@"Request_3");
    });
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        //界面刷新
        NSLog(@"任务均完成,刷新界面");
    });

打印如下

1
2
3
4
Request_2
Request_1
Request_3
任务均完成,刷新界面

根据打印结果观察可能并没有什么问题,但需要注意的是Request_1、Request_2等在真实开发中通常对应为某个网络请求。而网络请求通常为异步,那这时是否还会有同样结果呢?

口说无凭,我们将NSLog(@"Request");部分替换为真正的网络请求。

对于App请求数据大部分人都会选择AFNetworking。使用AFN异步请求,请求的数据返回后,就刷新相关UI。如果某一个页面有多个网络请求,我们假设有三个请求,A、B、C,而且UI里的数据必须等到A、B、C全部完成后刷新后才显示。

这里我们书写一个网络请求通用方法,假设同时请求某新闻列表的3页数据,每页均为一个独立的网络请求。使用我们最常用的AFNet请求,方法如下(真实开发中可能为banner数据请求、主体网络请求、广告网络请求等):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)request_A {
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];;
    manager.responseSerializer = [AFHTTPResponseSerializer serializer];
    NSDictionary *parameter = @{@"token":@"63104AB32427EBF89B957BBD1A5C5C11",
                                 @"page":@"1",
                                 @"upTime":@"desc"};
 
    [manager POST:URL parameters:parameter progress:^(NSProgress * _Nonnull uploadProgress) {
 
    } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
 
        NSDictionary * dict = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONReadingMutableContainers error:nil];
        for (NSDictionary *rowsDict in dict[@"rows"]) {
            NSLog(@"A___%@",rowsDict[@"title"]);
        }
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
 
    }];
}

request_B、request_C分别为请求第二页与第三页数据,这里不重复书写。为了显示更加明显,在请求中打印了对应新闻的标题内容。

运行打印如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
任务均完成,刷新界面
C___搞笑,不是认真的
C___摄影 | 街拍
C___生活小窍门
C___传统中国
C___想吃的美食系列
B___时间的见证者
B___没事 来吐槽吧……
B___触动心灵的摄影
B___摄影 | 黑白印记
B___每日插画推荐
A___左爱情,右面包
A___潮我看
A___世界各国的人们怎么过情人节
A___一点创意点亮生活
A___摄影 | 随手拍

运行后马上接收到了线程组完成的提示,之后数据才依次请求下来,很明显三个单纯的AFNet请求已经不能满足我们的需求了。线程组完成时并没有在我们希望的时候给予通知。在真实开发中会造成的问题为多个请求均加载完成,但界面已在未得到数据前提前刷新导致界面空白。

因此对于这种问题需要另辟蹊径,这里我们就要借助GCD中的信号量dispatch_semaphore进行实现,即营造线程同步情况。

dispatch_semaphore信号量为基于计数器的一种多线程同步机制。用于解决在多个线程访问共有资源时候,会因为多线程的特性而引发数据出错的问题。

如果semaphore计数大于等于1,计数-1,返回,程序继续运行。如果计数为0,则等待。

dispatch_semaphore_signal(semaphore)为计数+1操作。dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER)为设置等待时间,这里设置的等待时间是一直等待。我们可以通俗的理解为单柜台排队点餐,计数默认为0,每当有顾客点餐,计数+1,点餐结束-1归零继续等待下一位顾客。比较类似于NSLock。

我们将网络请求通用方法进行修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- (void)request_A {
    //创建信号量并设置计数默认为0
    dispatch_semaphore_t sema = dispatch_semaphore_create(0);
 
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];;
    manager.responseSerializer = [AFHTTPResponseSerializer serializer];
    NSDictionary *parameter = @{@"token":@"63104AB32427EBF89B957BBD1A5C5C11",
                                 @"page":@"1",
                                 @"upTime":@"desc"};
 
    [manager POST:URL parameters:parameter progress:^(NSProgress * _Nonnull uploadProgress) {
 
    } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        //计数+1操作
        dispatch_semaphore_signal(sema);
        NSDictionary * dict = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONReadingMutableContainers error:nil];
        for (NSDictionary *rowsDict in dict[@"rows"]) {
            NSLog(@"A___%@",rowsDict[@"title"]);
        }
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        ////计数+1操作
        dispatch_semaphore_signal(sema);
    }];
    //若计数为0则一直等待
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}

为方便阅读,伪代码如下:

1
2
3
4
5
6
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[网络请求:{
        成功:dispatch_semaphore_signal(sema);
        失败:dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);

这时我们再运行程序,打印如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
C___聆听的耳朵
C___家居 | 你的家里还缺点什么?
C___logo设计
C___时装
C___搞笑,不是认真的
A___左爱情,右面包
A___潮我看
A___世界各国的人们怎么过情人节
A___一点创意点亮生活
A___摄影 | 随手拍
B___时间的见证者
B___没事 来吐槽吧……
B___触动心灵的摄影
B___摄影 | 黑白印记
B___每日插画推荐
任务均完成,刷新界面

运行打印可见,通过信号量dispatch_semaphore完美的解决了此问题,并且网络请求仍为异步,不会堵塞当前主线程。

二、某界面存在多个请求,希望请求依次执行。

对于这个问题通常会通过线程依赖进行解决,因使用GCD设置线程依赖比较繁琐,这里通过NSOperationQueue进行实现,这里采用比较经典的例子,三个任务分别为下载图片,打水印和上传图片,三个任务需异步执行但需要顺序性。代码如下,下载图片、打水印、上传图片仍模拟为分别请求新闻列表3页数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    //1.任务一:下载图片
    NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
        [self request_A];
    }];
 
    //2.任务二:打水印
    NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
        [self request_B];
    }];
 
    //3.任务三:上传图片
    NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
        [self request_C];
    }];
 
    //4.设置依赖
    [operation2 addDependency:operation1];      //任务二依赖任务一
    [operation3 addDependency:operation2];      //任务三依赖任务二
 
    //5.创建队列并加入任务
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperations:@[operation3, operation2, operation1] waitUntilFinished:NO];

首先我们使用未添加信号量dispatch_semaphore时运行,打印如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
B___时间的见证者
B___没事 来吐槽吧……
B___触动心灵的摄影
B___摄影 | 黑白印记
B___每日插画推荐
A___潮我看
A___左爱情,右面包
A___世界各国的人们怎么过情人节
A___一点创意点亮生活
A___摄影 | 随手拍
C___盘
C___聆听的耳朵
C___家居 | 你的家里还缺点什么?
C___logo设计
C___时装

根据打印结果可见,若不对请求方法做处理,其运行结果并不是我们想要的,联系实际需求,A、B、C请求分别对应下载图片、打水印、上传图片,而此时运行顺序则为B->A->C,在未获得图片时即执行打水印操作明显是错误的。重复运行亦会出现不同结果,即请求不做处理,其结果不可控无法预测。线程依赖设置并未起到作用。

解决此问题的方法仍可通过信号量dispatch_semaphore进行解决。我们将请求方法替换为添加dispatch_semaphore限制的形式。即

1
2
3
4
5
6
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[网络请求:{
        成功:dispatch_semaphore_signal(sema);
        失败:dispatch_semaphore_signal(sema);
}]
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);

再次重复运行,我们会发现每次运行结果均一致,A、B、C三任务异步顺序执行(A->B->C)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
A___潮我看
A___左爱情,右面包
A___世界各国的人们怎么过情人节
A___一点创意点亮生活
A___摄影 | 随手拍
B___时间的见证者
B___没事 来吐槽吧……
B___触动心灵的摄影
B___摄影 | 黑白印记
B___每日插画推荐
C___盘
C___聆听的耳朵
C___家居 | 你的家里还缺点什么?
C___logo设计
C___时装

通过重复运行打印结果可证实确实实现了我们想要的效果。这样即解决了所提出的问题二。

后续

在开发中我们会遇到很多需要进行线程同步或请求同步的情况。比如弹出视图设置某功能,在点击确认按钮时发生请求,在请求成功同时销毁弹出视图等,均需要保证在请求真正完成时进行下一步操作。因此把握网络请求完成的正确时机还是很有必要的。

iOS开发——高级篇——线程同步、线程依赖、线程组的更多相关文章

  1. iOS开发Swift篇(02) NSThread线程相关简单说明

    iOS开发Swift篇(02) NSThread线程相关简单说明 一 说明 1)关于多线程部分的理论知识和OC实现,在之前的博文中已经写明,所以这里不再说明. 2)该文仅仅简单讲解NSThread在s ...

  2. iOS开发——高级篇——多线程的安全隐患

    资源共享 1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源 比如多个线程访问同一个对象.同一个变量.同一个文件 当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题   一.解 ...

  3. iOS开发——高级篇——多线程GCD死锁

    面试题 请问以下代码打印结果: - (void)interview01 { // 以下代码是在主线程执行的 NSLog(@"执行任务1"); dispatch_queue_t qu ...

  4. iOS开发——高级篇——iOS开发之网络安全密码学

    一.非对称加密 - RSA : + 公钥加密,私钥解密: + 私钥加密,公钥解密: + 只能通过因式分解来破解 二.对称加密 - DES - 3DES - AES (高级密码标准,美国国家安全局使用, ...

  5. iOS开发——高级篇——iPad开发、iPad开发中的modal

    一.iPad简介 1.什么是iPad一款苹果公司于2010年发布的平板电脑定位介于苹果的智能手机iPhone和笔记本电脑产品之间跟iPhone一样,搭载的是iOS操作系统 2.iPhone和iPadi ...

  6. iOS开发——高级篇——地理定位 CoreLocation

    一.CoreLocation 在移动互联网时代,移动app能解决用户的很多生活琐事,比如周边:找餐馆.找KTV.找电影院等等导航:根据用户设定的起点和终点,进行路线规划,并指引用户如何到达 在上述应用 ...

  7. iOS开发——高级篇——地图 MapKit

    一.简介 1.在移动互联网时代,移动app能解决用户的很多生活琐事,比如周边:找餐馆.找KTV.找电影院等等导航:根据用户设定的起点和终点,进行路线规划,并指引用户如何到达 在上述应用中,都用到了定位 ...

  8. iOS开发——高级篇——iOS 项目的目录结构

    最近闲来无事去面试一下iOS开发,让我感到吃惊的,面试官竟然问怎么分目录结构,还具体问每个子目录的文件名. 目录结构确实非常重要,面试官这么问,无疑是想窥探开发经验.清晰的目录结构,可让人一眼明白相应 ...

  9. iOS开发——高级篇——线程保活

    线程保活: 顾名思义,就是保护线程不死(保证线程处于激活状态,生命周期没有结束) 正常情况,当线程执行完一次任务之后,需要进行资源回收,也就意味着生命周期结束 应用场景: 当有一个任务,随时都有可能去 ...

随机推荐

  1. 并发-5CAS与AQS

    juc: java.util.concurrent 锁: 悲观锁:写的比较多,对数据的增删改,读(查)少.Lock 乐观锁:反之,读多写少.版本 并发编程之 CAS 的原理 什么是CAS CAS (c ...

  2. No-2.注释

    01. 注释的作用 使用用自己熟悉的语言,在程序中对某些代码进行标注说明,增强程序的可读性 02. 单行注释(行注释) 以 # 开头,# 右边的所有东西都被当做说明文字,而不是真正要执行的程序,只起到 ...

  3. mysql cluster配置

    依赖包要求:cmake     gcc    gcc-c++     ncurses     Perl     ncurses-devel 在7.3以及更高的版本中, WITH_NDB_JAVA是默认 ...

  4. [Python3网络爬虫开发实战] 6-Ajax数据爬取

    有时候我们在用requests抓取页面的时候,得到的结果可能和在浏览器中看到的不一样:在浏览器中可以看到正常显示的页面数据,但是使用requests得到的结果并没有.这是因为requests获取的都是 ...

  5. PHP典型功能与Laravel5框架开发学习笔记

    步骤一:PHP的Redis应用及HTTP协议 一.Redis初识 1.Linux下安装redis:具体看官网:https://redis.io/download:以下为以个人习惯的安装目录进行的red ...

  6. ubuntu14.04 mysql-workbench Connecting to MySQL server ... Native table 'performance_schema'.'session_variables' has the wrong structure错误解决

    使用的mysql版本: mysql  Ver 14.14 Distrib 5.7.9, for Linux (x86_64) using  EditLine wrapper 打开shell命令 1.输 ...

  7. 安装nvm 切换nodejs版本

    删除已安装的nodejs--------------------------------------------------------------- #查看已经安装在全局的模块,以便删除这些全局模块 ...

  8. python 开发搭建目录规范

    软件目录结构规范有什么好处: 通过规范化,能够更好的控制软件结构,让程序具有更高的可读性. 项目目录组织结构如下: Foo/ # 项目名 --bin/ # 可执行文件目录 --foo # 可执行程序 ...

  9. 又一个ajax实例,结合jQuery

    又一个ajax实例,配合jQuery   html <!DOCTYPE html> <html lang="zh-cn"> <head> < ...

  10. iar修改包含路径的方法