多线程(详解)

1.iOS 开发中多线程出现的本质?
  • 原因:一个iOS程序运行后,默认会开启1条线程,称为“主线程”或“UI线程”来处理任务。当任务量变得很大时,主线程疲于处理当前耗时操作,对于其他需要处理的任务就会响应不过来,宏观体现 —— 好卡。 为了解决这种“卡”的用户体验,出现了多线程处理任务策略。
  • 注意:多线程并发执行任务本质是cpu的快速切换。
  • 技巧:iOS中模拟耗时操作,只需写一个很大的for循环
2.iOS中多线程的实现方案(4种)

①pthread

  • 使用步骤:
  • 导入头文件 #import <pthread.h>
  • 调用创建线程函数
#import "ViewController.h"
#import <pthread.h>

@interface ViewController ()
@end

@implementation ViewController

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{

// 创建线程
/*
 第一个参数:线程对象地址
 第二个参数:属性
 第三个参数:要调用的方法指针
 第四个参数:要传递给函数的参数
 */
pthread_t thread = nil;
pthread_create(&thread, NULL, run, NULL);
}
//(*) =>函数名称
void *run(void *str)
{
    for (NSInteger i =0 ; i<300; i++) {
    NSLog(@"%zd--%@",i,[NSThread currentThread]);  //均为子线程
    }
    return NULL;
}
@end

②NSThread

  • 使用步骤:直接创建线程,不需要导入头文件

  • 创建线程方式:

    <方式1>
    NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(run:) object:@"ios"];
    [thread start];
    优点:能拿到线程对象 缺点:需要手动的启动线程

    <方式2>
    [NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:nil];
    优点:自动启动线程 缺点:不能拿到线程对象

    <方式3>
    [self performSelectorInBackground:@selector(run:) withObject:@"后台线程"];
    优点:自动启动线程 缺点:不能拿到线程对象

    <方式4>自定义,重写main方法封装任务
    NewThread *threadB = [[NewThread alloc]init];
    [threadB start];
    优点:能拿到线程对象 缺点:需要手动的启动线程
    => 线程封装任务是在main方法里面。

  • 注意:

  • 生命周期:线程当任务执行完毕的时候自动销毁

  • 线程对象先执行完任务的先销毁

  • 线程优先级只在任务相对较多时更好的体现,第一次执行还是按照线程开始的先后顺序执行。(线程优先级threadPriority,范围:0~1.0 默认是0.5)

③GCD

1.使用步骤:任务 + 队列

//1 获得队列
 第一个参数:C语言的字符串 对队列的名称(com.520it.www.DownloadQueue)
 第二个参数:队列的类型
 DISPATCH_QUEUE_SERIAL  串行队列
 DISPATCH_QUEUE_CONCURRENT 并发队列

dispatch_queue_t queue = dispatch_queue_create("com.520ios.www.DownloadQueue”,DISPATCH_QUEUE_SERIAL);
//dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

//2 封装任务并把任务添加到队列
dispatch_async(queue, ^{
    NSLog(@"download1---%@",[NSThread currentThread]);
});

拓展:GCD常用函数

1)一次性代码

  • 特点:
  • 整个程序运行过程中只会执行一次
  • 线程安全
  • 程序每次启动都会执行一次
  • 一次性代码不能放在懒加载中:因为创建多个对象时,也只会执行一次。单例中才可以。
-(void)once
{
    static dispatch_once_t onceToken;  //typedef long dispatch_once_t;
    //内部的实现原理:最开始的时候onceToken == 0 如果onceToken == 0 那么就执行一次,执行一次之后onceToken = -1
    dispatch_once(&onceToken, ^{
      NSLog(@"once"); 
    });
  }
应用场景:单例(注意:单例中使用一次性代码或同步锁均可,都是线程安全)

