runloop详解

1.什么是RunLoop
RunLoop的字面意思是运行循环,是在程序在运行过程中保持循环做一些事情,也就是保持程序的持续运行。每条线程都有唯一的一个与之对应的runloop对象,主线程的Runloop已经自己创建好,子线程的runloop需要主动创建。RunLoop在第一次获取时创建,在线程结束时销毁。主线程的runloop是默认开启的,iOS应用程序里面,程序启动后会调用main函数,main函数会调用UIApplicationMain()函数,这个方法的主线程会设置一个runloop对象。

2.应用范畴
定时器
PerformSelector
GCD ASYN MAIN QUEUE
事件响应、手势识别、界面刷新
网络请求
自动释放池

3.RunLoop的主要作用
保持程序的持续运行;
处理App中的各种事件(比如:触摸事件、定时器事件、Selector事件)
节省CPU资源,提高程序性能:该做事时做事,该休息时休息

4.RunLoop的获取

@property (class, readonly, strong) NSRunLoop *currentRunLoop;
@property (class, readonly, strong) NSRunLoop *mainRunLoop API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

5.RunLoop与线程的关系
每条线程都有唯一一个与之对应的RunLoop对象
RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
RunLoop会在线程结束时自动销毁
主线程的RunLoop已经默认获取并开启,子线程是 默认没有开启RunLoop

6.RunLoop的模式
NSDefaultRunLoopMode 默认状态,空间状态,默认主线程在此mode下
UITrackingRunLoopMode 滑动ScrollView
UIInitializationRunLoopMode 私有,App启动时使用,启动后就不在使用
NSRunLoopCommonModes 这是一个占位用的mode,作为标记defalt和tracking的mode用
GSEventReceiveRunLoopMode 接受系统事件的内部Mode,通常用不到

7.RunLoop相关的类
CFRunLoopRef:runloop
CFRunLoopModeRef:runloop的运行模式
CFRunLoopSourceRef:事件产生的地方
CFRunLoopTimerRef:基于事件的触发器
CGRunLoopObserverRef:观察者,每个Observer都包含了一个回调,当RunLoop的状态发生变化时,观察者就能通过回调接收到这个变化

这五个类的关系如下图:


image.png

RunLoop启动时只能选择一个Mode,作为currentMode,如果需要切换mode,需要退出当前的RunLoop,重新选择一个mode进入。如果RunLoop没有任何的sources0、source1、timer、observer,那么RunLoop会马上退出。

sources0:包含触摸事件处理、performSelector:OnThread

sources1:基于Port端口的线程间通信、系统事件的捕捉(触摸事件是由source1捕捉,包装成事件由source0来处理)

timer:NSTimer、performSelector:withObject:afterDelay:

observer:用于监听RunLoop状态、UI刷新(beforwaiting)、自动释放池(beforewaiting)

8.CGRunLoopObserverRef相关的状态

观测的时间点有这几个:

kCFRunLoopEntry         = (1UL << 0),           即将进入Loop
kCFRunLoopBeforeTimers  = (1UL << 1),     即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2),   即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5),    即将进入休眠
kCFRunLoopAfterWaiting  = (1UL << 6),      刚从休眠中唤醒
kCFRunLoopExit          = (1UL << 7),             即将退出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU    监听全部状态改变  

监听runloop的observer的状态如下:

#import "ViewController.h"
 
@interface ViewController ()
 
@end
 
@implementation ViewController
 
void observeRunLoopActicities(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    switch (activity) {
        case kCFRunLoopEntry:
            NSLog(@"kCFRunLoopEntry");
            break;
        case kCFRunLoopBeforeTimers:
            NSLog(@"kCFRunLoopBeforeTimers");
            break;
        case kCFRunLoopBeforeSources:
            NSLog(@"kCFRunLoopBeforeSources");
            break;
        case kCFRunLoopBeforeWaiting:
            NSLog(@"kCFRunLoopBeforeWaiting");
            break;
        case kCFRunLoopAfterWaiting:
            NSLog(@"kCFRunLoopAfterWaiting");
            break;
        case kCFRunLoopExit:
            NSLog(@"kCFRunLoopExit");
            break;
        default:
            break;
    }
}
 
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 创建Observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observeRunLoopActicities, NULL);
    // 添加Observer到RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    // 释放
    CFRelease(observer);
}
 
