RunLoop知识总结

1.RunLoop简介

1.1 什么是RunLoop

从字面上来讲是运行循环,也能够翻译为跑圈.安全

  • RunLoop本质上是一个对象,这个对象能够保持程序的持续运行而且处理程序中的各类事件(如触摸事件,定时器时间,selector事件).
  • RunLoop没有事情处理时就会使线程进入睡眠状态.这样能够节省CPU资源,提升程序性能.

1.2 RunLoop和线程

RunLoop和线程是息息相关的,咱们都知道线程的做用就是用来执行特定的一个或多个任务,正常状况下,线程执行完当前任务后就会退出,以后若线程又有任务须要执行也没法继续执行了.这时咱们就须要一种方式让线程能不断执行任务,即便当前线程没有任务执行,线程也不会退出,而是等待下一个任务的到来.因此咱们就有了RunLoop.markdown

  1. 每一条线程都有惟一一个与之对应的RunLoop对象.app

  2. 主线程的RunLoop对象系统已经自动帮咱们建立好了,而且只有主线程结束时即程序结束时才会销毁.框架

  3. 子线程的Runloop对象须要咱们主动建立并维护,子线程的Runloop对象在第一次获取时就会建立,销毁则是在子线程结束时. 而且建立出来的runLoop对象默认是不开启的,必须手动开启RunLoop.ide

  4. Runloop并不保证线程安全,咱们只能在当前线程内部操做当前线程的Runloop对象,而不能在当前线程中去操做其余线程的RunLoop对象.函数

    相关代码以下:oop

    NSRunLoop *currentRunLoop = [NSRunloop currentRunloop] //获取当前线程的RunLoop对象,在子线程中调用时若是是第一次获取内部会帮咱们建立RunLoop对象
    [currentRunLoop run];
    
    [NSRunLooop mainRunLoop] //获取主线程的RunLoop对象
    复制代码

1.3 默认状况下主线程的RunLoop原理

咱们在启动一个程序时,系统会自动调用建立项目时自动建立的main.m 的文件.main.m文件以下所示:性能

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

复制代码

其中UIApplicationMain函数中内部帮咱们开启了主线程的RunLoop,这个RunLoop使得程序只要不退出或者崩溃,UIApplicationMain函数就一直不会返回,保持了程序的持续运行.上边的代码中主线程开启RunLoop的过程能够简单理解为如下代码:spa

int main(int argc, char * argv[]) {
	BOOL isRunning = YES;
    do {
    //执行各类任务,处理各类事件
	} while(isRunning);
    
    return 0;
}
复制代码

下图是苹果官方的RunLoop模型图线程

从上图能够看出RunLoop就是线程中的一个循环,RunLoop会在循环中经过 Input sources(输入源) 和 Timer sources(定时源)不断检测是否有事件须要执行.而后对接收到的事件通知线程去处理,而且在没有事件的时候让线程去休息.

2.RunLoop的相关类

iOS为咱们提供了两套API来访问RunLoop, 一套是Foundation框架的NSRunLoop, 一套是Core Foundation框架的CFRunLoop. NSRunloop本质是基于CFRunLoop的oc对象封装,因此咱们在这里就讲解Core Foundation框架下有关RunLoop的五个类.

  1. CFRunLoopRef: 表明RunLoop对象
  2. CFRunLoopModeRef: 表明RunLoop的运行模式
  3. CFRunLoopSourceRef: 就是上面RunLoop模型图中的事件源/输入源
  4. CFRunLoopTimerRef: 就是上面RunLoop模型图中的定时源

5 CFRunLoopObserverRef: 观察者,可以监听RunLoop的状态改变

下面详细讲解几种类的具体含义相互关系. 先来看看一张能表示五个类关系的图:

接着来说解这五个类的相互关系:

