多线程(一)之 NSThread、GCD

---恢复内容开始---

pthread小结(了解)

  1. 在 C 语言中,没有对象的概念,对象是以结构体的方式来实现的
  2. 通常,在 C 语言框架中,对象类型以 _t/Ref 结尾,而且声明时不需要使用 *
  3. C 语言中的 void * 和 OC 中的 id 是等价的
  4. 内存管理
    • 在 OC 中,如果是 ARC 开发,编译器会在编译时,根据代码结构,自动添加 retain/release/autorelease
    • 但是,ARC 只负责管理 OC 部分的内存管理,而不负责 C 语言 代码的内存管理
    • 因此,开发过程中,如果使用的 C 语言框架出现 retain/create/copy/new 等字样的函数,大多都需要 release,否则会出现内存泄漏
  5. 在混合开发时,如果在 C 和 OC 之间传递数据,需要使用 __bridge 进行桥接,桥接的目的就是为了告诉编译器如何管理内存
  6. 桥接的添加可以借助 Xcode 的辅助功能添加
  7. MRC 中不需要使用桥接

NSThread小结:(了解)

 

GCD :(掌握)

异步执行任务:

- (void)gcdDemo1 {
    // 1. 全局队列
    dispatch_queue_t q = dispatch_get_global_queue(0, 0);

    // 2. 任务
    void (^task)() = ^ {
        NSLog(@"%@", [NSThread currentThread]);
    };

    // 3. 指定执行任务的函数
    // 异步执行任务 - 新建线程,在新线程执行 task
    dispatch_async(q, task);

    NSLog(@"come here");
}

异步执行任务(精简代码):

- (void)gcdDemo2 {
    for (int i = 0; i < 10; ++i) {
//下面这句代码的意思就是:把任务添加到全局队列异步执行 dispatch_async(dispatch_get_global_queue(
0, 0), ^{ NSLog(@"%@ %@", [NSThread currentThread], @"hello"); }); } }

同步异步:执行任务的函数

队列:负责调度任务

笔记中说的贼清楚了

串行队列同步执行:不开线程

串行队列异步执行:开线程

开不开线程决定权在于:同步还是异步 异步就开线程,不是异步就不开线程

//串行,异步
//开线程,一个线程,保证任务任然有序
//串行,同步
//在当前线程
//有序执行
//并发, 同步
//不开线程,任务有序==串行,同步
//并发, 异步
//开很多线程,线程复用,优化的措施之一
//无序
//在老版本Xcode GCD线程不会很多,但是在新的Xcode中,极限情况下,60+线程,线程数量太多,苹果推荐各位使用NSOperation
//执行效率最大

死锁:只有在主队列同步才会发生,建议用主队列异步,主队列异步是在主线程执行任务

进程是正在运行的程序,线程是基本的执行单元,队列呢是用来调度任务的,线程是执行队列中的任务,执行任务函数包括同步和异步,用来决定是否开线程

//特殊:
//1.一定在主线程
//2.在主线程不忙的时候,队列才会调度任务去主线程执行
//死锁:谁也执行不了,在主线程上主队列,同步
//推荐大家使用主队列,异步执行

全局并发队列就当并发队列去用,一般不是特殊情况,没有必要自己去创建一个并发队列.

 队列组:

要知道是干嘛用的:

队列组就是用来处理下载多个任务时,所有异步任务下载完后再通知队列组说,我下完了,可以安装了,例如,我需要下载一个游戏,这个游戏很大,需要很多压缩包,并且所有压缩包下载完毕后才去执行安装,所以这个时候是需要队列组来处理.

//
//  ViewController.m
//  队列组的小练习
//
//  Created by 曹魏 on 2016/12/8.
//  Copyright © 2016年 itcast. All rights reserved.
//

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self demo2];
}


- (void)demo2{
    /*
     查看更多的信息 man dispatch_group_async 终端
     dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block)
     {
     dispatch_retain(group);
     
     dispatch_group_enter(group);
     dispatch_async(queue, ^{
     block();
     dispatch_group_leave(group);
     
     dispatch_release(group);
     });
     }
     */
    //1.创建一个队列组
    dispatch_group_t group = dispatch_group_create();
    //2.添加下载任务到队列组
    dispatch_group_enter(group);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //模拟延时
        [NSThread sleepForTimeInterval:arc4random_uniform(5)];
        
        NSLog(@"LOL1.zip %@",[NSThread currentThread]);
        
        dispatch_group_leave(group);
    });
