Wrappres' Studio.

Runloop 的理解

字数统计: 1.9k阅读时长: 7 min
2019/05/09 Share

Runloop

Runloop 是什么

Runloop 实际是一个对象,这个对象一直在循环中来处理 App 运行过程中各种事件(触摸事件,UI 刷新事件,定时器事件,Selector 事件),从而保持程序的持续运行。Runloop 在没有事件需要处理的时候,会使线程进入睡眠模式,从而节省 CPU 资源,提高程序性能。

Runloop 与线程的关系

线程的作用是用来执行一个或多个任务。在默认情况下,线程执行完之后就会退出。如果需要线程能一直不断的处理任务,且并不会退出,这时就需要用到 Runloop。

  1. 一个线程对应一个 Runloop 对象,每条线程都有唯一一个对应的 Runloop 对象。
  2. Runloop 并不保证线程安全,只能在当前线程内部操作当前线程的 Runloop 对象,而不能在当前线程内部去操作其他线程的 Runloop 对象方法。
  3. Runloop 对象在第一次获取 Runloop 时创建,在线程结束时,销毁。
  4. 主线程的 Runloop 对象系统自动创建,其余线程的 Runloop 对象需要主动创建和维护。

主线程的 Runloop

主线程的 Runloop 是默认启动的。iOS App 在启动时,会执行一个 main 函数。

1
2
3
4
5
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

在 UIApplicationMain 内部拥有一个无限循环的代码,只要 App 不退出不崩溃,就一直在循环。

1
2
3
4
5
6
7
8
9
int main(int argc, char * argv[]) {        
BOOL running = YES;
do {
// 执行各种任务,处理各种事件
// ......
} while (running); // 判断是否需要退出

return 0;
}


Runloop 会在循环中会不断检测,通过 Input sources (输入源)和 Timer sources (定时源)两种来源等待接受事件,然后对接受到的事件通知线程进行处理,并且在没有事件时,让线程进行休息。

Runloop 的相关类

  1. CFRunLoopRef: Runloop 对象
  2. CFRunLoopModeRef:Runloop 的运行模式
  3. CFRunLoopSourceRef:Runloop 中的输入源
  4. CFRunLoopTimerRef:Runloop 中的定时源
  5. CFRunLoopObserverRef:观察者,能监听 Runloop 的状态改变

之间的关系


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

  • 每次 Runloop 启动时,只能指定一个运行模式(CFRunLoopModeRef),这个运行模式被称作当前运行模式(CurrentMode)。
  • 如果需要切换运行模式,只能退出当前的循环(Loop),再重新指定一个运行模式进入。
  • 这样做的目的是为了分隔开不同组的输入源(CFRunLoopSourceRef),定时源(CFRunLoopTimerRef),观察者(CFRunLoopObserverRef),让其互不影响。

CFRunLoopRef

  • CFRunLoopGetCurrent();获得当前线程的 RunLoop 对象
  • CFRunLoopGetMain(); 获得主线程的 RunLoop 对象
  • [NSRunLoop currentRunLoop];获得当前线程的 RunLoop 对象
  • [NSRunLoop mainRunLoop];获得主线程的 RunLoop 对象

CFRunLoopModeRef

  1. kCFRunLoopDefaultMode: App 的默认 Mode,通常主线程是在这个 Mode 下运行的。
  2. UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
  3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
  4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
  5. kCFRunLoopCommonModes: 这是一个占位的 Mode,是一个伪模式。

其中 kCFRunLoopDefaultMode。UITrackingRunLoopMode,kCFRunLoopCommonModes 是我们开发时需要用到的运行模式。

CFRunLoopTimerRef

例子:

  1. 新建项目,在 Main.storyboard 中拖入一个 TextView
  2. 在 ViewController 中加入以下代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    - (void)viewDidLoad {
    [super viewDidLoad];

    // 定义一个定时器,约定两秒之后调用self的run方法
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

    // 将定时器添加到当前RunLoop的NSDefaultRunLoopMode下
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    }

    - (void)run
    {
    NSLog(@"---run");
    }
  3. 运行,这时我们不对模拟器进行任何操作,定时器会稳定的每隔 2 秒调用 run 方法打印

  4. 当我们滚动 TextView 时,run 方法不打印了。松开鼠标时,run 方法有开始正常工作
    原因:
  • 当我们不做任何操作时,Runloop 处于 NSDefaultRunLoopMode 下的运行模式
  • 当我们拖动 TextView 时,Runloop 就结束 NSDefaultRunLoopMode,切换到了 UITrackingRunLoopMode 模式,这个 Mode 下没有添加 NSTImer,所有 run 方法不执行了
  • 当我们松开鼠标时,Runloop 就结束 UITrackingRunLoopMode 模式,又切换回 NSDefaultRunLoopMode 模式,所以 NSTimer 又开始正常工作