一个RunLoop对象(CFRunLoopRef)包含若干个运行模式(CFRunLoopModeRef)。而每一个运行模式下又有若干个输入源(CFRunLoopSourceRef),定时源(CFRunLoopTimerRef),观察者(CFRunLoopObserverRef)

  • 每次RunLoop启动时只能指定其中的一种运行模式, 这个运行模式被称做当前的运行模式(CurrentMode).
  • 在每一个运行模式中知识须要一个输入源或者一个定时源.
  • 若是须要切换运行模式, 必须退出当前RunLoop, 再从新指定一个运行模式进入,
  • 这样作主要是为了区别不一样组以前的Source/Timer/Observer,让其互不影响

下面咱们来详细讲解一下这五个类:

2.1 CFRunLoopRef类

CFRunLoop类是Core Foundation框架下的RunLoop对象类.咱们能够经过如下方式获取RunLoop对象

  • Core Foundation
    • CFRunLoopGetCurrent(); //获取当前线程的RunLoop对象,在子线程中调用时若是是第一次获取内部会帮咱们建立RunLoop对象
    • CFRunLoopGetMain(); //获取主线程的RunLoop对象

2.2 CFRunLoopModeRef

系统默认定义了多种运行模式, 以下:

  1. kCFRunLoopDefaultMode: APP的默认运行模式, 一般主线程就是在这个模式下运行的
  2. UITrackingRunLoopMode: 跟踪用户交互事件(用于ScrollView追踪触摸滑动,保证界面滑动时不受其余Mode影响)
  3. UIInitializationRunLoopMode: 在刚启动APP时进入的第一个Mode,启动完成后就不会再使用
  4. CSEventReceiveRunLoopMode: 接受系统内部事件(用于绘图),一般用不到
  5. kCFRunLoopCommonMode:这是一种占位模式,并非一种真正的运行模式(后边会用到)

其中kCFRunLoopDefaultMode, UITrackingRunLoopMode,kCFRunLoopCommonModes是咱们开发中须要用到的模式.具体使用方法咱们在2.3 CFRunLoopTimerRef中结合CFRunLoopTimerRef来演示说明

2.3 CFRunLoopTimerRef

CFRunLoopTimerRef是定时源, 理解为基于时间的触发器, 基本上就是NSTimer. 下面咱们来演示一下CFRunLoopModeRef和CFRunLoopTimerRef结合的使用方法.

在Main.Storyboard中拖入一个textView. 而后尝试执行如下代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self timer1];
}
- (void)timer1 {
    //1.建立定时器
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    //2.将定时器添加到当前的RunLoop,指定RunLoop的运行模式为默认运行模式
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
    }
    - (void)run {
    NSLog(@"run --- %@ --- %@", [NSThread currentThread], [NSRunLoop currentRunLoop]);
}

复制代码

当程序运行时, run方法每隔两秒就会执行一次, 可是若拖动textView,run方法就不会执行.这是由于什么呢?

咱们建立的timer是加入到RunLoop的NSDefaultRunLoopMode运行模式中, 可是当咱们拖动textView,当前RunLoop会退出当前运行模式,并进入到UITrackingRunLoopMode运行模式,咱们建立的timer并无添加到并到UITrackingRunLoopMode运行模式中,因此run方法就不会执行.

那么有什么解决方法呢?

  • 解决方法一:

    把timer也添加到UITrackingRunLoopMode运行模式中.这样就能够在两种运行模式下都执行run方法.
    增长代码以下:

    [[NSRunLoop currentRunLoop]addTimer:timer forMode:UITrackingRunLoopMode];
    复制代码
  • 解决方法二:

    把timer加入到kCFRunLoopCommonMode运行模式中.前面2.2中已经提到这种模式其实知识一种占位模式,并非真正的运行模式.如果将timer添加到这个模式中,那么timer会被添加到打上common标签的运行模式中.

    那么那些运行模式会被打上common标签呢?
    NSDefaultRunLoopMode 和 UITrackingRunLoopMode

    因此只要添加到kCFRunLoopCommonMode运行模式也就等价于把timer加入到NSDefaultRunLoopMode和UITrackingRunLoopMode这两种运行模式中.

    将代码替换成以下代码:

    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
    复制代码

    除了上面代码中使用的timer的建立方法,还有一种经常使用的timer建立方法

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

    这种方法建立出来的timer会被默认添加到NSDefaultRunLoopMode运行模式,若想添加到UITrackingRunLoopMode中,只要拿到timer对象而后选择上面的其中一种解决方法便可.

