1.概念:

运行循环,内部有do-while循环实现的

保持程序的持续运行,处理APP各种事件(滑动,定时,selector),节省CPU资源,提高程序性能

一个线程对应一个runloop,主线程的RunLoop随着程序已自动创建好,但是子线程的RunLoop需要手动创建

每次RunLoop启动时,只能指定其中一个Mode,这个Mode被称作currentMode,如果当前的Mode中没有任何source,timer,那么就直接退出Runloop

 

2.runloop对象

iOS中有两套API来访问和使用RunLoop

1)Foundation:NSRunLoop(OC语言的)

2)Core Foundation:CFRunLoopRef(C语言的)

 

3.runLoop与线程

1)每条线程都唯一的一个与之对应的RunLoop对象

2)主线程的RunLoop随着程序已自动创建好,但是子线程的RunLoop需要手动创建

3)获得主线程RunLoop的方法是[NSRunLoop mainRunLoop];

4)创建子线程的RunLoop的方法是[NSRunLoop currentRunLoop].

  注意:苹果不允许创建RunLoop,只提供两种获取RunLoop的方法

CFRunLoopGetCurrent();

CFRunLoopGetMain();

 

4.RunLoop相关类

CFRunLoopModeRef

  代表了RunLoop的运行模式,一个RunLoop可以包含若干个Mode,没个Mode包含若干个Source/Timer/Observer

  每次RunLoop启动时,只能指定其中一个Mode,这个Mode被称作currentMode

  如果需要切换Mode,需要退出RunLoop,在重新指定一个Mode进入.这样做的原因是为了分离不同组的Source/Timer/Observer.让其互补影响

  系统默认注册了5个Mode:

  • kCFRunLoopDefaultMode:APP的默认Mode,通常主线程是在改Mode下运行的
  • UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时,不受其他Mode影响
  • UIInitializationRunLoopMode:刚启动时APP进入的第一个Mode,启动完成后就不再使用
  • GSEventReceiveRunLoopMode:接受系统时间的内部Mode,通常情况下不用
  • kCFRunLoopCommonModes:这是一个占位用的Mode,不是真正的Mode

 

 

 

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

    

    //定时器自动加在RunLoop下,可以直接运行

//    [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

    

    

    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

    

    //只适用于默认模式下

//    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];

    

    //只适应Tracking模式下

//    [[NSRunLoop currentRunLoop]addTimer:timer forMode:UITrackingRunLoopMode];

    

    //commonModes只是一个标记,有这个标记的模式有NSDefaultRunLoopMode UITrackingRunLoopMode

    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];

   

}

-(void)run{

    NSLog(@"--run--%@",[NSRunLoop currentRunLoop]);

}

应用二:

  [NSThread detachNewThreadSelector:@selector(time2) toTarget:self withObject:nil];

-(void)time2

{

  NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];

  

  //该方法内部自动添加到runloop中,并且设置运行模式为默认

  [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

  

  [currentRunLoop run];

  

  

}

CFRunLoopSourceRef

  是事件源(输入源)

  按照官方文档分类:

  1.Port-Based Sources从其他线程或内核发出的

  2.Custom Input Sources自定义的

  3.Cocoa Perform Selector Sources

       [self performSelector:@selector(run ) withObject:nil afterDelay:2 inModes:@[NSRunLoopCommonModes ]];  

   

  按函数调用栈分类,可分为2类:

  1.sources0:非基于Port的

  2.sources1:基于Port的,通过其他线程或者内核通信,接收,分发系统事件

CFRunLoopTimerRef

  • CFRunLoopTimerRef􏱀􏹞􏰆􏰫􏰌􏱈􏷠􏲌􏰵是基于时间的触发

  • CFRunLoopTimerRef基本上说的就是􏹞􏰽􏲵􏳂􏱈􏰿􏱀NSTimer􏱨􏱯􏺫 ,它受RunLoop􏱈的Mode􏺬􏹘影响

  • GCD的定时器不受􏱈􏰴􏰫􏰵􏰬􏺫RunLoop的􏱈Mode􏺬􏹘 