2)延迟执行

  • 特点:
    • dispatch_after本身是一个异步函数
    • 该函数的队列参数选项,除了队列设置的为“主队列”则延时执行的任务在主线程中执行外,其他队列做参数,均在子线程中执行延时任务,包括参数队列设置为自己创建的串行队列。
    • 延迟执行的单位:纳秒 ,精度高。
  dispatch_queue_t queue = dispatch_queue_create("TestQueue", DISPATCH_QUEUE_SERIAL);
  //   延迟2秒,然后再把任务提交到队列
  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), queue, ^{
    NSLog(@"GCD----%@",[NSThread currentThread]); //子线程=> 异步函数
});
  • 拓展总结:延迟执行方法
    //延迟方法一(NSTimer)
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(task) userInfo:nil repeats:NO]; //默认添加进“当前RunLoop”,且是默认模式
    [NSTimer timerWithTimeInterval:2.0 repeats:YES block:nil]; //此种方法要将定时器手动添加进RunLoop

    //延迟方法二(NSRunloop)
    [self performSelector:@selector(task) withObject:nil afterDelay:2.0];
    
    //延迟方法三:(GCD中的延迟执行/GCD定时器)
    dispatch_queue_t queue = dispatch_queue_create("TestQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 *     NSEC_PER_SEC)), queue, ^{
      NSLog(@"GCD----%@",[NSThread currentThread]); //子线程
    });
    

3)快速迭代

  • 特点:
  • 快速迭代属于:同步函数 (在主队列中会发生死锁 + 打印【显示它没开子线程!!】共同推导出的)
  • 但注意:在并发队列中还是会同时开启"主线程"和"子线程"一起并发执行任务(主流)
  • 在自己创建的串行队列中会在主线程中串行执行,不开子线程故无意义,因为开子线程还要耗费性能!性能更低。
  • 快速迭代:本质就是遍历执行任务,只不过对比for循环,多了开启子线程的能力而已,故效率更高。
 dispatch_queue_t queue =  dispatch_get_global_queue(0, 0);// 开启子线程协助主线程完成任务
//     dispatch_queue_t queue = dispatch_queue_create("good", DISPATCH_QUEUE_CONCURRENT);
//     dispatch_queue_t queue = dispatch_get_main_queue();//死锁 => 同步函数 + 主队列
//     dispatch_queue_t queue = dispatch_queue_create("TestQueue", DISPATCH_QUEUE_SERIAL);

dispatch_apply(10, queue, ^(size_t i) {
    NSLog(@"%zd---%@",i,[NSThread currentThread]); //内部执行顺序是并发的,外部却认为它没开子线程!!
});


应用场景 :遍历并剪切文件

-(void)moveFile
{
    //01 得到上层文件夹的路径
    NSString *fromPath = @"/Users/wuyuanping/Desktop/from";
    NSString *toPath = @"/Users/wuyuanping/Desktop/to";

    //02 得到上层文件夹中所有的文件
    NSArray *subPaths =  [[NSFileManager defaultManager] subpathsAtPath:fromPath];

    //03 遍历并剪切文件
    dispatch_apply(subPaths.count, dispatch_get_global_queue(0, 0), ^(size_t index) {
   
        NSString *fileName = subPaths[index];
        NSString *fromFullPath = [fromPath stringByAppendingPathComponent:fileName];
        NSString *toFullPath = [toPath stringByAppendingPathComponent:fileName];
    
        [[NSFileManager defaultManager] moveItemAtPath:fromFullPath toPath:toFullPath error:nil];
    
        NSLog(@"%@--%@--%@",fromFullPath,toFullPath,[NSThread currentThread]); //子线程和主线程都有
   });
}

4)栅栏函数

  • 特点:
  • 拦截栅栏后面的任务,必须等前面的任务执行完才执行栅栏当前的block,且必须等栅栏的block执行完才执行栅栏后面任务
  • dispatch_barrier_async 栅栏Block创建了子线程则在子线程中执行任务(因为“主队列”只能在主线程中执行任务);dispatch_barrier_sync 主线程执行栅栏任务
  • 子线程中可以出现:同步函数 + 主队列 不会发生死锁 ,不建议子线程中开子线程,系统空闲就可以开,否则不能
  • 栅栏函数在使用中不能使用系统提供的全局并发队列(会丧失拦截的功能),只能用自己创建的并发队列
dispatch_queue_t queue = dispatch_queue_create("TestQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_barrier_async(queue, ^{
      NSLog(@"+++++%@",[NSThread currentThread]);
 });
应用场景:需求是哪些子线程任务先执行