注意点

刚才提到了例子都是在主线程中建立timer并加入到RunLoop中特定的运行模式中,那么要是在子线程中建立timer有什么区别呢?
请尝试执行下面的代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    [NSThread detachNewThreadSelector:@selector(timer2) toTarget:self withObject:nil];
}
- (void)timer2 {

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

}
- (void)run {
    NSLog(@"run --- %@ --- %@", [NSThread currentThread], [NSRunLoop currentRunLoop]);
}
复制代码

你会发现run方法根本不会调用,这是为何呢?

这其实就要和上面提到的runLoop的的建立和管理有关了.

子线程的Runloop对象须要咱们主动建立并维护,子线程的Runloop对象在第一次获取时就会建立,销毁则是在子线程结束时. 而且建立出来的runLoop对象默认是不开启的,必须手动开启RunLoop.

因此咱们应该修改代码为以下:

- (void)timer2 {
    //1.获取RunLoop并建立
   NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
    //2.建立timer
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    //3.启动子线程的RunLoop
    [currentRunLoop run];
}
复制代码

2.4 CFRunLoopSourceRef

CFRunLoopSourceRef是事件源(RunLoop模型图中提到过的)

  • 之前的分法:
    • Port-Based Sources(基于端口的)
    • Custiom Input Sources(自定义)
    • Cocoa Peform Selector Sources(peform selector 方法)
  • 如今的分法:
    • Source0: 非基于Port(端口)的(用户事件)
    • Source1: 基于Port的, 经过内核和其余线程通讯,接收,分发系统事件(系统事件)

第一种是经过官方理论来分的, 第二种是在实际应用中经过调用函数来分的.

下面咱们举个例子经过函数调用栈中的source
1.首先咱们在main.storyboard中拖入一个按钮,并添加动做
2.而后在点击动做中的代码中加入一个输出语句,并打上一个断点

步骤以下:

当咱们运行程序后点击按钮后就会来到此断点,而后咱们就能够查看当前的函数调用栈.

以下图所示:

因此点击事件是这样来的:

  1. 首先程序启动而后运行到18行的main函数,以后在main函数中调用17行的UIApplicationMain函数,而后一直往上调用函数, 最终调用到点击函数.
  2. 咱们能够看到在12行中有CFRunLoopDoSources0,即咱们的点击事件属于sourece0函数的,点击事件就是source0中处理的.
  3. 而至于source1就是用来接收和分发系统的事件,而后再分发到Source0中处理.

2.5 CFRunLoopObserver

CFRunLoopObserver是监听者, 可以监听RunLoop的状态改变.

能够监听的时间点有如下:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry = (1UL << 0),           //即将进入RunLoop
        kCFRunLoopBeforeTimers = (1UL << 1),    //即将处理Timer
        kCFRunLoopBeforeSources = (1UL << 2),   //即将处理Source
        kCFRunLoopBeforeWaiting = (1UL << 5),   //即将进入休眠
        kCFRunLoopAfterWaiting = (1UL << 6),    //刚从休眠中被唤醒
        kCFRunLoopExit = (1UL << 7),  	        //即将退出RunLoop
        kCFRunLoopAllActivities = 0x0FFFFFFFU   //监听全部事件
    };

复制代码

具体使用方法以下:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self observer];
}
- (void)observer {
    /**
     @param1:怎么分配空间(通常传入默认分配方式)
     @param2:要监听的RunLoop的什么状态
     @param3:是否要持续监听
     @param4:优先级 老是传0
     @param5:当状态改变时的回调
     */
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"即将进入RunLoop");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"即将处理Timer");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"即将处理Source");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"即将进入休眠");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"刚从休眠中被唤醒");
                break;
            case kCFRunLoopExit:
                NSLog(@"即将退出RunLoop");
                break;
        
            default:
                break;
        }
    });
    /**
     @param1:要监听的RunLoop对象
     @param2:观察者
     @param3:运行模式
     */
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
}
复制代码

