美文网首页
15 - GCD、NSOperation

15 - GCD、NSOperation

作者: RadioWaves | 来源:发表于2017-07-02 12:41 被阅读23次
    • 全称是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;
    }
    

    相关文章

      网友评论

          本文标题:15 - GCD、NSOperation

          本文链接:https://www.haomeiwen.com/subject/oqvncxtx.html