Runloop


什么是RunLoop

  • 运行循环
  • 内部其实就是do-while循环,在这个循环内部不断地处理各种任务(比如Source、Timer、Observer)
  • 线程和RunLoop是一一对应的关系.一个线程对应一个RunLoop,主线程的RunLoop默认在底层启动,子线程的RunLoop必须得创建,还得调用run方法来进行启动
    -RunLoop一直循环不退出,必须得有运行循环模式,并且在这个模式中得存在Source(Source0,Source1)、Timer中的任意一个

自动释放池什么时候释放

  • 1)runloop启动的时候,会创建一个自动释放池
  • 2)runloop退出和即将休眠的时候,会销毁自动释放池

在开发中RunLoop的应用场景

  • 应用场景1:滑动与图片刷新
  • 当tableview的cell上有需要从网络获取的图片的时候,滚动tableView,异步线程会去加载图片,加载完成后主线程就会设置cell的图片,但是会造成卡顿。可以让设置图片的任务在CFRunLoopDefaultMode下进行,当滚动tableView的时候,RunLoop是在 UITrackingRunLoopMode 下进行,并不会执行CFRunLoopDefaultMode模式下的任务,所以不会去设置图片,而是当停止滚动tableView的时候,才设置图片。
  • 默认情况下,不滚动tableview的cell是CFRunLoopDefaultMode模式,滚动tableview的cell是 UITrackingRunLoopMode模式,这两个模式执行的任务是互不影响的。但是如果让不滚动tableview的cell变为UITrackingRunLoopMode模式,那么当我们滚动tableview的cell时,程序也会开启异步线程去加载图片,造成卡顿。
- (void)viewDidLoad {  
  [super viewDidLoad];  
// 只在NSDefaultRunLoopMode下执行(刷新图片)  
[self.myImageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@""] afterDelay:ti inModes:@[NSDefaultRunLoopMode]];      
}  
  • 应用场景2:开启一个常驻子线程,让子线程不断处理事件
    • 在子线程中创建runloop并开启runloop,而且线程中的runloop中至少添加一个source或者一个timer
    • 在子线程中处理网络请求操作,图片下载操作

GCD中的定时器和OC中的定时器是否受Runloop影响

  • OC中的NSTimer定时器是否工作是受Runloop的影响的.
    • 例如: 定时器简单的添加到RunLoop中时,设置了默认的运行模式。当在界面上滚动textView,ScrollView,TableView时,定时器不会工作。
    • 解决办法:把定时器添加到runloop中,并把runloop的运行模式为kCFRunLoopCommonModes.
  • GCD中的定时器不受Runloop影响