打印台信息以下:

能够看到RunLoop在程序运行后就会处理大量的Source和Timer事件,当没有事情须要作的时候就会进入休眠状态,即让线程休眠,当有事件须要处理时就会唤醒RunLoop再次处理事件,

3. RunLoop原理

五个类都理解完以后咱们就来具体说明RunLoop的运行原理.
其中咱们借助下面这张网友的逻辑图进行说明

结合上面这个逻辑图咱们来讲明一个苹果官方文档给出的RunLoop运行逻辑

具体顺序以下:
首先RunLoop会去检查Mode里是否有source/timer, 没有直接退出

  1. 通知观察者RunLoop已经启动(系统自己就会为咱们添加一个观察者)
  2. 通知观察者即将要处理Timer
  3. 通知观察者即将要处理Sourece0
  4. 启动任何准备好的Source0
  5. 若是Soure1准备好并处于等待状态进入,当即启动,进入步骤9.(source1内部就是由source0和timer组成)
  6. 通知观察者进入休眠状态
  7. 将线程置于休眠状态直到下面任一事件发生
    • 某一事件到达基于端口的源
    • 定时器启动
    • RunLoop设置的时间已经超时
    • RunLoop被外部显示唤醒。
  8. 通知观察者,线程被唤醒
  9. 处理未处理的事件
    • 若是用户定义的定时器启动, 处理定时器事件并从新启动RunLoop,进入步骤2.
    • 若是输入源启动, 传递相应消息。
    • 若是RunLoop被显示唤醒而且时间还没超时,重启RunLoop,进入步骤2
  10. 通知观察者RunLoop结束

4. RunLoop的实战运用

前面都是一些理论知识的讲解,接下来咱们咱们就讲讲在实战中如何使用RunLoop.

4.1 NSTimer的使用

刚刚在前面的2.3中咱们已经讲解了把Timer加入到RunLoop的不一样运行模式的做用和区别.你们若是忘了能够回去再看看如何使用.

4.2 ImageView推迟显示

咱们可能有时会遇到一种状况,就是咱们的界面有tableView,每一个tableView的cell中都有许多图片.而后当咱们滚动tableView,须要显示不少图片,这时候可能就会出现卡顿现象.

那么这时咱们就可使用RunLoop来解决这个问题.具体方法为利用performSelector方法调用UIImageView的setImage:方法,而后指定在RunLoop下的NSDefaultRunLoopMode运行模式.代码以下:

[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"男孩"] afterDelay:5.0 inModes:@[NSDefaultRunLoopMode]];
复制代码

咱们设置显示图片的时间为五秒以后,可是程序运行后咱们拖动textView,发现五秒后图片并无出现,而是当咱们拖动结束时候才显示出来.

这是由于咱们设置显示图片的操做是在RunLoop的NSDefaultRunLoopMode模式中,当咱们拖动textView时,RunLoop会切换到UITrackingRunLoopMode模式,这时即便设定的操做执行时间也不会执行,而是要等到咱们结束完拖动后才会切换回NSDefaultRunLoopMode模式执行设置图片的操做.

注意点

在上面推迟显示图片的程序中,咱们能够发现当咱们切换到UITrackingRunLoopMode中,设定的执行操做的时间并无中止计时,因此当咱们一中止拖动时就会立刻执行操做.

那么咱们要是在RunLoop的NSDefaultRunLoopMode模式下添加了一个timer,拖动textView一段时间后,许多本该执行的操做在中止拖动以后会怎样执行呢.让咱们运行下面代码来看看效果吧.

- (void)viewDidLoad {
    [super viewDidLoad];
    //[self observer];
    NSLog(@"%s", __func__);
    [self test2];
}
- (void)test2 {
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"test2");
    }];
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
    
}
复制代码

效果以下图:


我是在13:02:50进行拖动textView,而后13:03:14结束拖动,能够发现若是当时期间24秒间隔本应该要执行12次打印,最后只执行了两次,并且这两次执行是基本紧接着执行的,期间没有间隔.而后又开始了正常的两秒种执行一次打印.