5)队列组

  • 特点:

  • 当队列组中所有的任务都执行完毕,那么就会执行dispatch_group_notify的block

  • dispatch_group_notify该函数本身是异步的

  • 队列组的使用:

方式一:
// 添加任务进组函数
//dispatch_group_async =>异步函数封装任务|提交到队列|监听任务是否执行完毕
//dispatch__async      =>异步函数封装任务|提交到队列

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("TestQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue, ^{
    NSLog(@"1----%@",[NSThread currentThread]);
});

dispatch_group_async(group, queue, ^{
    NSLog(@"2----%@",[NSThread currentThread]);
});

dispatch_group_async(group, queue, ^{
    NSLog(@"3----%@",[NSThread currentThread]);
});

dispatch_group_async(group, queue, ^{
    NSLog(@"4----%@",[NSThread currentThread]);
});

dispatch_group_notify(group, queue, ^{ //组任务都执行完毕之后会调用此方法
    NSLog(@"---end---%@",[NSThread currentThread]); //子线程
});

方式二:
 //使用函数对来监听任务(dispatch_group_enter|dispatch_group_leave)
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("TestQueue", DISPATCH_QUEUE_CONCURRENT);

//在该函数后面的异步任务会被group监听
dispatch_group_enter(group);
dispatch_async(queue, ^{
    NSLog(@"1----%@",[NSThread currentThread]);
    dispatch_group_leave(group);
});

dispatch_group_enter(group);
dispatch_async(queue, ^{
    NSLog(@"2----%@",[NSThread currentThread]);
    
    dispatch_group_leave(group);
});

dispatch_group_enter(group);
dispatch_async(queue, ^{
    NSLog(@"3----%@",[NSThread currentThread]);
    
    dispatch_group_leave(group);
});

dispatch_group_notify(group, queue, ^{ //组任务都执行完毕之后会调用此方法
    NSLog(@"---end---%@",[NSThread currentThread]);
});

拓展:GCD定时器

  -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//1 创建定时器对象(技巧:输入dispatch_source 再选GCD的会一次性出来四个函数)
/*
 第一个参数:创建的source的类型 DISPATCH_SOURCE_TYPE_TIMER 定时器事件
 第二个参数:描述信息
 第三个参数:更详细的描述信息
 第四个参数:队列(线程)[主队列那么回调就在主线程中执行,否则就在子线程中执行]
 */
 dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL));

//2 设置定时器对象
/*
 第一个参数:定时器对象
 第二个参数:开始时间(第一次执行的时间)
 第三个参数:间隔时间 GCD的时间单位是纳秒
 第四个参数:精准度(允许的误差,一般取0)
 */
dispatch_time_t timeT = dispatch_time(DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC);//tips:取消一开始就执行一次,即达到也是延时两秒执行
dispatch_source_set_timer(timer, timeT, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);

//3 设置定时器的事件
dispatch_source_set_event_handler(timer, ^{
    NSLog(@"GCD---%@",[NSThread currentThread]); //非主队列,则为子线程
});

//4 执行定时器对象
dispatch_resume(timer);

self.timer = timer;//强引用,防止定时器一出作用域就销毁
}

GCD总结

  • 具体是否创建子线程?
  • 异步函数 + 并发队列: => 能够开启多条子线程“并发”的执行队列中的任务(验证“线程间”及“各个子线程中任务”并发执行?打印)
  • 异步函数 + 串行队列: => 一个串行队列对应只能开启一条子线程串行执行任务,即严格按照任务代码块从上往下"串行"执行任务。
    注意:多个串行队列,就相当于开启了多条子线程,线程间则是异步执行任务的。同一条子线程则串行
  • 同步函数 + 并发队列/串行队列: => 不会开启子线程,所有的任务在当前线程中串行执行
  • 异步函数 + 主队列:=> 不会开子线程,所有的任务均在主线程中的串行执行
    (异步函数可以并发执行,不会一直等待前面线程执行完才轮到自己执行,故不会发生死锁)
  • 同步函数 + 主队列: => 代码写在主线程中会发生死锁(会一直傻等主线程来执行故发生死锁) 但代码写在子线程中就不会发生死锁。故可以用在线程间通信(子线程跳到主线程)
  • “并发”的本质是:取出上一个任务之后,无论是否执行完直接接着取下一个任务,多个Blcok块各有很多任务时,按一开始执行子线程的顺序你一下我一下并肩完成。
  • 开子线程的数量并不是仅仅由任务的数量决定的,要看系统是否空闲。出于性能考虑iOS中一般只能开6条子线程
  • 一个Block任务代码块中只能开启一个子线程
  • 当系统空闲时,子线程执行任务的顺序等于创建子线程的先后顺序,当任务变多,顺序就不确定。
  • 只要不涉及到创建子线程,代码执行的顺序就是老老实实的从上往下串行执行。(因为均在主线程,默认主队列)
  • 创建子线程本质原因:“并发”从而避免主线程忙不过来(卡),或者说充分利用cpu性能。