RunLoop与线程

  • 每条线程都有唯一的一个与之对应的RunLoop对象

  • 主线程的RunLoop系统底层已经自动创建好了,子线程的RunLoop需要主动创建

  • RunLoop在第一次获取时创建,在线程结束时销毁


  • Foundation框架(都是方法) 需要加*号,oc的框架
    • [NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
    • [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
    • 例如:NSRunLoop * runloop1 = [NSRunLoop currentRunLoop];
  • Core Foundation框架(都是函数) 不需要加* ,c语言的框架
    • CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
    • CFRunLoopGetMain(); // 获得主线程的RunLoop对象
    • 例如:CFRunLoopRef runloop2 = CFRunLoopGetCurrent();
  • kCFRunLoopDefaultMode等价NSDefaultRunLoopMode
  和RunLoop相关的五个类
        a.CFRunloopRef
        b.CFRunloopModeRef【Runloop的运行模式】
        c.CFRunloopSourceRef【Runloop要处理的事件源】
        d.CFRunloopTimerRef【Timer事件】
        e.CFRunloopObserverRef【Runloop的观察者(监听者)】
核心知识点:
b中的CFRunloopModeRef中,
        Runloop要想跑起来,它的内部必须要有一个mode,这个mode里面必须有source\observer\timer,至少要有其中的一个。
        01.CFRunloopModeRef代表着Runloop的运行模式
        02.一个Runloop中可以有多个mode,一个mode里面又可以有多个source\observer\timer等等
        03.每次runloop启动的时候,只能指定一个mode,这个mode被称为该Runloop的当前mode
        04.如果需要切换mode,只能先退出当前Runloop,再重新指定一个mode进入
        05.这样做主要是为了分割不同组的定时器等,让他们相互之间不受影响
        06.系统默认注册了5个mode
            a.kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
            b.UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
            c.UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
            d.GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
            e.kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode


OC中的定时器


(一)定时器在主线程工作
Runloop_第1张图片
28-01.png

(二)定时器在子线程工作
28-02.png

(三)定时器在主线程工作+Runloop
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
     [self timer2];
}

//第二种获得定时器的方法(基本)
-(void)timer2{
    //1.创建定时器
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(task) userInfo:nil repeats:YES];
    
    //2.添加到运行循环中
    
    /*
     第一个参数:定时器对象
     第二个参数:runloop的运行模式
     */
    //一个Runloop(运行循环)中可以有多个模式,例如:运行循环模式(NSDefaultRunLoopMode)  追踪循环模式UITrackingRunLoopMode
    
    //kCFRunLoopDefaultMode等价于NSDefaultRunLoopMode。只不过前者是C语言,后者是OC。
    //因为下面的NSRunLoop是OC语言,所以传递的参数也应为OC的参数
    
    //2.把定时器添加到运行循环中. 运行循环:[NSRunLoop currentRunLoop].运行循环模式:NSDefaultRunLoopMode
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];//A 第一个参数:定时器对象  第二个参数:runloop的运行循环模式
    
    //出现的问题:当手上下拖动手机上的textView时候,默认运行循环模式切换为了追踪运行循环模式.因为之前的定时器所处的模式是默认运行循环模式,所以并不会在追踪运行循环模式模式调用task方法.
    //所以就造成了当程序开始运行的时候,定时器每隔两秒钟调用task方法,当手上下拖动textView的时候,定时器不再工作,即不会调用task方法。当手指停止在textView上拖动时,定时器又保持每隔两秒钟调用task方法
    
    
    //3.把定时器添加到运行循环中. 运行循环:[NSRunLoop currentRunLoop].运行循环模式:UITrackingRunLoopMode.
    
      [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];//B   A+B可以解决出现的问题,相当于设置了两个运行循环模式
}
-(void)task
{
    NSLog(@"task---%@--%@",[NSThread currentThread],[NSRunLoop currentRunLoop].currentMode);
}
Runloop_第2张图片
Snip20160906_10.png

演示

  • 遇到的问题:


    101.108.gif
  • 解决办法:
101.109.gif

(四)定时器在主线程工作+Runloop
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
     [self timer3];
}

//第二种获得定时器的方法(拓展)
-(void)timer3{
    
    //1.创建定时器
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(task) userInfo:nil repeats:YES];
    /*
     
     在touchesgegan中打印NSLog(@"%@",[NSRunLoop mainRunLoop]);结果如下:
     -----------------------------------------------------------------
     common modes = {type = mutable set, count = 2,
     entries =>
     0 : {contents = "UITrackingRunLoopMode"}
     2 : {contents = "kCFRunLoopDefaultMode"}
     }
     -----------------------------------------------------------------
     从打印结果可知:
     
     被打印成common modes标签的有两个模式,一个是kCFRunLoopDefaultMode,另一个是UITrackingRunLoopMode.
     所以NSRunLoopCommonModes等价于UITrackingRunLoopMode加上kCFRunLoopDefaultMode
     
     可以把定时器添加到NSRunLoopCommonModes中,就可以解决手上下拖动textView时,定时器不工作的问题,并且仅需一行代码.
     *///一个Runloop(运行循环)中可以有多个模式,例如:运行循环模式(NSDefaultRunLoopMode)  追踪循环模式UITrackingRunLoopMode
    
    //2.把定时器添加到运行循环中. 运行循环:[NSRunLoop currentRunLoop].运行循环模式:NSRunLoopCommonModes
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

