RunLoop学习笔记

一、什么是 RunLoop

  1. 从字面上来说,RunLoop 可以被翻译为跑圈,但通常状况下我们都称之为运行循环或者消息循环
  2. RunLoop 对我们开发者来说有什么意义呢?
    1.)RunLoop 可以让程序运行不退出我们所写的程序之所以可以一直运行,而不是像程序中的方法一样,运行完就结束,就是 RunLoop 的功劳,所以 RunLoop 也可以被理解为一个死循环(下面会详细解释)
    2.)Runloop 可以让程序等待事件的输入
    3.)没有事件的时候,使线程休眠,一旦有事件输入,就唤醒线程去处理

二、初探 RunLoop
打个招呼

现阶段,我们在平时的开发中,与 RunLoop 接触最多的地方,大概就是 NSTimer 了。

NSTimer *timer = [NSTimer timerWithTimeInterval:1
                                        repeats:YES
                                        block:^(NSTimer * _Nonnull timer) {
                                                  NSLog(@"定时器正在运转");
                                              }];
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];

那么,这个 RunLoop 具体又是什么呢?我们可以打印一下
NSLog(@"%@",[NSRunLoop currentRunLoop]

Snip20170106_5.png

我的天啊,怎么打印出这么一堆东西,那些 Sources/Observers/Timers 究竟是什么,OMG~~~~
然而你会发现此时的你并不清楚这些是什么,没关系,我们会慢慢解释

又见面了

前面说到我们打印了 RunLoop ,结果打印出许多难以理解的东西,现在我们就来研究一下,这些让人头晕的东西究竟是什么

打开 RunLoop 的头文件,我们看到了下面这些

Snip20170106_6.png

对照着前面的打印结果再看,这下是不是感觉好点了?我们再往下看

头文件的描述是不是看的你依旧有些晕晕的?没关系,我们继续

根据官方文档中的描述,与 RunLoop 有关的类有五个(即上图中显示的五个类)
那么这五个类又是做什么的呢,我们再看一张图

3F75B370-6F1D-44E3-B595-3568DC6E0790.png

一个 RunLoop 可以有多个 Mode (模式),每个模式又包含若干个 Source/Observer/Timer
这里请注意上图中Source/Observer/Timer 的类型,除却 Source 是 NSSet 类型,Observer 及 Timer 都是 NSArray 类型的,这说明一个 Mode 中实际上是包含了许多的 Source/Observer/Timer

RunLoop 一次只能运行一种 Mode ,这个 Mode 被称为CurrentMode ,
切换 Mode 需要退出当前 Mode ,这样做是为了避免不同组的 Source/Observer/Timer 混淆

这里需要注意的是Runloop一次只能运行一个mode,并不与上述的一个RunLoop 可以有多个Mode 冲突,下面会具体解释
那么 Source/Observer/Timer/Mode 究竟又是什么呢?

在研究这个问题的之前,我们再来看看一些基础的东西
我们已经知道,程序之所以可以不退出,在没有事件需要处理的时候会进入睡眠状态,等待接收事件,那么这是为什么呢?

实际上,这是因为在主线程中有一个RunLoop 一直在运行,并且这个 RunLoop 是系统创建的,如果我们想要拿到这个 RunLoop ,只需要在主线程中调用 方法 currentRunLoop 就可以了

Snip20170106_7.png

主线程的 RunLoop 是在什么时候创建的呢?我们可以探究一下

当我们创建一个干净项目的时候,项目里除了基础文件,别的什么也不会有,当然除了 Main.m 文件中,那么猫腻会不会就出现在这里呢?

//新建项目中,唯一一个显得不是那么干净的方法
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

我们对代码进行如下测试

int main(int argc, char * argv[]) {
            NSLog(@"000 - 我要出来了");
            @autoreleasepool {
                return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
                NSLog(@"111 -- 你猜我能出来吗");
            }
            NSLog(@"222 -- 你猜我能出来吗");
        }
Snip20170106_9.png

最终只有第一句打印了出来,所以我们不得不猜测是不是因为 main 函数中的方法是一个死循环,所以后面的代码不会执行

当然猜测并没有用,我们还是需要找到证据,打开 RunLoop 的源码,找到 RunLoop 的实现方法 (Apple的部分源文件已经开源,这里附上下载地址:https://opensource.apple.com/release/os-x-1010.html 进入页面下载 CF-1151.16 即可

Snip20170106_10.png

由于代码过长,我们对代码进行了折叠,这段三百多行的代码中,有我们 RunLoop 会一直运行的原因

Snip20170106_12.png

这段代码中,我同样折叠了一部分代码,然而我们可以很清楚的看到一个 do while 循环,这就是我们的 RunLoop 可以循环的原因。
原先我一直认为这段代码就是RunLoop中最关键的代码,直到跟一位前辈的进行交流时才发现我对RunLoop的理解有一部分是不正确的。正真的核心代码其实是这一段。

do {
            if (kCFUseCollectableAllocator) {
                // objc_clear_stack(0);
                // <rdar://problem/16393959>
                memset(msg_buffer, 0, sizeof(msg_buffer));
            }
            msg = (mach_msg_header_t *)msg_buffer;
            
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
            
            if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
                // Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
                while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
                if (rlm->_timerFired) {
                    // Leave livePort as the queue port, and service timers below
                    rlm->_timerFired = false;
                    break;
                } else {
                    if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
                }
            } else {
                // Go ahead and leave the inner loop.
                break;
            }
        } while (1);

我们需要清楚的一点是,一个 RunLoop 对应一个线程,这一点我们同样可以去 RunLoop 的源文件中看到

iOS 中有两套API访问 Foundation(NSRunLoop), CoreFoundation (CFRunLoopRef)
使用 NSRunLoop ,我们可以通过

//使用 NSRunloop ,我们可以通过下面两个方法分别拿到当前线程以及主线程的 Runloop 
[NSRunLoop mainRunLoop];
[NSRunLoop currentRunLoop];
//使用 CFRunLoopRef ,我们可以通过下面两个方法分别拿到当前线程以及主线程的 Runloop CFRunLoopGetMain();
CFRunLoopGetCurrent();

打开 CFRunLoopRef 的源文件,搜索 CFRunLoopGetCurrent,找到 CFRunLoopGetCurrent 的函数方法

Snip20170106_8.png

我们可以看到这个方法最终返回的是一个函数方法的结果,搜索这个函数方法,我们去看看这个方法究竟是何方神圣

// should only be called by Foundation
        // t==0 is a synonym for "main thread" that always works
        CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
            if (pthread_equal(t, kNilPthreadT)) {
                t = pthread_main_thread_np();
            }
            __CFLock(&loopsLock);
            if (!__CFRunLoops) {
                __CFUnlock(&loopsLock);
                CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
                CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
                CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
                if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
                    CFRelease(dict);
                }
                CFRelease(mainLoop);
                __CFLock(&loopsLock);
            }
            CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
            __CFUnlock(&loopsLock);
            if (!loop) {
                CFRunLoopRef newLoop = __CFRunLoopCreate(t);
                __CFLock(&loopsLock);
                loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
                if (!loop) {
                    CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
                    loop = newLoop;
                }
                // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
                __CFUnlock(&loopsLock);
                CFRelease(newLoop);
            }
            if (pthread_equal(t, pthread_self())) {
                _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
                if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
                    _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
                }
            }
            return loop;
        }