④NSOperation

  • 步骤: 操作 + 队列
  • 注意:
  • NSOperation是抽象类不能直接使用,可以使用它的子类(系统提供了NSInvocationOperation,NSBlockOperation和自定义的)
    【自定义继承自NSOperation :重写内部的Main方法,说明start方法内部也是调用的main方法 ,优点:有利于代码的封装和复用】
  • 单独封装操作并执行时并不会开启子线程,只会在主线程中执行,除非配合非主队列一起使用时才会生成子线程并发执行任务
  • 单独封装操作并执行时,也可以通过添加额外任务addExecutionBlock,即操作对象中封装的任务数量>1也会开子线程执行任务
    但注意:额外操作才会开子线程去执行,之前的操作 仍然在主线程中执行,这是与操作直接添加进非主队列的区别所在。
  • GCD和NSOperation队列对比:
    GCD:
    并发:自己创建|全局并发
    串行:自己创建|主队列
    NSOperation:
    并发:自己创建[[NSOperationQueue alloc]init]默认是并发队列
    串行: 自己创建[[NSOperationQueue alloc]init]设置最大并发数等于1 | 主队列
    [NSOperationQueue MainQueue]
    注意:通过设置最大并发数为1而达到串行执行任务目的 与 主队列 的串行 是有区别的。前者还是会生成子线程,只不过是控制子线程间只能串行执行任务,具体生成子线程个数不确定。 顺序就为添加进队列(开始)顺序执行
    后者不会生成子线程,在主线程中串行执行任务。
    maxConcurrentOperationCount 最大并发数:允许并发执行的子线程数
非主流开发演示:
//1.封装操作
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"3---%@",[NSThread currentThread]); //主线程
}];

//NSBlockOperation如果操作对象中封装的操作数量>1那么添加的代码块中就会开子线程和当前线程一起执行任务
[op3 addExecutionBlock:^{
    NSLog(@"+++++++4++++%@",[NSThread currentThread]); //子线程
}];
[op3 addExecutionBlock:^{
    NSLog(@"+++++++5++++%@",[NSThread currentThread]);//子线程
}];

//2 执行操作
[op3 start];


主流开发代码演示:
-(void)BlockOperationWithQueue
{   //操作 + 队列:只要不是主队列,就会开子线程并发执行任务
    
  //1.封装操作
  NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"1----%@",[NSThread currentThread]);
  }];
  //NSInvocationOperation *op1 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(download) object:nil];

  //2.把操作添加到队列
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    [queue addOperation:op1];  //addOperation 内部调用start方法,故你可以不写start

  //该方法内部先封装操作,然后把操作添加到队列中(简便写法:直接操作添加到队列)
  [queue addOperationWithBlock:^{
    NSLog(@"3---%@",[NSThread currentThread]); //子线程
  }];
}

拓展:操作队列的其他用法

  • 设置最大并发数量 (maxConcurrentOperationCount)
  • 设置操作依赖(addDependency)和监听(completionBlock)
  • 操作队列的暂停(setSuspended)和取消(cancel/cancelAllOperations)
    注意:
  • 操作的状态: 正在执行|等待执行|执行完毕
  • 暂停 只能暂停下一个操作,当前正处于执行状态的操作是不能暂停的
  • 取消队列中所有的任务 当前正在执行的操作不能马上取消,需要等当前操作执行完毕才能取消后面的操作
  • 如果自定义NSOperation苹果官方的建议:每执行完一段耗时操作之后就检查当前操作是否被取消