//    dispatch_group_async(group,dispatch_get_global_queue(0, 0), ^{
//        [NSThread sleepForTimeInterval:arc4random_uniform(5)];
//        NSLog(@"LOL2.zip %@",[NSThread currentThread]);
//        
//    });
    dispatch_group_enter(group);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //模拟延时
        [NSThread sleepForTimeInterval:arc4random_uniform(5)];
        
        NSLog(@"LOL2.zip %@",[NSThread currentThread]);
        
        dispatch_group_leave(group);
    });
    
    
    //3.异步任务执行完毕后通知队列组下载完成,可以弹出个提示框,提示安装之类 的
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        
        NSLog(@"下载完成,可以安装  %@",[NSThread currentThread]);
    });
    
    
    
}


//队列组,有时候我们下载游戏或者其他大型的东西时,不可能只有一个压缩包,因为只有一个压缩包很大,下载速度超级慢,
//所以会分好几个压缩包,异步下载,全部压缩包下载完后才能解压,这个时候就用到了队列组的概念

- (void)demo1{
    //1.创建一个队列组
    dispatch_group_t group = dispatch_group_create();
    //2.添加下载任务到队列组
    dispatch_group_async(group,dispatch_get_global_queue(0, 0), ^{
        //模拟延时
        [NSThread sleepForTimeInterval:arc4random_uniform(5)];
        
        NSLog(@"LOL1.zip %@",[NSThread currentThread]);
        
    });
    dispatch_group_async(group,dispatch_get_global_queue(0, 0), ^{
        [NSThread sleepForTimeInterval:arc4random_uniform(5)];
        NSLog(@"LOL2.zip %@",[NSThread currentThread]);
        
    });
    
    
    //3.异步任务执行完毕后通知队列组下载完成,可以弹出个提示框,提示安装之类 的
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        
        NSLog(@"下载完成,可以安装  %@",[NSThread currentThread]);
    });
    
    
    
    
}


@end

 

如何实现一次性执行? 

什么叫一次性执行?就是多线程同时执行一个方法,怎么保证只执行一次:dispatch_once_t

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self demo1];
    
//    for (int i = 0; i<20; i++) {
//        dispatch_async(dispatch_get_global_queue(0, 0), ^{
//            [self demo1];
//        });
//    }
}

- (void)demo1 {
    
    //问题:在多线程的环境下 不能保证盼有一定有用,因为线程是切换运行的
//    if(isinit)
//        return;
//    NSLog(@"demo1");
//    isinit = YES;
    
    NSLog(@"begin");
    /**
     *  线程安全的一次性执行
     当onceToken为0 的时候,表示没有执行过,执行过之后,非0
     同步执行
     */
    static dispatch_once_t onceToken;
    NSLog(@"%ld",onceToken);
    dispatch_once(&onceToken, ^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"demo1---------");
    });
    
    NSLog(@"over");
}

单例:dispatch_once_t

如果面试官问到:你们单例怎么写的?你了解单例吗?

答:首先回答什么是单例?单例就是指在内存中只有一个实例对象,那么为什么要用到单例呢?因为在程序的开发过程中,有些代码我只想让它执行一次,这里就用到了单例.

单例一般我们都是用dispatch_once_t,为什么能做到只访问一次,这是因为dispatch中有一把锁,这把锁的名称是互斥锁,

 

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc]init];
    });
    
    return instance;

 

当程序第一次进去的时候,long型的onceToken是为0,这是说明程序还没有执行,那么执行一次后,onceToken的值变成非0 ,互斥锁打开,阻止后面的程序进入,达到只执行一次的效果.

 

如果我想无论怎么实例化该单例类下的对象,都保证这个对象是个单例对象,怎么操作呢?即我想让下面代码中的person对象成为一个单例对象,怎么操作呢?

 

