15 - GCD、NSOperation

  • 全称是Grand Central Dispatch,可译为“牛逼的中枢调度器”
  • 纯C语言,提供了非常多强大的函数

GCD的优势 :

  1. GCD是苹果公司为多核的并行运算提出的解决方案
  2. GCD会自动利用更多的CPU内核(比如双核、四核)
  3. GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
  4. 程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码

GCD的使用就两个步骤 :

  1. 定制任务(确定想做的事情)
  2. 将任务添加到队列中GCD会自动将队列中的任务取出,放到对应的线程中执行,任务的取出遵循队列的FIFO(First in First out)原则:先进先出,后进后出

GCD有两个核心概念 :

概念一 : 任务 (执行什么操作)

GCD中有两个用来执行任务的函数

// 用同步的方式执行任务 :只能在当前线程中执行任务,不具备开启新线程的能力

//  sync: 同步                 queue:队列           block:任务    
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);

// 用异步的方式执行任务 : 可以在新的线程中执行任务,具备开启新线程的能力

//  async: 异步                 queue:队列           block:任务   
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

概念二 : 队列 :(用来存放任务)

GCD中的队列可以分为两大类型
1: 并发队列(Concurrent Dispatch Queue):
  • 可以让多个任务并发同时执行(自动开启多个线程同时执行任务)
  • 并发功能只有在异步(dispatch_async)函数下才有效
2: 串行队列(Serial Dispatch Queue)
  • 让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)

同步、异步、并发、串行的作用:

  • 同步和异步主要影响:能不能开启新的线程
  • 同步:在当前线程中执行任务,不具备开启新线程的能力
  • 异步:在新的线程中执行任务,具备开启新线程的能力
  • 并发和串行主要影响:任务的执行方式
  • 并发:多个任务并发(同时)执行
  • 串行:一个任务执行完毕后,再执行下一个任务

并发队列与串行队列

并发队列

GCD的默认已经提供了全局的并发队列,供整个应用使用,不需要手动创建.

  1. 获得全局并发队列
// 用这个获得全局并发队列{dispatch_get_global_queue}
// 用这个获得优先级{DISPATCH_QUEUE_PRIORITY_DEFAULT}   
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
  1. 并发队列全局并发队列的优先级
define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高
define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默认(中)
define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低
define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后台

并发队列 + 同步函数 : 不会开启线程

Snip20150902_4.png

并发队列 + 异步函数 : 会开启线程

Snip20150902_7.png

串行队列

GCD中获得串行有2种途径

  • 1 - 使用dispatch_queue_create函数创建串行队列
dispatch_queue_t queue = dispatch_queue_create("c.w.z", NULL); 

串行队列 + 异步函数 : 会开启线程

Snip20150902_10.png

串行队列 + 同步函数 : 不会开启线程

Snip20150902_12.png

  • 2 - 使用主队列(跟主线程相关联的队列)
    主队列是GCD自带的一种特殊的串行队列
    只要是放在主队列中的任务,不管你是同步还是异步函数全都放在主线程中执行
dispatch_queue_t queue = dispatch_get_main_queue();

主要是主队列:不会开启线程

Snip20150902_14.png

各种队列的执行效果

Snip20150831_56.png

线程间的通讯

从子线程回到主线程

执行耗时的异步操作...

dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
         // 回到主线程,执行UI刷新操作
       dispatch_async(dispatch_get_main_queue(), ^{
    });
});

下载图片的操作

Snip20150902_15.png

延时执行

iOS常见的延时执行有2种方式

// 调用NSObject的方法
// 2秒后再调用self的run方法
self performSelector:@selector(run) withObject:nil afterDelay:2.0;