影响

1、必须保证有一个活跃的RunLoop。

     系统框架提供了几种创建NSTimer的方法,其中以scheduled开头的方法会自动把timer加入当前RunLoop,到了设定时间就会触发selector方法,而没有scheduled开头的方法则需要手动添加timer到一个RunLoop中才会有效。程序启动时,会默认启动主线程的RunLoop并在程序运行期内有效,所以把timer放入主线程时不需要启动RunLoop,但现实开发中主线程更多的是处理UI事物,把耗时且耗能的操作放在子线程中,这就需要将子线程的RunLoop激活。

     我们不难知道RunLoop在运行时一般有两个:NSDefaultRunLoopMode、NSEventTrackingRunLoopMode,scheduled生成的timer会默认添加到NSDefaultRunLoopMode,当某些UI事件发生时,如页面滑动RunLoop切换到NSEventTrackingRunLoopMode运行,我们会发现定时器失效,为了解决timer失效的问题,我们需要在scheduled一个定时器的时候,设置它的运行模式为:

[[NSRunLoop currentRunLoop] addTimer:self.progressTimer forMode:NSRunLoopCommonModes];

     注意:NSRunLoopCommonModes并不是一种正在存在的运行状态,这个模式等效于NSDefaultRunLoopMode和NSEventTrackingRunLoopMode的结合,相当于它标记了timer可以在这两种模式下都有效。

     2.NSTimer的创建与撤销必须在同一个线程操作,不能跨越线程操作。

     3.存在内存泄漏的风险(这个问题需要引起重视)

     scheduledTimerWithTimeInterval方法将target设为A对象时,A对象会被这个timer所持有,也就是会被retain一次,timer又会被当前的runloop所持有。使用NSTimer时,timer会保持对target和userInfo参数的强引用。只有当调取了NSTimer的invalidate方法时,NSTimer才会释放target和userInfo。生成timer的方法中如果repeats参数为NO,则定时器触发后会自动调取invalidate方法。如果repeats参数为YES,则需要手动调取invalidate方法才能释放timer对target和userIfo的强引用。

GCD定时器

//1.创建GCD中的定时器

  /*

   第一个参数:source的类型,DISPATCH_SOURCE_TYPE_TIMER 表示定时器

   第二个参数:描述信息,如线程ID

   第三个参数:更详细的描述信息

   第四个参数:队列,决定GCD定时器的任务在哪个线程中执行

   */

  dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));

  

  

  //2.设定定时器(起始时间|间隔时间|精准度)

  /*

   第一个参数:定时器

   第二个参数:起始时间

   第三个参数:间隔时间

   第四个参数:精准度

   */

  dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);

  dispatch_source_set_event_handler(timer, ^{

    

  });

  dispatch_resume(timer);

CFRunLoopObserverRef

  是观察者,能够监听RunLoop的状态的改变

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),//即将进入RunLoop
    kCFRunLoopBeforeTimers = (1UL << 1), //即将处理Timer
    kCFRunLoopBeforeSources = (1UL << 2),//即将处理Sources
    kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),//即将从休眠中唤醒
    kCFRunLoopExit = (1UL << 7),//即将退出RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU //活跃中
};

 

-(void)observer{

    //添加RunLoop添加观察者,需要用到CF类

    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {

        //

        NSLog(@"--%lu--",activity);

    });

    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

}

 

1.通知观察者run loop已经启动

2.通知观察者任何即将要开始的定时器

3.通知观察者任何即将启动的非基于端口的源

4.启动任何准备好的非基于端口的源

5.如果基于端口的源准备好并处于等待状态,立即启动;并进入步骤9。

6.通知观察者线程进入休眠