//
//  ViewController.m
//  单例
//
//  Created by apple on 16/8/20.
//  Copyright © 2016年 itcast. All rights reserved.
//

#import "ViewController.h"
#import "HMPerson.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    HMPerson *person = [[HMPerson alloc] init];//这个方法去实例化对象,即alloc方法,会调用系统的allocwithzone方法,所以我要想保证代码只执行一次就要在这个方法里面加一把互斥锁,如下个代码所示.
    HMPerson *person2 = [HMPerson sharedPerson];//这个方法道理也一样,在sharedPerson方法中加一把互斥锁,同样达到只执行一次的效果
    HMPerson *person3 = person2.copy;//这个方法的前提是必须让person遵循NSCoping协议,协议里有个方法叫copyWithZone方法,person2.copy,由于是浅拷贝,所以就是person3指针指向了person2的地址,所以返回的还是person2对象,下面补充一下关于copy的知识.
    NSLog(@"%@,%@",person,person3);
    NSLog(@"sss");
    
}


@end

 

static id instance;

+(instancetype)sharedPerson{
//开发的时候,推荐这种写法,没有必要重写allocwithzone来禁用alloc方法等,提供更灵活创建对象的方式
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc]init];
    });
    
    return instance;
}

+(instancetype)allocWithZone:(struct _NSZone *)zone{
//保证内存分配一次
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [super allocWithZone:zone];
    });
    
    return instance;
}

-(id)copyWithZone:(NSZone *)zone{
    return instance;
}

copy的深层次理解:

首先,什么是copy?

    Copy的字面意思是“复制”、“拷贝”,是一个产生副本的过程。

    常见的复制有:文件复制,作用是利用一个源文件产生一个副本文件。

    特点:1、修改源文件的内容,不会影响副本文件;

             2、修改副本文件的内容,不会影响源文件。

    OC中copy的作用是:利用一个源对象产生一个副本对象

    特点:1、修改源对象的属性和行为,不会影响副本对象;

             2、修改副本对象的属性和行为,不会影响源对象。

    如何使用copy功能?

    一个对象可以调用copy或mutableCopy方法来创建一个副本对象。

    1、copy:创建的时不可变副本(如NSString、NSArray、NSDictionary)。

    2、mutableCopy:创建的可变副本(如NSMutableString、NSMutableArray、NSMutableDictionary)。

    使用copy功能的前提:

    1、copy:需要遵守NSCopying协议,实现copyWithZone:方法。

    @protocol NSCopying

   - (id)copyWithZone:(NSZone *)zone;

      @end

      2、mutableCopy : 需要遵守NSMutableCopying协议,实现mutableCopyWithZone:方法

      @protocol NSMutableCopying

      - (id)mutableCopyWithZone:(NSZone *)zone;

      @end 

      深复制和浅复制的区别:

      深复制(深拷贝、内容拷贝、deep copy):

      特点:1、源对象和副本对象是不同的两个对象; 

               2、源对象引用计数器不变,副本对象计数器为1(因为是新产生的)。

      本质:产生了新对象。

      浅复制(浅拷贝、指针拷贝、shallow copy):

      特点:1、源对象和副本对象是同一对象; 

               2、源对象(副本对象)引用计数器+1,相当于做一次retain操作。

      本质:没有产生新对象。

      常见的复制如下图:

 只有源对象和副本对象都不可变时,才是浅复制,其他都是深复制。

      关于区分深复制与浅复制的一些详细代码如下:

/**
 NSMutableString调用mutableCopy : 深复制
 */
void mutableStringMutableCopy()
{
    NSMutableString *srcStr = [NSMutableString stringWithFormat:@"age is %d", 10];
    NSMutableString *copyStr = [srcStr mutableCopy];
     
    [copyStr appendString:@"abc"];
     
    NSLog(@"srcStr=%@, copyStr=%@", srcStr, copyStr);
}
/**
 NSMutableString调用copy : 深复制
 */
void mutableStringCopy()
{
    NSMutableString *srcStr = [NSMutableString stringWithFormat:@"age is %d", 10];
     
    NSString *copyStr = [srcStr copy];
     
     
    [srcStr appendString:@"abc"];
     
    NSLog(@"srcStr=%p, copyStr=%p", srcStr, copyStr);
}
/**
 NSString调用mutableCopy : 深复制
 */