-(void)task
{
    NSLog(@"task---%@--%@",[NSThread currentThread],[NSRunLoop currentRunLoop].currentMode);
}

Runloop_第3张图片
Snip20160906_9.png

演示


101.107.gif

(五)定时器在子线程工作+Runloop
Runloop_第4张图片
28-05.png

GCD中的定时器

  • GCD的定时器不会受到runloop的影响,即,程序运行时,你执行别的操作也行
  • 队列决定了定时器在哪个线程工作(前提条件:在没有同步函数和异步函数参与的情况下)
    • 参数为主队列,就在主线程中执行
    • 参数为其他的任何队列(全局队列,全局并发队列,串行队列),都在子线程执行
Runloop_第5张图片
28-06.png

CFRunLoopObserverRef

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self timer3];

    [self observer];

}

-(void)observer
{
    //1.创建监听者
    /*
     第一个参数:分配存储空间
     第二个参数:要监听runloop的什么状态
     第三个参数:持续监听 == YES
     第四个参数:传0
     */
    CFRunLoopObserverRef observer =CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(),kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        //当runloop的状态发生改变时候,就执行这个block块里面的内容
        /*
         kCFRunLoopEntry = (1UL << 0),         runloop启动
         kCFRunLoopBeforeTimers = (1UL << 1),  runloop即将处理定时器事件
         kCFRunLoopBeforeSources = (1UL << 2), runloop即将处理source事件
         kCFRunLoopBeforeWaiting = (1UL << 5), runloop即将休眠
         kCFRunLoopAfterWaiting = (1UL << 6),  runloop被唤醒
         kCFRunLoopExit = (1UL << 7),          退出
         kCFRunLoopAllActivities = 0x0FFFFFFFU
         */
        
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"runloop启动");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"runloop即将处理定时器事件");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"runloop即将处理source事件");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"runloop即将休眠");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"runloop被唤醒");
                break;
            case kCFRunLoopExit:
                NSLog(@"runloop退出");
                break;
                
            default:
                break;
        }
    });
    
    
    //2.设置监听的runloop和运行模式
    /*
     第一个参数:runloop对象
     第二个参数:监听者
     第三个参数:runloop的运行模式
     第四个参数:
     */
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    
    //3.释放监听者
    //因为监听者是通过Core Foundation框架创建出来的,必须释放监听者
    CFRelease(observer);
}
-(void)timer3{
    
    //1.创建定时器
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(task) userInfo:nil repeats:YES];

    
    //2.把定时器添加到运行循环中. 运行循环:[NSRunLoop currentRunLoop].运行循环模式:NSRunLoopCommonModes
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];


}
-(void)task
{
    NSLog(@"task---%@--%@",[NSThread currentThread],[NSRunLoop currentRunLoop].currentMode);
}
@end

runloop被唤醒的时候就是要做一些事

2016-03-18 01:05:39.733 08-掌握-NSRunLoop相关类[9957:450017] runloop被唤醒
2016-03-18 01:05:39.734 08-掌握-NSRunLoop相关类[9957:450017] task---{number = 1, name = main}--kCFRunLoopDefaultMode
2016-03-18 01:05:39.734 08-掌握-NSRunLoop相关类[9957:450017] runloop即将处理定时器事件
2016-03-18 01:05:39.734 08-掌握-NSRunLoop相关类[9957:450017] runloop即将处理source事件
2016-03-18 01:05:39.734 08-掌握-NSRunLoop相关类[9957:450017] runloop即将休眠
2016-03-18 01:05:41.733 08-掌握-NSRunLoop相关类[9957:450017] runloop被唤醒
2016-03-18 01:05:41.734 08-掌握-NSRunLoop相关类[9957:450017] task---{number = 1, name = main}--kCFRunLoopDefaultMode
2016-03-18 01:05:41.734 08-掌握-NSRunLoop相关类[9957:450017] runloop即将处理定时器事件
2016-03-18 01:05:41.734 08-掌握-NSRunLoop相关类[9957:450017] runloop即将处理source事件
2016-03-18 01:05:41.734 08-掌握-NSRunLoop相关类[9957:450017] runloop即将休眠
2016-03-18 01:05:43.734 08-掌握-NSRunLoop相关类[9957:450017] runloop被唤醒
2016-03-18 01:05:43.734 08-掌握-NSRunLoop相关类[9957:450017] task---{number = 1, name = main}--kCFRunLoopDefaultMode
2016-03-18 01:05:43.734 08-掌握-NSRunLoop相关类[9957:450017] runloop即将处理定时器事件
2016-03-18 01:05:43.735 08-掌握-NSRunLoop相关类[9957:450017] runloop即将处理source事件
2016-03-18 01:05:43.735 08-掌握-NSRunLoop相关类[9957:450017] runloop即将休眠