// 使用GCD函数
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)),`                                  
    dispatch_get_main_queue(), ^{
    // 2秒后异步执行这里的代码...       
});

// 使用NSTimer
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:NO];

只执行一次的代码

// 运行时只执行一次
// 使用dispatch_once函数能保证某段代码在程序运行过程中只被执行1次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    // 只执行1次的代码(这里面默认是线程安全的)
});

队列组

Warning:有这么1种需求:
首先:分别异步执行2个耗时的操作
其次:等2个异步操作都执行完毕后,再回到主线程执行操作
如果想要快速高效地实现上述需求,可以考虑用队列组

dispatch_group_t group =  dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)^{
    // 执行1个耗时的异步操作
});

dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 执行1个耗时的异步操作
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    // 等前面的异步操作都执行完毕后,回到主线程...
});

代码详细实现:

需求:在子线程中下载2张图片.分别显示在屏幕的左右.
分析:由于下载图片属于耗时操作,所以要在子线程中操作.等2张图片都下载好后,在回到主线程加载.

// 点击屏幕后调用
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [self group];
}

// 队列组
- (void)group
{
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    // 创建一个队列组
    dispatch_group_t group = dispatch_group_create();
    
    // 1.下载图片1
    dispatch_group_async(group, queue, ^{
        // 图片的网络路径
        NSURL *url = [NSURL URLWithString:@"http://img.pconline.com.cn/images/photoblog/9/9/8/1/9981681/200910/11/1255259355826.jpg"];
        
        // 加载图片
        NSData *data = [NSData dataWithContentsOfURL:url];
        
        // 生成图片
        self.image1 = [UIImage imageWithData:data];
    });
    
    // 2.下载图片2
    dispatch_group_async(group, queue, ^{
        // 图片的网络路径
        NSURL *url = [NSURL URLWithString:@"http://pic38.nipic.com/20140228/5571398_215900721128_2.jpg"];
        
        // 加载图片
        NSData *data = [NSData dataWithContentsOfURL:url];
        
        // 生成图片
        self.image2 = [UIImage imageWithData:data];
    });
    
    // 3.将图片1、图片2合成一张新的图片
    dispatch_group_notify(group, queue, ^{
        // 开启新的图形上下文
        UIGraphicsBeginImageContext(CGSizeMake(100, 100));
        
        // 绘制图片
        [self.image1 drawInRect:CGRectMake(0, 0, 50, 100)];
        [self.image2 drawInRect:CGRectMake(50, 0, 50, 100)];
        
        // 取得上下文中的图片
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        
        // 结束上下文
        UIGraphicsEndImageContext();
        
        // 回到主线程显示图片
        dispatch_async(dispatch_get_main_queue(), ^{
            // 4.将新图片显示出来 
            self.imageView.image = image;
        });
    });
}

显示结果

barrier

GCD中还有个用来执行任务的函数barrier(栅栏/障碍物)

dispatch_barrier_async(dispatch_queue_t queue>, ^(void)block)
  • 在前面的任务执行结束后它才执行,而且它后面的的任务等它执行完之后才会执行
  • 这个queue不能是全局的并发队列
Paste_Image.png

他会在执行完 barrier上面的1和2后在执行barrier后面的3和4


NSOperation:

是在GCD的基础上进行的一层面向对象的包装.

核心概念:

任务和队列.和GCD基本上是一样的,只不过更加的面向对象.用起来比较爽.

NSOperation的作用

  • 配合使用NSOperation和NSOperationQueue也能实现多线程编程
  • NSOperation:就是任务
  • NSOperationQueue:就是队列

NSOperation的子类

  • NSOperation是个抽象类,并不具备封装操作的能力,必须使用它的子类

使用NSOperation子类的方式有3种:

  1. NSInvocationOperation,
  2. NSBlockOperation,
  3. 自定义子类继承NSOperation,实现内部相应的方法;

NSOperation和NSOperationQueue实现多线程的具体步骤:

  • 先将需要执行的操作封装到一个NSOperation对象中,
  • 然后将NSOperation对象添加到NSOperationQueue中,
  • 系统会自动将NSOperationQueue中的NSOperation取出来,
  • 将取出的NSOperation封装的操作放到一条新线程中执行;

1 - NSInvocationOperation(比较少用)

// 1.创建NSInvocationOperation对象
- (id)initWithTarget:(id)target selector:(SEL)sel object:
- (id)arg;

// 2. 调用start方法开始执行操作
    - (void)start;
一旦执行操作,就会调用target的sel方法
  1. 默认情况下,调用了start方法后并不会开一条新线程去执行操作,而是在当前线程同步执行操作
  2. 只有将NSOperation放到一个NSOperationQueue中,才会异步执行操作


    Snip20150920_62.png

2 - NSBlockOperation

// 1. 创建NSBlockOperation对象
+ (id)blockOperationWithBlock:(void (^)(void))block;

// 2. 通过addExecutionBlock:方法添加更多的操作
- (void)addExecutionBlock:(void (^)(void))block;
  • 只要NSBlockOperation封装的操作数 > 1,就会异步执行操作


    Snip20150920_63.png

3 -自定义NSOperation

  • 首先,在控制器中,将操作添加到队列中
  • 然后会调用自定义类的- main方法,执行- main方法中的任务
    1.首先新建一个类,继承自NSOperation
    2.在.m文件中重写- main方法 (不是main函数而是- main方法)
    3.将任务写入- main方法中

NSOperationQueue

  • NSOperationQueue的作用
  • NSOperation可以调用start方法来执行任务,但默认是同步执行
  • 如果将NSOperation添加到NSOperationQueue(操作队列)中,系统会自动异步执行NSOperation中的操作
// 添加操作到NSOperationQueue中
- (void)addOperation:(NSOperation *)op;
- (void)addOperationWithBlock:(void (^)(void))block;

NSOperationQueue的队列类型

主队列:
凡是放到了主队列的任务(NSOperation),都会放到主线程中执行
[NSOperationQueue mainQueue];

其他队列(串行/并发):
同时包含:串行 / 并发功能
添加这种队列中的任务,就会自动放到子线程中执行
[NSOperationQueue alloc] init];

最大并发数

同时执行的任务数:比如同时开3个线程执行3个任务,并发数就是3

// 最大并发数的相关方法
-  (NSInteger)maxConcurrentOperationCount;  
- (void)setMaxConcurrentOperationCount:(NSInteger)cnt;

队列的取消、暂停、恢复

// 取消队列的所有操作
- (void)cancelAllOperations;    

__提示:
也可以调用NSOperation的- (void)cancel方法取消单个操作

// 暂停和恢复队列(YES代表暂停队列,NO代表恢复队列)
- (void)setSuspended:(BOOL)b; 
- (BOOL)isSuspended;

经常通过`- (BOOL)isCancelled`方法检测操作是否被取消,对取消做出响应

suspended 暂定/挂起

__warning:#cancel     
    官方建议:执行完一段耗时操作的后面最好加上是否点击了取消的判断.
    人为控制取消操作.以免,全部执行完,还没有取消掉.

操作优先级

设置NSOperation在queue中的优先级,可以改变操作的执行优先级
- (NSOperationQueuePriority)queuePriority;
- (void)setQueuePriority:(NSOperationQueuePriority)p;

##优先级的取值
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8

操作依赖

  • NSOperation之间可以设置依赖来保证执行顺序

比如一定要让操作A执行完后,才能执行操作B,可以这么写

// 操作B依赖于操作A
operationB addDependency:operationA;
  • 可以在不同queue的NSOperation之间创建依赖关系


    Snip20150831_57.png

操作的监听

// 可以监听一个操作的执行完毕
- (void (^)(void))completionBlock;
- (void)setCompletionBlock:(void (^)(void))block;

自定义NSOperation的步骤很简单

重写`-(void)main`方法,在里面实现想执行的任

重写- (void)main方法的注意点:

  1. 自己创建自动释放池(因为如果是异步操作,无法访问主线程的自动释放池)
  2. 经常通过-(BOOL)isCancelled方法检测操作是否被取消,对取消做出响应

GCD和NSOperation的区别

  • GCD是基于C的底层API
  • NSOperation属于object-c类。
  • iOS首先引入的是NSOperation,IOS4之后引入了GCD和NSOperationQueue并且其内部是用GCD实现的。

NSOperation:

1 - NSOperation拥有更多的函数可用,具体查看API。
2 - 在NSOperationQueue中,可以建立各个NSOperation之间的依赖关系。
3 - 有KVO,可以监测operation是否
--------------------------正在执行isExecuted
--------------------------是否结束isFinished
--------------------------是否取消isCanceld;
4 - NSOperationQueue可以方便的管理并发、NSOperation之间的优先级。

NSOperationQueue的队列类型

  • 主队列
  • [NSOperationQueue mainQueue]
  • 凡是添加到主队列中的任务(NSOperation),都会放到主线程中执行
  • 非主队列(其他队列)
  • [[NSOperationQueue alloc] init]
  • 同时包含了:串行、并发功能
  • 添加到这种队列中的任务(NSOperation),就会自动放到子线程中执行

GCD:

1 - 主要与block结合使用。代码简洁高效。
2 - GCD也可以实现复杂的多线程应用,主要是建立个个线程时间的依赖关系这类的情况,但是需要自己实现相比NSOperation要复杂。具体使用哪个,依需求而定。

GCD的队列类型

  • 并发队列
  • 全局
  • 自己创建的
  • 串行队列
  • 主队列
  • 自己创建的

从个人使用的感觉来看,比较合适的用法是:除了依赖关系尽量使用GCD,因为苹果专门为GCD做了性能上面的优化。


GCD实现单例模式

单例模式 :

其实就是一种设计模式

适用场合

单例模式一般会在整个应用程序中,共享一份资源(这份资源只需要创建初始化1次)的时候使用.

比如:

  • 一个APP中有一个界面是提供公司的信息的,或者创建账号的界面
  • 那么,无论是点击添加好友或者登录,都会跳转到(访问)创建账号这个界面
  • 那么就可以把这个界面写成单例模式,保证属性以及各种信息都一样
  • 这样不仅方便地控制了实例个数,并节约系统资源
比如:
Person *p1 = [Person alloc] init];
Person *p2 = [Person alloc] init];
Person *p3 = [Person alloc] init];
Person *p4 = [Person alloc] init];
Snip20150920_33.png

从上图我们可以看到:

  • 打印出来的p1,p2,p3,p4的内存地址是不一样的
  • 因为每次调用alloc会分配新的内存空间.
  • 但是如果实现了单例模式,就能可以保证在程序运行过程中,
  • 无论alloc了多少次,Person这个类都是同一个对象.

那么我们怎么样来实现单例模式呢?

第一种方法:重写+ allocWithZone

  • 之所以我们要实现单例模式,是希望无论alloc了多少次,都只产生一份内存
  • 那么我们就要从alloc这里下手
  • 最容易想到的办法就是在内部直接重写alloc
  • 而且alloc内部会调用+ allocWithZone这个方法
  • 无论别人在外界调用alloc还是调用+ allocWithZone都会来到+ allocWithZone这个方法
  • 所以要重写+ allocWithZone
    Snip20150920_41.png
Snip20150920_36.png
  • 上图可以看出我们打印出来的内存地址的结果都是一样的.

接下来我们来通过打印属性值来验证下:

Snip20150920_37.png

Snip20150920_39.png

Snip20150920_38.png

第二种方法:[UIApplication sharedApplication]

Snip20150920_42.png

Snip20150920_45.png

Snip20150920_46.png

第三种方法:copy

Snip20150920_48.png

那么,我们应该这样做

Snip20150920_49.png

Snip20150920_50.png
  • 看到这里可能有一个疑问,这里直接返回_Person的话值不会为空么?
  • 答案是肯定不会为空的.
  • 因为调用copy方法,肯定是有对象在调用的
  • 所以对象都有值了,那么这个方法里面肯定也是回有值的

将以上的结合起来才属于一套完整的单例模式

Snip20150920_52.png

那么以后一个工程中可能会有很多单例,总不能一个一个改吧?

那么我们可以将单例弄成宏的形式,以后遇到了直接拖入工程就可以了


Snip20150920_58.png

那么 我们来看下效果:

.h文件

Snip20150920_59.png

.m文件

Snip20150920_60.png

实现的效果

Snip20150920_61.png

单例模式在ARC\MRC环境下的写法有所不同

需要编写2套不同的代码

可以用宏判断是否为ARC环境

#if __has_feature(objc_arc)
// ARC
#else
// MRC
#endif

ARC中,单例模式的实现

1. 在.m中保留一个全局的`static`的实例

static id _instance;

// 重写allocWithZone:方法,在这里创建唯一的实例(注意线程安全)  
+(id)allocWithZone:(struct _NSZone *)zone                      
{
    @synchronized(self) {
        if (!_instance) {                                
        _instance = [super allocWithZone:zone];          
        }
    }   
    return _instance;                                       
}

2. 提供1个类方法让外界访问唯一的实例
        
+ (instancetype)sharedSoundTool{
    @synchronized(self) {
        if (!_instance) {                                
        _instance = [[self alloc] init];                 
        }
    }
    return _instance;                                       
}

3. 实现copyWithZone:方法
- (id)copyWithZone:(struct _NSZone *)zone 
        {
            return _instance;
        }

MRC中,单例模式的实现(比ARC多了几个步骤)

// 实现内存管理方法
- (id)retain { 
     return self; 
}

- (NSUInteger)retainCount{ 
 return 1; 
}

- (oneway void)release {
}

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

推荐阅读更多精彩内容