因此咱们能够得出RunLoop的逻辑,当timer添加到RunLoop的NSDefaultRunLoopMode模式时,在切换到UITrackingRunLoopMode模式后,RunLoop会最多暂存两次操做,而后等到RunLoop切换回NSDefaultRunLoopMode模式下,再紧挨着执行两次操做.

结论:
因此当NSTimer添加到NSDefaultRunLoopMode模式并非绝对精准的,当咱们滚动一些视图时,执行操做就会变得不按时.解决方法就是把timer也添加到UITrackingRunLoopMode模式中,或者使用其余定时器如GCD定时器.

4.3 后台常驻线程

线程有关知识

  • [NSThread detachNewThreadSelector:@selector(run1) toTarget:self withObject:nil]会建立并自动开启一条线程执行任务,不须要手动启动

  • 咱们以前建立线程都是为了执行特定任务,执行问特定任务后,线程会自动进入死亡状态.线程进入死亡状态后,是没法再次启动线程,让线程继续执行任务的.

    若线程进入死亡状态再次调用start方法会报错

利用RunLoop实现后台常驻线程

咱们在作项目时可能会在后台执行频繁操做,在子线程中执行耗时操做(以下载文件,后台播放音乐,后台记录用户信息),那么我最好能让线程不进入死亡状态所以能够持续的执行任务,而不是频繁的建立和销毁线程.

那么咱们应该怎么作呢?

添加一条指向常驻内存的线程强引用,而后在这条线程中建立一个RunLoop,并添加一个Sources,而后开启RunLoop.缘由是RunLoop只要没有超时,任务就会一直执行不完,那么线程就不会进入死亡状态.

具体实现过程:

  1. 首先建立一条子线程并添加要执行的方法
  2. 在执行的方法中开启一个RunLoop,并添加一个Source或Timer,若不添加RunLoop循环会直接退出.通常作法是添加一个port即端口,由于port并不须要指定须要作什么任务,而timer须要指定,咱们这里添加Source或Timer只是为了保证循环不退出,因此不须要指定任务,因此通常选择port.

实现代码以下:

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"%s", __func__);
    [self residentThread];
}

- (void)residentThread {
    NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(run1) object:nil];
    self.thread = thread;
    [self.thread start];
}
- (void)run1 {
    //这里写须要执行的代码
    NSLog(@"run1 -- %@", [NSThread currentThread]);
    //一个RunLoop至少须要一个Source或者Timer,在这里添加一个Source1
    [[NSRunLoop currentRunLoop]addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop]run];
    NSLog(@"未开启RunLoop -- %@", [NSThread currentThread]);
}

复制代码

3.运行后会发现 未开启RunLoop 并不打印,由于RunLoop循环一直没有返回.

为了线程是否还能够继续执行其余任务即没有进入死亡状态,咱们在touchesBegan中调用PerformSelector方法,看看是否会打印.

代码以下:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:nil];
}

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

复制代码

运行代码后点击屏幕,发现能够打印,即线程可以继续执行任务.这样常驻线程就完成了.

5.RunLoop有关知识注意点

1 只有子线程的RunLoop设置退出时间才有用,主线程的RunLoop是没法退出的.即下面这句代码是不会起到使RunLoop退出的做用.

[[NSRunLoop mainRunLoop]runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
复制代码

2 RunLoop何时建立和销毁自动释放池

首先咱们要知道RunLoop为何要建立自动释放池?
由于在一个RunLoop运行循环过程当中会产生大量变量和对象,并且大多数变量是不会再使用的.那么若不清理掉这些不用的变量,内存就可能会被堆满.因此RunLoop会按期建立一个自动释放池,而且在特意时间释放掉释放池,并从新再建立一个.

第一次建立: 启动RunLoop的时候 最后一次销毁: 退出RunLoop以前 其余时候的建立和销毁: 在RunLoop进入休眠状态前会释放掉旧的释放池,释放池中的变量也一块儿被销毁了.而后建立出一个新的释放池,用来存放新产生的不用的变量.

相关文章
相关标签/搜索