自定义NSOperation:
#import “YPOperation.h"
@implementation YPOperation

-(void)main
{
  for (NSInteger i = 0; i<1000; i++) {
      NSLog(@"1---%zd--%@",i,[NSThread currentThread]);
      //if(self.isCancelled) 
      return;
  }

  //每执行完一段耗时操作之后就判断当前操作是否被取消,如果被取消了那么就直接退出
  if(self.isCancelled) return;

  NSLog(@"++++++++++++++");

  for (NSInteger i = 0; i<1000; i++) {
      NSLog(@"2---%zd--%@",i,[NSThread currentThread]);
       //if(self.isCancelled) return;
  }
   if(self.isCancelled) return;

  for (NSInteger i = 0; i<1000; i++) {
       if(self.isCancelled) return;
      NSLog(@"3---%zd--%@",i,[NSThread currentThread]);
   }
}
@end


例子:设置监听和操作依赖
 NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"3----%@",[NSThread currentThread]);
}];

 NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"4-下载电影-%@",[NSThread currentThread]);
}];   
 NSBlockOperation *op5 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"+++++++5+%@+++",[NSThread currentThread]);
}];
//设置监听
op4.completionBlock = ^{
    NSLog(@"我已经被下载完了,快点来看我吧--%@",[NSThread     currentThread]);
};

//设置依赖(不能设置循环依赖)
[op3 addDependency:op4];
[op4 addDependency:op5];

注意:因为开启子线程自定义队列默认异步执行,故只能保证op4任务结束会执行他的监听Block,即block在op4后面执行,
     但无法保证一定紧接其后,要看具体情况)  

综合案例: 多图下载(利用上面所学的多线程技术)

  • 总结:

    • 基本数据展示
      问题:UI不流畅 => 在子线程中下载图片
      注意:cell的复用造成数据错乱,通过设置占位图片解决该问题
    • 性能优化
      问题:重复下载图片
      ①已经下载好了图片的 => 二级缓存(内存缓存磁盘缓存)
      ②已经开始执行下载任务但还没有下载完 => 缓存下载任务(操作)
  • 注意点
    ①图片下载完成之后把操作从缓存中移除
    ②图片下载完成后,对获得的图片数据做容错处理
    ③发生内存警告后的处理:1)移除内存缓存中的所有元素 2)取消下载图片队列中所有的操作

  • 拓展:磁盘(沙盒)缓存的目录结构:

  • ①Documents :该目录下面的数据在连接手机时会备份,苹果官方不允许把下载的数据存放于该目录下

  • ②Libriary
    1)caches
    2)perference 该目录用来存放偏好设置如登录名密码等等

  • ③Temp :会被随机删除

3.线程状态
 新建 -> 就绪 <-> 运行 -> 死亡
          |
         阻塞
  • 注意:
  • “运行”状态可以直接切换为“阻塞”状态,阻塞状态不能直接切换为运行,必须先切换为“就绪”状态,再从就绪状态切换为“运行”状态。
  • 线程死亡之后不能重新开启,得重新创建。
  • 阻塞线程的方式:
    [NSThread sleepForTimeInterval:3.0];
    [NSThread sleepUntilDate: [NSDatedateWithTimeIntervalSinceNow:2.0]];
  • 线程死亡的方式:
    ①任务执行完毕(默认)
    ②强制退出线程 + (void)exit;
    ③提前让任务结束 break和return
4.线程安全
  • 问题产生的原因:多个线程访问同一块资源会发生数据安全问题
  • 解决方式: 线程同步(同一时间只有一个线程访问同一块资源)
    @synchronized(锁对象){
    要锁住的代码
    }
    锁对象:要求是全局唯一的属性,一般用self
  • 注意点:
  • 要注意加锁的位置
  • 加锁需要耗费性能,因此需要注意加锁的条件(多线程访问同一块资源,不得已才去加锁)
  • 原子和非原子属性:
    atomic会对setter方法加锁 (OSX应用,常用atomic,电脑性能好,更多的是关注安全)
    noatomic不会对Setter方法加锁(性能高,安全性低常用于iOS 应用)