可以将[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];替换为[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];,则情况相反。
使用[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];可以保证在 NSDefaultRunLoopMode 和 UITrackingRunLoopMode 下都能够正常的执行 run 方法。这里用到的就是 kCFRunLoopCommonModes。

CFRunLoopSourceRef

有两种分类方式
根据官方文档:

  • Port-Based Sources(基于端口)
  • Custom Input Sources(自定义)
  • Cocoa Perform Selector Sources

根据函数调用栈

  • Source0 :非基于 Port
  • Source1:基于 Port,通过内核和其他线程通信,接收、分发系统事件

CFRunLoopObserverRef

监听的状态有下面几种:

1
2
3
4
5
6
7
8
9
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop:1
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer:2
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source:4
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠:32
kCFRunLoopAfterWaiting = (1UL << 6), // 即将从休眠中唤醒:64
kCFRunLoopExit = (1UL << 7), // 即将从Loop中退出:128
kCFRunLoopAllActivities = 0x0FFFFFFFU // 监听全部状态改变
};

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)viewDidLoad {
[super viewDidLoad];

// 创建观察者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"监听到RunLoop发生改变---%zd",activity);
});

// 添加观察者到当前RunLoop中
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

// 释放observer,最后添加完需要释放掉
CFRelease(observer);
}

原理

  1. 通知观察者 RunLoop 已经启动
  2. 通知观察者即将要开始的定时器
  3. 通知观察者任何即将启动的非基于端口的源
  4. 启动任何准备好的非基于端口的源
  5. 如果基于端口的源准备好并处于等待状态,立即启动;并进入步骤 9
  6. 通知观察者线程进入休眠状态
  7. 将线程置于休眠知道任一下面的事件发生:
    • 某一事件到达基于端口的源
    • 定时器启动
    • RunLoop 设置的时间已经超时
    • RunLoop 被显示唤醒
  8. 通知观察者线程将被唤醒
  9. 处理未处理的事件
    • 如果用户定义的定时器启动,处理定时器事件并重启 RunLoop。进入步骤 2
    • 如果输入源启动,传递相应的消息
    • 如果 RunLoop 被显示唤醒而且时间还没超时,重启 RunLoop。进入步骤 2
  10. 通知观察者 RunLoop 结束

应用

NSTimer 的使用

参考 CFRunLoopTimerRef 中的例子

延迟显示 ImageView

当列表中有很多 ImageView 需要显示图片时,就可能会出现卡顿现象。
利用 [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"tupian"] afterDelay:4.0 inModes:NSDefaultRunLoopMode]; 实现在列表滚动时,不显示图片保证流畅性,只在列表静止时,显示图片。

创建后台常驻的线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)viewDidLoad {
[super viewDidLoad];

// 创建线程,并调用run1方法执行任务
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
// 开启线程
[self.thread start];
}

- (void) run1
{
// 这里写任务
NSLog(@"----run1-----");

// 添加下边两句代码,就可以开启RunLoop,之后self.thread就变成了常驻线程,可随时添加任务,并交于RunLoop处理
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];

// 测试是否开启了RunLoop,如果开启RunLoop,则来不了这里,因为RunLoop开启了循环。
NSLog(@"未开启RunLoop");
}
CATALOG
  1. 1. Runloop
    1. 1.1. Runloop 是什么
    2. 1.2. Runloop 与线程的关系
    3. 1.3. 主线程的 Runloop
    4. 1.4. Runloop 的相关类
      1. 1.4.1. 之间的关系
      2. 1.4.2. CFRunLoopRef
      3. 1.4.3. CFRunLoopModeRef
      4. 1.4.4. CFRunLoopTimerRef
      5. 1.4.5. CFRunLoopSourceRef
      6. 1.4.6. CFRunLoopObserverRef
    5. 1.5. 原理
    6. 1.6. 应用
      1. 1.6.1. NSTimer 的使用
      2. 1.6.2. 延迟显示 ImageView
      3. 1.6.3. 创建后台常驻的线程