7.将线程置于休眠直到任一下面的事件发生:
7.1某一事件到达基于端口的源
7.2定时器启动
7.3Run loop设置的时间已经超时
7.4run loop被显式唤醒

8.通知观察者线程将被唤醒。

9.处理未处理的事件
9.1如果用户定义的定时器启动,处理定时器事件并重启run loop。进入步骤2
9.2如果输入源启动,传递相应的消息
9.3如果run loop被显式唤醒而且时间还没超时,重启run loop。进入步骤2

10.通知观察者run loop结束。

 
 
什么时候会用一个run loop
开启一个常驻线程(让一个子线程不进入消亡状态,等待其他线程发来消息,处理其他事件):在子线程中开启定时器;在子线程中进行一些长期的监控
可以控制定时器在特定的模式下执行
可以让某些事件(行为\任务)在特定模式下执行
可以添加observe监听runloop的状态,比如监听点击事件的处理(在所有点击事件之前做一些处理)

1.使用端口或者自定义的输入源和其他线程通信

这种类型的没有遇到过,文档后面有一些相应地代码,但是感觉比较复杂。不知道什么时候用这种场景,以后用到了补充。

2.线程上使用定时器

一般情况下,自己使用定时器都是在主线程上,主线程的runloop默认是开启的。定时器提供了一个可以直接将定时器添加到当前线程的run loop的NSDefaultRunLoopMode构造方法,所以很多时候什么都不需要做,就能正常使用。但是如果在非主线程上添加定时器就需要手动开启run loop了。如果不开启的话和上面的代码没有什么区别,线程执行完startNewThread的方法后就退出了,会导致周期性执行定时器任务根本实现不了。那如果我在startNewThread方法里面加上一个do {;} while(1);虽然虽然能防止线程退出,但是线程会进入死循环同样无法执行定时器任务。所以run loop开启的意义可以使得和线程有更多的交互,让线程在忙碌的时候忙碌,不忙碌的时候休眠。对比主线程是一样的,如果主线程有任务主线程会执行任务,没有任务的话就不耗费资源。但是并没有退出(否则app就退出了);

 - (void)viewDidLoad {
      [super viewDidLoad];
      MyThread * thread = [[MyThread alloc]initWithTarget:self       
       selector:@selector(startNewThread) object:nil];
      [thread start];
    }
-(void)startNewThread{
//获取当前线程的runloop对象
    NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
    //结构体
    CFRunLoopObserverContext  context = {0, (__bridge void *)(self), NULL, NULL, NULL};
    //创建runloop观察者,绑定run loop。
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                    kCFRunLoopAllActivities, YES, 0,myRunLoopObserver, &context);
    if (observer)
    {
        CFRunLoopRef cfLoop = [myRunLoop getCFRunLoop];//获取Core Foundation形式的runloop引用
        //为run loop的默认的模式添加观察者
        CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
    }
    // Create and schedule the timer.
    [NSTimer scheduledTimerWithTimeInterval:1 target:self
                                   selector:@selector(doFireTimer:) userInfo:nil repeats:YES];
   // NSTimer * timer = [NSTimer timerWithTimeInterval:0.1 repeats:YES block:^(NSTimer * _Nonnull timer) {
    
   //     NSLog(@"纯净的定时器");
    //}];
    //[myRunLoop addTimer:timer forMode:NSDefaultRunLoopMode];
        [myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
}
上面的代码在辅助线程中,开启了run loop。并为run loop添加了观察者和定时器源,循环里面让run loop运行3秒钟。[myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];。RunLoop运行3秒,然后线程退出,在这3秒期间,正确的在该线程上处理了定时器的事件,并且观察者在回调函数中观察到了run loop运行过程需要通知给观察者的状态信息。该案例中如果没有为run loop添加定时器和观察者,线程会如文档描述的那样立刻退出。

3. 在cocoa应用中使用任意一个performSelector…方法