应用:售票
-(void)saleTicket
{
     while (1) {  //让线程一直执行
         @synchronized (self) {
          //检查余票
          NSInteger count = self.totalCount;
          if (count >0) {
              //卖出去一张
              self.totalCount = count - 1;
              NSLog(@"%@卖出去了一张票还剩下%zd张票",[NSThread   currentThread].name,self.totalCount);
            }else
          {
              NSLog(@"%@发现票已经卖完了",[NSThread currentThread].name);
              break; //让线程死亡
          }
       }
    }
}
5.线程间通信
①利用NSThread方法(2种)
 [self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:NO];
 [self performSelector:@selector(showImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];
 //直接调用本质方法
 [self.imageView performSelector:@selector(setImage:) onThread:
 [NSThread mainThread] withObject:image waitUntilDone:YES];

②利用GCD方法
//同步函数+主队列(异步函数+ 主队列更主流)
 dispatch_sync(dispatch_get_main_queue(), ^{// 注意:当“同步函数+主队列”是写在子线程当中时是不会发生死锁的
        self.imageView.image = image;
        NSLog(@"UI---%@",[NSThread currentThread]);//主线程
});

③利用NSOperation方法
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
        
        self.imageView.image = image;
        NSLog(@"UI----%@",[NSThread currentThread]);
}];
6.RunLoop
  • 基本作用
    ①保持程序的持续运行(ios程序为什么能一直活着不会死,如果没有Runloop,那么程序一启动就会退出)
    ②处理app中的各种事件(比如触摸事件、定时器事件、selector事件)
    ③节省CPU资源,提高程序性能,有事情就做事情,没事情就休息

  • 注意:
    1>如果有了Runloop,那么相当于在内部有一个死循环
    2>main函数中的Runloop :在UIApplicationMain函数内部就启动了一个Runloop,这个默认启动的Runloop是跟主线程相关联的

  • RunLoop使用
    (在iOS开发中有两套api来访问Runloop,它们是等价的,可以互相转换)
    ①Foundation框架 :(OC)
    [NSRunLoop mainRunLoop];获得主线程对应的Runloop
    [NSRunLoop currentRunLoop];获得执行当前方法的线程对应的Runloop

    ②CoreFoundation框架 :(C)
                          CFRunLoopGetMain();获得主线程对应的Runloop
                          CFRunLoopGetCurrent();获得执行当前方法的线程对应的Runloop
    
    相互转换: CFRunLoopRef runloop = mainRunLoop.getCFRunLoop;
             NSRunLoop和CFRunLoopRef都代表着RunLoop对象
    
  • Runloop与线程关系
    ①一个Runloop对应着一条唯一的线程
    ②主线程Runloop已经创建好了,子线程的runloop需要手动创建并开启(模式不能为空,得有任务干Runloop才不会退出)
    ③Runloop在第一次获取时创建,在线程结束时销毁

  • RunLoop相关5个类
    ①CFRunloopRef(Runloop对象)
    ②CFRunloopModeRef(Runloop的运行模式)
    ③CFRunloopSourceRef(Runloop要处理的事件源)
    ④CFRunloopTimerRef(Timer事件)
    ⑤CFRunloopObserverRef(Runloop的观察者(监听者))

  • RunLoop使用总结:

  • <1>CFRunloopModeRef (代表着Runloop的运行模式5种)
    ①kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
    ②UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动
    ③UIInitializationRunLoopMode: 在刚启动 App 时进入的第一个 Mode,启动完成后就不再使用
    ④GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
    ⑤kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode,标记mode
    (kCFRunLoopCommonModes == UITrackingRunLoopMode +kCFRunLoopDefaultMode)

    • 注意:
      1. NSTimer在各种模式下运行的效果:
      ①scheduledTimerWithTimeInterval方法:创建定时器并默认添加到当前线程的Runloop中指定默认运行模式
      ②timerWithTimeInterval:创建定时器,如果该定时器要工作还需要添加到runloop中并指定相应的运行模式
      2.GCD中的定时器在各种模式下运行的效果:
      ①GCD的定时器不会受到Runloop运行模式的影响(因为source不包括这个定时器)
      ②GCD定时器对象需要添加强引用,防止被销毁
  • <2>CFRunloopSourceRef(Runloop要处理的事件源)
    按照苹果官方文档进行划分的:
    ①Port-Based Sources (基于端口)
    ②Custom Input Sources (自定义输入源)
    ③Cocoa Perform Selector Sources (Perform Selector 输入源)
    函数调用栈划分:
    ①Source0:非基于Port的
    ②Source1:基于Port的

  • <3>CFRunLoopObserverRef
    作用:监听指定模式下运行循环的状态
    如何监听?
    1.创建观察者对象
    2.给RunLoop添加观察者
    3.注意对象的释放

  -(void)observer
{
  //1 创建观察者对象
  //block调用:当监听者发现runloop状态改变的时候会调用block
  CFRunLoopObserverRef observer =   CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    
    switch (activity) {
        case kCFRunLoopEntry:
            NSLog(@"进入runloop");
            break;
        case kCFRunLoopBeforeTimers:
            NSLog(@"即将处理time事件");
            break;
        case kCFRunLoopBeforeSources:
            NSLog(@"即将处理source事件");
            break;
        case kCFRunLoopBeforeWaiting:
            NSLog(@"即将休眠");
            break;
        case kCFRunLoopAfterWaiting:
            NSLog(@"runloop被唤醒");
            break;
        case kCFRunLoopExit:
            NSLog(@"runloop退出");
            break;
            
        default:
            break;
      }
  });

  //2 给RunLoop添加观察者
