程序员

初次接触 RunLoop

最近看了两位大神关于RunLoop的一些资料,对RunLoop算是有了一个初步的认识,在这里整理总结一下自己对于RunLoop的理解。如果我有理解错的地方,希望大家帮我指出。我看的是 @ibireme 的博客和 @我就叫Sunny怎么了 的视频,链接在下面。

深入理解RunLoop

iOS线下分享《RunLoop》by 孙源@sunnyxx

什么是RunLoop


说白了就是一个循环,能够让整个程序一直运行着不会退出,接收用户的信息。当然这个循环是一个十分复杂的循环。

RunLoop与线程的关系


每个线程有一个自己的main函数,当这个main函数执行完后这个线程也就没了。所以我们需要给这个线程创建一个 RunLoop。但是在线程创建的时候并没有 RunLoop ,只有当我们第一次去获取这个线程的 RunLoop 时,才会创建。

RunLoop的结构


每个 RunLoop 中有一个集合其中存放了若干个 Mode,一个 RunLoop 只能在一种 Mode 下运行,如果需要切换那么久要退出 Loop 再重新指定一个 Mode 进入。每个 Mode 中又有若干个 item ,总共有三类,分别是 Source,Timer,Observer。先说简单的

Timer

Timer 就是一个定时器,我们平时使用 NSTimer 与它紧密相关。它包含一个时间长度和一个作为回调的函数指针,当它加入到 RunLoop 后,每次到那个时间点,就会执行一次回调。

Observer

Observer 就是观察者,它也包含一个回调,当 RunLoop 的状态改变时,就会执行这个回调。RunLoop 有以下几种状态:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), 
    kCFRunLoopBeforeTimers  = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2), 
    kCFRunLoopBeforeWaiting = (1UL << 5), 
    kCFRunLoopAfterWaiting  = (1UL << 6), 
    kCFRunLoopExit          = (1UL << 7), 
};

Source

Source就比较难理解了。反正我自己没有弄明白,还是引用一下@ibireme的原话吧。

CFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0 和 Source1。

Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。

Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程

CFRunLoopMode 和 CFRunLoop 的结构

我觉得看了下面这两个结构体后,对于 RunLoop 的理解会明朗很多。

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};

struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
    ...
};

__CFRunLoop 中有一个 _modes 用来存放这个 RunLoop 中的所有 Mode。还有一个 _currentMode 用来标记当前的 Mode。而在 _CFRunLoopMode 中除了 _name 就是那三种 item,应该很容易看出来。

需要一提的是在 __CGRunLoop 中有 CFMutableSetRef _commonModes; CFMutableSetRef _commonModeItems; 这个两个结构。我们可以把一个 Mode 标记为 Common。 这样每次 RunLoop 内容改变时都会把 _commonModeItems 里的 items 全部同步到标记了 Common 的 Mode 中。在后面讲 timer 的例子时我们还会再提到。

RunLoop 的内部逻辑

还是引用一下@ibireme的图片,十分的直观明了。

RunLoop 应用


NSTimer

在我刚刚学 iOS 的时候,一次偶然知道了 NSTimer 这个东西后,高兴的用它模仿了一个系统的计时器。源码在一次整理的时候被我删了。。只能看看截图了:

本来挺开心的,结果一滚动下面的表格,WHAT?!计时器怎么不动了??

后来也查了解决办法,不过一直没搞懂为什么。现在知道了 RunLoop 后总算是明白了。

在主线程中有预置两个 Mode,一个是 Default,还有一个就是 UITrackingRunLoopMode,并且这两个Mode都标记了 Common。这个 Mode 在 ScrollView 滚动时会切换,用来保证滚动的流畅。而我们注册 timer 使用的方法:

 _timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];

会默认把这个 timer 注册到当前 RunLoop 的 DefaultMode 中,那么滚动的时候 Mode 一切换 timer 很显然就没法工作了。解决办法也很容易,可以分为两种:

  • 把timer注册到UITrackingRunLoopMode或者NSRunLoopCommonModes(添加到 CommonModes 中就相当于自动添加到了UITrackingRunLoopMode)。

    // 二选一
    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:UITrackingRunLoopMode];
    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
  • 把timer添加到子线程的 RunLoop 中,不管是什么Mode都不会影响到主线程

    dispatch_queue_t queue = dispatch_queue_create("com.cyrusdev.queue", DISPATCH_QUEUE_CONCURRENT);
     dispatch_async(queue, ^{
         _timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
         [[NSRunLoop currentRunLoop] run]; // 一定要在注册timer之后让 RunLoop 跑起来
     });

我写了一个Demo,地址在这里,可以感受一下:

RunLoopDemo

AutoreleasePool

在 App 启动后,系统会在主线程的 RunLoop 里注册两个 Observer。

第一个 Observer 监听 kCFRunLoopEntry 也就是即将进入 RunLoop 的状态,在这里会创建一个自动释放池。

第二个 Observer 监听 kCFRunLoopBeforeWaiting 以及 kCFRunLoopExit 两个状态。在 RunLoop 即将进入休眠时释放旧池,创建新池。在 RunLoop 要退出时释放池子。

让一个子线程一直运行

- (void)viewDidLoad {
    [super viewDidLoad];
    // 创建子线程
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMain) object:nil];
    [self.thread start];
}

- (void)threadMain {
    [[NSThread currentThread] setName:@"myThread"]; 
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    [runLoop run];
}

我在 myThread 线程中创建了一个 RunLoop,这个线程就不会自动销毁了。我们可以暂停程序看一下它的线程堆栈:

总结


对于 RunLoop 的了解其实还很浅,只能有一个大概的结构,不过了解了这些内容对于整个App运行的过程还是会更加深入一些了。