@end

通过上面的代码,可以了解runloop模式的切换,以及可以添加定时器查看模式的切换。

9.RunLoop的运行逻辑

image.png

注意:GCD很多时候是不依赖与RunLoop来实现的,当在子线程做耗时操作,在主线程刷新时,才会依赖于RunLoop来实现。

查看源码的大概逻辑如下:

image.png

当runloop休眠的时候,是从用户态切换到了内核态,当有消息唤醒时,就从内核态再切换到用户态中。

10.RunLoop与NSTimer的关系
NSTimer创建之后,需要添加到Runloop中才会工作,RunLoop的主线程默认是默认的模式,而子线程的RunLoop是默认没有开启的。


image.png
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo

这个方法内部会自动创建定时器对象添加到当前的runloop,并且制定运行模式为默认的,
如果想修改运行模式,可修改成:

//如果想利用上面的那种方法,修改运行模式,可修改成如下
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

如果定时器添加到主线程中,则不需要开始runloop,定时器就可以工作,但是如果,直接添加到子线程中,需要手动开启:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
 
    [self performSelectorInBackground:@selector(RunLoopModeAndTimer) withObject:nil];
 
}
 
- (void)RunLoopModeAndTimer {
 
   //该方法内部会自动创建的定时器对象添加到当前的runloop,并且指定运行模式为默认
   [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
   [[NSRunLoop currentRunLoop] run];  //如果不加这一步,点击界面时,定时器是不工作的,因为定时器对象添加到当前的runloop中,而当前的runloop在子线程中;子线程中的runloop需要手动创建,所以此时定时器不工作。
}
 
- (void)run {
    NSLog(@"run----%@", [NSRunLoop currentRunLoop].currentMode);
}

11.RunLoop的线程保活(常驻线程)
正如上文所说,runLoop与线程是一一对应的关系,如果线程结束,而且RunLoop没有任何的sources0、source1、timer、observer,那么RunLoop会马上退出;如果想让RunLoop不要立马退出,那么就需要添加事件源,RunLoop本身提供了添加事件源的接口,如下:

- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;
 
- (void)addPort:(NSPort *)aPort forMode:(NSRunLoopMode)mode;
- (void)removePort:(NSPort *)aPort forMode:(NSRunLoopMode)mode;

一般情况下,我们会选择 - (void)addPort:(NSPort *)aPort forMode:(NSRunLoopMode)mode; 这种方法来为线程保活。如果我们在子线程开启一个RunLoop,那么我们不应该调用runloop的run方法,这种开发runloop的方法是无法停止runloop的。

11.1 三种启动RunLoop的方式
通过[NSRunLoop currentRunLoop]或者CFRunLoopGetCurrent()可以获取当前线程的runloop。
启动一个runloop有以下三种方法:

- (void)run;  
 
- (void)runUntilDate:(NSDate *)limitDate;
 
- (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

这三种方式无论通过哪一种方式启动runloop,如果没有一个输入源或者timer附加于runloop上,runloop就会立刻退出。

(1) 第一种方式,runloop会一直运行下去,在此期间会处理来自输入源的数据,并且会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:方法;

(2) 第二种方式,可以设置超时时间,在超时时间到达之前,runloop会一直运行,在此期间runloop会处理来自输入源的数据,并且也会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:方法;

(3) 第三种方式,runloop会运行一次,超时时间到达或者第一个input source被处理,则runloop就会退出。

前两种启动方式会重复调用runMode:beforeDate:方法。

11.2 退出RunLoop的方式
第一种启动方式的退出方法

文档说,如果想退出runloop,不应该使用第一种启动方式来启动runloop。

如果runloop没有input sources或者附加的timer,runloop就会退出。
虽然这样可以将runloop退出,但是苹果并不建议我们这么做,因为系统内部有可能会在当前线程的runloop中添加一些输入源,所以通过手动移除input source或者timer这种方式,并不能保证runloop一定会退出。

第二种启动方式runUntilDate:

可以通过设置超时时间来退出runloop。

第三种启动方式runMode:beforeDate:

通过这种方式启动,runloop会运行一次,当超时时间到达或者第一个输入源被处理,runloop就会退出。

如果我们想控制runloop的退出时机,而不是在处理完一个输入源事件之后就退出,那么就要重复调用runMode:beforeDate:,

具体可以参考苹果文档给出的方案,如下:

 NSRunLoop *myLoop  = [NSRunLoop currentRunLoop];
 myPort = (NSMachPort *)[NSMachPort port];
 [myLoop addPort:_port forMode:NSDefaultRunLoopMode];
 
BOOL isLoopRunning = YES; // global
 
while (isLoopRunning && [myLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);



//关闭runloop的地方
- (void)quitLoop
 {
    isLoopRunning = NO;
    CFRunLoopStop(CFRunLoopGetCurrent());
}

11.3 总之

如果不想退出runloop可以使用第一种方式启动runloop;
使用第二种方式启动runloop,可以通过设置超时时间来退出;
使用第三种方式启动runloop,可以通过设置超时时间或者使用CFRunLoopStop方法来退出。

11.4 代码演示


#import "ViewController.h"
#import "YZThread.h"
 
@interface ViewController ()
 
@property (nonatomic, strong) YZThread *thread;
@property (nonatomic, assign) BOOL isStop;
 
@end
 
@implementation ViewController
 
- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak typeof(self) weakSelf = self;
    self.thread = [[YZThread alloc] initWithBlock:^{
        NSLog(@"begin----");
        [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
        
        while (!weakSelf.isStop){
            NSLog(@"------");
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        NSLog(@"end----");
    }];
    [self.thread start];
    
}
 
- (IBAction)gotoView:(id)sender {
    ViewController *vc = [[ViewController alloc] init];
    [self.navigationController pushViewController:vc animated:YES];
}
 
 
 
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self performSelector:@selector(test2) onThread:self.thread withObject:nil waitUntilDone:NO];
}
 
- (void)test2 {
    NSLog(@"打印线程 - %@", [NSThread currentThread]);
}
 
- (void)stop {
    self.isStop = YES;
    CFRunLoopStop(CFRunLoopGetCurrent());
}
 
- (void)willMoveToParentViewController:(UIViewController *)parent {
    if (parent == nil) {
        [self performSelector:@selector(stop) onThread:self.thread withObject:nil waitUntilDone:YES];
    }
}
 
- (void)dealloc {
    NSLog(@"viewcontroller --- dealloc");
}
 
@end

需要注意的是,如果在线程中开启了一个runloop那么就需要查看是否释放内存了。这种方式容易造成内存泄漏或者循环引用,内存一直不释放。

另外,开启runloop时,如果选择NSRunLoopCommonModes 这种模式,那么runloop就是一直在工作,而且程序会卡死!!!只能选中一种模式进入!!!
————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/lyz0925/article/details/103818848

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 不得不说,人的惰性是真可怕啊。从上周六就到写runLoop的建议开始,星期三告诉自己从星期四开始着手写这篇博客。然...
    老司机Wicky阅读 7,199评论 20 137
  • 本文的相关题目,都是查阅网上资料,如有疑问,欢迎讨论! 1、如果页面 A 跳转到 页面 B,A 的 viewDid...
    sy随缘阅读 800评论 0 1
  • RunLoop 是 iOS 和 OSX 开发中非常基础的一个概念,这篇文章将从 CFRunLoop 的源码入手,介...
    钵_Right阅读 830评论 0 1
  • 写在前面 本文仅是自己学习RunLoop的一个记录,参考了ibireme大神的 深入理解RunLoop[https...
    苏东没有坡阅读 7,674评论 0 8
  • 前言,文章是转载的,因为之前收藏的,今天突然发现没了。不知道什么原因,简书搜索不到了,有几篇同样转载的,但是代码没...
    安静就好_阅读 2,671评论 6 42