/*
 NSDefaultRunLoopMode = kCFRunLoopDefaultMode
 NSRunLoopCommonModes = kCFRunLoopCommonModes  //追踪模式下的RunLoop也可以追踪
 */
  CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

  //3.释放Observer
  CFRelease(observer);
}
RunLoop总结
  • 说明:

  • 一个Runloop中可以有多个mode,一个mode里面又可以有多个source\observer\timer等等

  • 每次Runloop启动的时候,只能指定其中一个mode,这个mode被称为该Runloop的当前mode

  • 如果需要切换mode,只能先退出当前Runloop,再重新指定一个mode进入当前Runloop,目的:为了分割不同组的定时器等,让他们相互之间不受影响

  • runloop要想持续运行,必须选中的mode不为空(即mode里面source或Timer至少有一个)

  • 子线程的RunLoop必须手动创建并开启,要想持续运行,mode不能为空,得要有任务处理才不会退出。

  • 线程与RunLoop一一对应,不过子线程的Runloop需要手动创建并开启,否则执行完一次任务就退出。

  • 线程执行完任务就会死亡,故保持后台常驻线程 — RunLoop一直有活干,一般加定时器,或端口(source三种)

  • Runloop应用
    1.ImageView显示(利用PerformSelector 可以设置运行模式)
    2.常驻线程
    3.自动释放池
    第一次创建:Runloop启动的时候创建
    最后一次销毁:Runloop退出的时候销毁
    其它时候的创建和销毁:当Runloop即将进入休眠状态的时候会把当前的自动释放池释放并创建一个新的

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

推荐阅读更多精彩内容

  • 从哪说起呢? 单纯讲多线程编程真的不知道从哪下嘴。。 不如我直接引用一个最简单的问题,以这个作为切入点好了 在ma...
    Mr_Baymax阅读 2,676评论 1 17
  • 欢迎大家指出文章中需要改正或者需要补充的地方,我会及时更新,非常感谢。 一. 多线程基础 1. 进程 进程是指在系...
    xx_cc阅读 7,118评论 11 69
  • 在了解GCD之前,我们首先要知道几个概念。关于队列和同/异步函数。为了让读者更简单直观的理解这些概念,我尽可能用最...
    fou7阅读 861评论 1 2
  • 线程的串行 一个线程中任务的执行是串行的如果要再一个线程中执行多个任务,那么只能一个一个的按顺序执行这些任务也就是...
    Coder007阅读 201评论 0 2
  • demo:MultiThreadingPractice 进程:系统正在运行的一个应用程序,没打开一个app系统就开...
    YY_Lee阅读 448评论 0 6