比较常见的是performSelectorOnMainThread,非主线程中不能更新UI,开启额外线程进行网络请求后有时可以调用这个方法在主线程中更新UI。因为主线程中runloop已经开启了,所以很自然就成功了。但是如果在非主线程执行某个方法。就需要开启线程的runloop了。系统提供了一系列类似的方法。

performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes
 

执行特定的selector在主线程的下一个run loop回路。这两个方法给你提供了选项来阻断当前线程直到selector被执行完毕。

performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
 

执行特定的selector在任意线程上,这些线程通过NSThread对象表示。同样提供了阻断当前线程直到selector被执行。

performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:

在当前线程上下一个run loop回路中执行selector,并附加了延迟选项。因为它等待下一个run loop回路到来才执行selector,这些方法从当前执行的代码中提供了一个自动的微小延迟。多个排队的selector会按照顺序一个一个的执行。

cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:

让你取消一个通过performSelector:withObject:afterDelay: or performSelector:withObject:afterDelay:inModes: method方法发送到当前线程的消息。

这段代码让图片在默认的模式下加载,如果用户在进行滑动操作,不会进入这个模式,可以解决部分滑动卡顿问题

[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];

4.使得线程不被杀死去做周期性任务

可以根据业务需求,设置runloop运行时间。可以类似于上面的案例。周期性的在新线程上执行runmethod方法。

下面对应了3和4两种情况.

- (void)viewDidLoad {
    [super viewDidLoad];
    MyThread * thread = [[MyThread alloc]initWithTarget:self selector:@selector(longrun) object:nil];
    [thread start];
    [NSTimer scheduledTimerWithTimeInterval:0.5 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [self performSelector:@selector(runmethod) onThread:thread withObject:nil waitUntilDone:YES modes:@[NSDefaultRunLoopMode]];
    }];
}
-(void)longrun{
    NSRunLoop * runLoop = [NSRunLoop currentRunLoop];
   //为了防止runloop退出,添加一个端口。
    [runLoop addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode];
    [runLoop runUntilDate:[NSDate distantFuture]];
}
-(void)runmethod
{
    NSLog(@"%@ 辅助线程上执行的代码",[NSThread currentThread]);
}
 

如果需要某个线程一直执行,等待某个条件具备时触发可以参考苹果提供的案例

- (void)skeletonThreadMain
{
    // Set up an autorelease pool here if not using garbage collection.
    BOOL done = NO;
    // Add your sources or timers to the run loop and do any other setup.
    do
    {
        // Start the run loop but return after each source is handled.
        SInt32    result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
        // If a source explicitly stopped the run loop, or if there are no
        // sources or timers, go ahead and exit.
        if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
            done = YES;
        // Check for any other exit conditions here and set the
        // done variable as needed.
    }
    while (!done);
     
    // Clean up code here. Be sure to release any allocated autorelease pools.
}

CFRunLoopRunInMode在这里表示在kCFRunLoopDefaultMode模式下启动,时间长度为10秒,如果源的事件被处理runloop应该退出,而不是非要等到10秒耗尽。返回结果如下

  • 时间耗尽(kCFRunLoopRunTimedOut)

  • 被 CFRunLoopStop函数调用导致runloop(kCFRunLoopRunStopped)

  • 源事件被处理了(kCFRunLoopRunHandledSource)

  • 没有了任何源或定时器(kCFRunLoopRunFinished)

代码设定了一个标志位。并且开启runloop事件为10秒,当然也可以设定为其他的值。在每个源得到处理后返回结果,根据结果,状态设定标志位状态。最后可以进行一些线程退出前的操作。在这里可以依然像上面那些方法一样为runloop添加定时器和其他输入源和观察者。

 
 


自动释放池的生命周期:
第一次创建:启动runloop
最后一次销毁:runloop退出的时候
其他时候的创建和销毁:当runloop即将休眠时,销毁之前的释放池,重新创建一个新的