void stringMutableCopy()
{
    NSString *srcStr = [NSString stringWithFormat:@"age is %d", 10];
     
    NSMutableString *copyStr =  [srcStr mutableCopy];
    [copyStr appendString:@"abc"];
     
    NSLog(@"srcStr=%@, copyStr=%@", srcStr, copyStr);
}
/**
 NSString调用copy : 浅复制
 */
void stringCopy()
{
    //  copy : 产生的肯定是不可变副本
     
    //  如果是不可变对象调用copy方法产出不可变副本,那么不会产生新的对象
    NSString *srcStr = [NSString stringWithFormat:@"age is %d", 10];
    NSString *copyStr = [srcStr copy];
     
    NSLog(@"%p %p", srcStr, copyStr);
}
     @property内存管理策略的选择
      1.非ARC
      1> copy : 只用于NSString\block;
      2> retain : 除NSString\block以外的OC对象;
   3> assign : 基本数据类型、枚举、结构体(非OC对象),当2个对象相互引用,一端用retain,一端                     用assign。
 
      2.ARC
      1> copy : 只用于NSString\block;
      2> strong : 除NSString\block以外的OC对象;
      3> weak : 当2个对象相互引用,一端用strong,一端用weak;
   4> assgin : 基本数据类型、枚举、结构体(非OC对象)。

 

   

 

 

 

 

消息循环:NSRunLoop

面试官肯定会问:你了解消息循环吗?或者你对NSRunLoop了解多少?

回答:了解,首先消息循环的目的是:

1.保证程序不退出,随时接收输入消息(或者说是输入源),为什么我要加入这个消息循环?或者说为什么我要让程序不退出?因为程序一旦运行结束就退出了,就不能接收消息了,举个例子,我们玩微信,有个事件是:我输入一句话,这个事件肯定在程序中执行,但是如果不用消息循环,程序执行完就退出了,即微信自动退出,那么再发消息难道要重新运行程序吗?所以说这就是消息循环存在的意义.

2.负责处理输入事件

3.如果没有事件发生,会让程序进入休眠状态

接下来说:消息循环处理的事件分为两大时间:1.input Sources(输入源事件)2.timer Sources(定时源事件)

输入源事件:I/O :I是Input O是Output      输入源一般是指:键盘,鼠标,NSPort,NSConnection等  输出源一般指:显示屏,音响,打印机  

定时源事件:就是指NSTimer事件

接下来说:一般情况下我们怎么使用消息循环?

使用消息循环大致分三步:1.创建事件 2.获得当前消息循环对象 3.设置消息循环模式

然后答:消息循环的模式分为两个:1.NSDefaultRunLoopMode--默认模式 2.NSRunLoopCommonModes--普通模式

主线程默认是开启消息循环的,子线程是默认不开启消息循环的

子线程必须手动开启消息循环,即

//开启
    [runloop run];

有三种开启消息循环的方法:

  第一种:直接run(一般情况下这一个就足够了,但缺点是开启了不能关闭,如果想用代码去关闭,可以采用第二种方法)    


  第二种:apple推荐的方式 BOOL shouldKeepRunning
= YES; // global NSRunLoop *theRL = [NSRunLoop currentRunLoop]; //运行一次 while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]); 第三种:第三种指定时间,不靠谱 [runloop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];

具体消息循环代码如下所示:

//
//  ViewController.m
//  消息循环
//
//  Created by apple on 16/8/20.
//  Copyright © 2016年 itcast. All rights reserved.
//

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

   
    
}

//主线程
-(void)demo1{
    /*
     1.时间间隔
     2.对象
     3.方法
     4.其他信息
     5.是否重复
     */
    NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(task) userInfo:nil repeats:YES];
    
    //2.得到当前线程的消息循环
    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    
    
    //3.把定时源加入到消息循环中
    /*
     1.定时源 timer
     2.模式
     */
    
    /*
     NSDefaultRunLoopMode 触摸界面->timer停止
     NSRunLoopCommonModes 触摸界面不影响timer的运行
     kCFRunLoopDefaultMode->UITrackingRunLoopMode->kCFRunLoopDefaultMode
     NSRunLoopCommonModes起码包含kCFRunLoopDefaultMode UITrackingRunLoopMode模式的一组集合
     */
    [runloop addTimer:timer forMode:NSRunLoopCommonModes];
}