代码模拟runloop的死循环(超级经典)

Runloop_第6张图片
28-07.png

  • source1 是基于端口的事件(系统触发的事件, source0是基于非端口的事件(用户主动调用的事件(就是方法,例如[self timer3]))
  • RunLoop先通知Observer观察者,RunLoop先处理Timer,再处理Source0,再处理Source1,最后休眠,如果在休眠时,外界有事件发生(7左边三个),那么RunLoop将被唤醒,进而处理外界的事件,最后再跳回(2),如此往复执行。

RunLoop处理逻辑-网友整理版

Runloop_第7张图片
28-08.png

RunLoop处理逻辑-官方版

Runloop_第8张图片
28-09.png

RunLoop处理逻辑-官方版

  • 输入源和定时器源中的事件都是通过RunLoop(运行循环,黄色的环)传递到线程中,让线程中的4个方法处理对应的事件
  • 输入源(Input source):Port、perforSelector:onThread: 、Custom
    • Port让线程中的handlePort:处理
    • perforSelector:onThread:让线程中mySelector:处理
    • Custom让线程中customSrc:处理
  • 定时器源:Timer source
    • 让线程中timerFired:处理
Runloop_第9张图片
28-10.png

注意

  • 让子线程不死,变成常驻线程 ,那么必须创建子线程的runloop并开启runloop,而且线程中的runloop中至少添加一个source或者一个timer,才不会让线程不死,才不会让runloop退出。以下列出两种做法:

  • 做法1:

    • 1.创建子线程对应的runloop.
    • 2.添加source
    • 3.子线程对应的runloop还需要手动开启
    
    -(void)task1{
     NSLog(@"%s-start--%@",__func__,[NSThread currentThread]);
    
    //1.创建子线程对应的runloop
      NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    
    //2.添加timer
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(timerRun) userInfo:nil repeats:YES];
    [runloop addTimer:timer forMode:NSDefaultRunLoopMode];
    
    //3.子线程对应的runloop还需要手动开启
    [runloop run];
    

}
```

  • 做法2:

    • 1.创建子线程对应的runloop.
    • 2.添加timer
    • 3.子线程对应的runloop还需要手动开启

-(void)task1{
NSLog(@"%s-start--%@",func,[NSThread currentThread]);

//1.创建子线程对应的runloop
  NSRunLoop *runloop = [NSRunLoop currentRunLoop];

//2.添加source
[runloop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];

//3.子线程对应的runloop还需要手动开启
[runloop run];

}
```

具体代码:



#import "ViewController.h"

@interface ViewController ()
/** 线程对象*/
@property (nonatomic ,strong) NSThread *thread;
@end

@implementation ViewController


//子线程(a中的thread)只要执行完任务它的task1,thread就会进入死亡状态,即使 用强指针引用着局部变量thread也不能阻止

//不让thread进入死亡状态的唯一方法就是对task1方法做手脚,不让task1任务执行完,thread就不会进入死亡状态
//具体做法:在task1方法中创建子线程(thread)的运行循环(runloop),并启用runloop。但是runloop要跑起来,必须得有一个运行循环模式(默认是NSDefaultRunLoopMode),而运行循环模式中还必须得有source\observer\timer其中一个,如果没有source\observer\timer其中一个,那么runloop就会退出,并不会循环下去。所以1,2,3缺一不可.