可以很清楚的看见,这个函数实际上就是创建 RunLoop 的方法,并且需要传入一个线程

这会儿,大家应该清醒点了吧
那就让你们的视线集中到下面这张图上

03AAAA02-5AAE-4F07-8FA8-E256EDE401F8.png

这张图是官方文档中的,很直观的描述了 RunLoop
一个线程中有一个 RunLoop ,接收到事件之后会对事件进行操作,没有事件的时候会进入睡眠状态。

接收事件的类型分为两类
输入源:输入源可以是键盘鼠标,NSPort, NSConnection 等对象
定时源:NSTimer事件

现在我们真的要去了解一下 Source/Observer/Timer/Mode 了

CFRunLoopSourceRef
按照官方文档,Source分类(理论性的分类,了解就好)
Port - Based Sources
Custom Input Sources
Cocoa Perform Selector Sources

按照函数调用栈,Source分类(实际应用中的)
Source0: 非基于Port
Source1: 基于Port,通过内核和其他线程通信,接收、分发系统事件

通过函数调用堆栈进一步了解 Source


Snip20170106_13.png

函数调用堆栈是倒过来看的,在途中我们可以清楚的看见标红的位置就是 Source0 事件,它在 RunLoop 运行后被执行,虽然我们没有看见 Source1 ,但这并不代表它没有,当有事件产生式,系统会将事件包装由 Source1 分发给 Source0 处理

CFRunLoopObserverRef
它是一个观察者,用来监听RunLoop状态的改变,可以监听以下几个状态

/* Run Loop Observer Activities */typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {    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 的状态改变

CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
            NSLog(@"---RunLoop状态发生变化----%lu",activity);
        });
        CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);
        //注意,这里需要对 observer 进行 release
        CFRelease(observer
                  );

注意内存管理,凡是带有Create、Copy、Retain等字眼的函数,创建出来的对象,都需要在最后做一次Release
打印出来的结果如下,我们可以通过 Observer 对程序的运行进行拦截,让程序在我们需要的时候做我们想做的事情


AB9D90E6-D6C8-4925-8674-59BCBEF715D3.png

这里用一张图片让大家更直观的去看到 Observer 的状态改变


C281FECD-0901-4116-9911-F84B5A2BC546.png

CFRunLoopTimerRef