- (void)task {
    //打印当前现成的消息循环
    //kCFRunLoopDefaultMode
    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    NSString *str = runloop.currentMode;
    NSLog(@"task is running %@",str);
}

#pragma mark 子线程

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self demo2];
}


-(void)demo2{
//子线程
    //结论:主线程里面的消息循环是开启的,子线程默认不开启
    NSThread *thread  = [[NSThread alloc] initWithTarget:self selector:@selector(mytask) object:nil];
    [thread start];
    //往子线程的消息循环中添加事件
    /*
     1.方法
     2.线程
     3.参数
     4.waitUntilDone 是否等待
     把一个输入员添加到指定线程的消息循环中
     */
    [self performSelector:@selector(test2) onThread:thread withObject:nil waitUntilDone:NO];
}

-(void)mytask{
    NSLog(@"---->%@",[NSThread currentThread]);
    //开启子线程的消息循环
    
    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    
    //开启
    [runloop run];
    
    
//    apple推荐的方式
//     BOOL shouldKeepRunning = YES;        // global
//     NSRunLoop *theRL = [NSRunLoop currentRunLoop];
//    //运行一次
//     while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
//    
//
//    第三种指定时间,不靠谱
//    [runloop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
    
    NSLog(@"over");
}

-(void)test2{
    NSLog(@"???? test2");
}
     


@end

注意:如果子线程没有手动打开消息循环,打印如下:

2016-12-09 23:33:37.013 消息循环[4824:501434] ----><NSThread: 0x608000260740>{number = 3, name = (null)}
2016-12-09 23:33:37.014 消息循环[4824:501434] over

即不会执行test2方法,是因为,子线程默认不开启消息循环,所以,程序运行后会退出,当子线程开启的时候,会立即执行mytask方法,而且,mytask方法并没有什么耗时任务,所以来不及添加test2方法就已经程序执行完毕,退出了,所以就没有执行test2方法.

如果开启消息循环,打印结果如下:

2016-12-09 23:46:40.963 消息循环[4855:506953] ----><NSThread: 0x600000071bc0>{number = 3, name = (null)}
2016-12-09 23:46:40.963 消息循环[4855:506953] ???? test2

那么问题来了,为什么没有执行NSLog(@"over");方法?

因为,消息循环一旦开启,执行到消息循环开启的方法[runloop run];就像一个漩涡一样,一旦进去就不会出来了,就会一直执行循环,不会运行下面的代码.

 补充消息循环:

1. 什么是消息循环?

runloop就是消息循环,每一个线程内部都有一个消息循环

主线程消息循环默认开启,子线程消息循环默认不开启,要手动开启

2.消息循环目的?

保证程序不退出

负责处理输入事件

如果没有事件发生,会让程序进入休眠状态

 

消息循环的步骤:

1.创建事件 2.获取当前消息循环,并且把事件加入到消息循环 3.指定事件模式

事件模式要和消息循环模式相匹配才能执行下去

事件模式分为两种:第一种是NSDefaultRunLoopMode  第二种是NSRunLoopCommonModes 而第二种模式是一个模式组,包含第一种模式

消息循环模式:没有触摸界面时默认是kCFRunLoopDefaultMode,触摸界面时会变成UITrackingRunLoopMode,停止触摸又变回kCFRunLoopDefaultMode

所以当我往SB上拖拽一个textView时,上下拉它还能继续进行消息循环的只有commonModes模式匹配.

 

另外还要注意的是子线程手动开启消息循环的方法是run方法

//开启

    [runloop run];

 



 

 总结两把锁:自旋锁联想到上厕所很急,急得团团转,就是自旋锁,

互斥锁联想到排他性,就是单例里的.

 

 

 

 

 

 

 

 

 

 

 

 

---恢复内容结束---

posted @ 2016-12-06 16:21  忆缘晨风  阅读(258)  评论(0编辑  收藏  举报