- (IBAction)createBtnClick:(id)sender{//主线程中的方法
    
    //创建子线程
    NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(task1) object:nil];//a
    
    //开始执行
    [thread start];
    
    //NSThread类型的强指针强引用着thread,不让thread销毁
    self.thread = thread;
}

-(void)task1//子线程中的方法
{
    NSLog(@"--------------");//x
    
    //1.创建子线程对应的runloop
    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    
    //2.添加source或者是timer
    
    //方法1:添加timer(2行代码)
    //点击createBtnClick按钮,每隔两秒就执行timerRun方法,从打印可知,形成了死循环,所以y中的内容一直不会打印.所以task1方法永远不会执行完。达到了子线程thread不会进入死亡状态。这样goOnBtnClick按钮就能拿到强指针引用着的,并且是没有死亡的thread,近而就能执行task2方法,如果thread死亡了,那么执行task2就会崩掉。
    
        NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(timerRun) userInfo:nil repeats:YES];
        [runloop addTimer:timer forMode:NSDefaultRunLoopMode];
  
    
    
    //方法2:添加source(1行代码)
    //点击createBtnClick按钮,只会打印x,不会打印y,说明在x和y之间肯定有一个死循环。并且点击goOnBtnClick程序会打印task2的内容,更加验证了在x和y之间肯定有一个死循环,促使task1永远不会执行完,这样task1的线程thread就不会被销毁
    
   //[runloop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    
    //3.子线程对应的runloop还需要手动开启
    [runloop run];
    
    NSLog(@"%s---%@",__func__,[NSThread currentThread]);//y
}



- (IBAction)goOnBtnClick:(id)sender//主线程中的方法
{
    //只要是performSelector: onThread的这种形式一定涉及到了线程间通信。详细看线程间通信的两个方法Day26
    
    NSLog(@"--goOn---");
    //让强指针引用者的self.thread这个子线程调用task2方法
    //调用前提:self.thread必须保证他里面的task1方法没有执行完,这样子线程thread才不会被销毁。如果被销毁了,程序就会崩掉
    [self performSelector:@selector(task2) onThread:self.thread withObject:nil waitUntilDone:YES];//子线程
    
    
}

-(void)task2
{
    NSLog(@"%s---%@",__func__,[NSThread currentThread]);
}

-(void)timerRun
{
    NSLog(@"%s---%@",__func__,[NSThread currentThread]);
}
@end


runloop的自动释放池

/*
 打印NSLog(@"%@",[NSRunLoop mainRunLoop]);可知:
 
 runloop的自动释放池有两种状态: activities = 0xa0 -->(160=128+32),activities = 0x1 -->(1)
 一个肯定是创建时的状态,另一个肯定是销毁时的状态。

 runloop本身是有肯多状态的。选中CFRunLoopActivity进入文档,如下
 
        kCFRunLoopEntry = (1UL << 0),              1(枚举值)     runloop启动
        kCFRunLoopBeforeTimers = (1UL << 1),       2            runloop即将处理定时器事件
        kCFRunLoopBeforeSources = (1UL << 2),      4            runloop即将处理source事件
        kCFRunLoopBeforeWaiting = (1UL << 5),      32           runloop即将休眠
        kCFRunLoopAfterWaiting = (1UL << 6),       64           runloop被唤醒
        kCFRunLoopExit = (1UL << 7),               128          退出
 
 通过动释放池有两种状态的值和runloop本身的状态值比对,可以得到如下2个总结:
 
 1)runloop启动的时候,会创建一个自动释放池【根据:自动动释放池二进制值为1】
 2)runloop退出和即将休眠的时候,会销毁自动释放池【根据:自动释放池二进制值为160=128+32】
 
 */

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    
    NSLog(@"%@",[NSRunLoop mainRunLoop]);
    //十六进制转换成十进制
    NSLog(@"%d---%d",0x1,0xa0);
    
}
@end

你可能感兴趣的:(Runloop)