NSTimer *timer = [NSTimer timerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"定时器正在运转");
        }];
        
        //定时器只在 NSDefaultRunLoopMode 的模式下运行,一旦切换到其他模式,将不再运行
        //可以使用UIScorllView及其子类来验证,这里为了方便,我们选取用UITextFiled来检验
        [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
        
        //定时器被标记为 NSRunLoopCommonModes ,切换到其他模式也可以运行
        //具体为什么,在 CFRunLoopModeRef 中我们再进行了解
        [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];

下图是使用UITextFiled检验定时器运行的结果


Snip20170106_15.png

图中标注位置,有明显时间间隔,这段时间,是对 UITextFiled 进行了拖动,定时器停止运行,一旦松开,定时器又再度运行

CFRunLoopModeRef
CFRunLoopModeRef 类并没有对外暴露,只是通过CFRunLoopRef 的接口进行了封装
系统默认注册了5个Mode
kCFRunLoopDefaultMode: App的默认Mode,通常主线程是在这个Mode下运行
UITrackingRunLoopMode: 界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他的Mode影响
UIInitializationRunLoopMode: 在刚启动App是进入的第一个Mode,启动完成后就不再使用
GSEventReceiveRunLoopMode: 接受系统时间的内部Mode,通常用不到
kCFRunLoopCommonMode: 这是一个占位用的Mode,不是一种真正的Mode

                        上文添加定时器时,给RunLoop指定为此Mode,只是起到一个标记作用,实际上是给让RunLoop拥有两种模式,即kCFRunLoopDefaultMode、UITrackingRunLoopMode,所以定时器才可以在两种模式下都能运行

最后附上孙源大神,15年博客中有关RunLoop的面试题及答案
[※※※]runloop和线程有什么关系?

总的说来,Run loop,正如其名,loop表示某种循环,和run放在一起就表示一直在运行着的循环。实际上,run loop和线程是紧密相连的,可以这样说run loop是为了线程而生,没有线程,它就没有存在的必要。Run loops是线程的基础架构部分, Cocoa 和 CoreFundation 都提供了 run loop 对象方便配置和管理线程的 run loop (以下都以 Cocoa 为例)。每个线程,包括程序的主线程( main thread )都有与之相应的 run loop 对象。

runloop 和线程的关系:
主线程的run loop默认是启动的。

iOS的应用程序里面,程序启动后会有一个如下的main()函数

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

重点是UIApplicationMain()函数,这个方法会为main thread设置一个NSRunLoop对象,这就解释了:为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。

对其它线程来说,run loop默认是没有启动的,如果你需要更多的线程交互则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。

在任何一个 Cocoa 程序的线程中,都可以通过以下代码来获取到当前线程的 run loop 。

NSRunLoop *runloop = [NSRunLoop currentRunLoop];

参考链接:《Objective-C之run loop详解》。

[※※※]runloop的mode作用是什么?

model 主要是用来指定事件在运行循环中的优先级的,分为:

NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态

UITrackingRunLoopMode:ScrollView滑动时

UIInitializationRunLoopMode:启动时

NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合

苹果公开提供的 Mode 有两个:

NSDefaultRunLoopMode(kCFRunLoopDefaultMode)

NSRunLoopCommonModes(kCFRunLoopCommonModes)

[※※※※]以+ scheduledTimerWithTimeInterval...
的方式触发的timer,在滑动页面上的列表时,timer会暂定回调,为什么?如何解决?

RunLoop只能运行在一种mode下,如果要换mode,当前的loop也需要停下重启成新的。利用这个机制,ScrollView滚动过程中NSDefaultRunLoopMode(kCFRunLoopDefaultMode)的mode会切换到UITrackingRunLoopMode来保证ScrollView的流畅滑动:只能在NSDefaultRunLoopMode模式下处理的事件会影响ScrollView的滑动。

如果我们把一个NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。

同时因为mode还是可定制的,所以:

Timer计时会被scrollView的滑动影响的问题可以通过将timer添加到>NSRunLoopCommonModes(kCFRunLoopCommonModes)来解决。代码如下:

        // http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
        // https://github.com/ChenYilong
        
        //将timer添加到NSDefaultRunLoopMode中
        [NSTimer scheduledTimerWithTimeInterval:1.0
                                         target:self
                                       selector:@selector(timerTick:)
                                       userInfo:nil
                                        repeats:YES];
        //然后再添加到NSRunLoopCommonModes里
        NSTimer *timer = [NSTimer timerWithTimeInterval:1.0
                                                 target:self
                                               selector:@selector(timerTick:)
                                               userInfo:nil
                                                repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

[※※※※※]猜想runloop内部是如何实现的?

一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑 是这样的:

function loop() {
            initialize();
            do {
                var message = get_next_message();
                process_message(message);
            } while (message != quit);
        }

或使用伪代码来展示下:

        // http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
        // https://github.com/ChenYilong
        int main(int argc, char * argv[]) {
            //程序一直运行状态
            while (AppIsRunning) {
                //睡眠状态,等待唤醒事件
                id whoWakesMe = SleepForWakingUp();
                //得到唤醒事件
                id event = GetEvent(whoWakesMe);
                //开始处理事件
                HandleEvent(event);
            }
            return 0;
        }

参考链接:
《深入理解RunLoop》
摘自博文CFRunLoop,原作者是微博@我就叫Sunny怎么了

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341

推荐阅读更多精彩内容