RunLoop - 知识点总结

RunLoop 初窥:

  • 从字面意思看:运行循环, 跑圈
  • 其实它内部就是一个 do-while 循环, 在这个循环内部不断的处理各种任务(比如 source, timer, observer)

基本作用

  1. 保持程序的持续运行
  2. 处理 APP 中的各种事件(触摸事件, 定时器事件, Selector 事件等)
  3. 节省 CPU 资源, 提高程序性能, 该做事时做事, 该休息时休息

在程序中的体现

  • 在有 RunLoop 的情况下,由于 main 函数里面启动了 RunLoop, 所以程序不会马上退出, 保持持续运行状态
  • main.m 里的 UIApplicationMain 函数内部会启动一个 RunLoop
  • 所以 UIApplicationMain 函数一直没有返回,保持了程序的持续运行
  • 这个默认启动的 RunLoop 是跟主线程相关联的

RunLoop 对象

iOS 中有两套 API 来访问和使用 RunLoop

  • Foundation -- 用 NSRunLoop 访问

  • Core Foundation -- 用 CFRunLoopRef 访问

NSRunLoop 和 CFRunLoopRef 都代表 RunLoop 对象, NSRunLoop是在基于CFRunLoopRef的一层 OC 封装

RunLoop 与线程

  • 每条线程都有唯一的一个与之对应的 RunLoop 对象
  • 主线程的 RunLoop 已经自动创建好了, 子线程的 RunLoop 需要手动去创建
  • RunLoop 在第一次获取时创建,在线程结束的时候销毁

获得 RunLoop 对象

Foundation

    [NSRunLoop currentRunLoop];     // 获得当前线程的 RunLoop 对象
    [NSRunLoop mainRunLoop];     // 获得主线程的 RunLoop 对象

Core Foundation

   CFRunLoopGetCurrent(); // 获得当前线程的 RunLoop 对象
   CFRunLoopGetMain();  // 获得主线程的 RunLoop 对象

在代码中是这样的

// 1. 获得主线程对应的 RunLoop
   NSRunLoop *mainRunLoop = [NSRunLoop mainRunLoop];
   
   // 2. 获得当前线程对应的 RunLoop
   NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
   
   // 两者内存地址是一样的
   NSLog(@"%p - %p", mainRunLoop, currentRunLoop);
   //NSLog(@"%@", mainRunLoop);
   
   // 3. core
   NSLog(@"%p - %p", CFRunLoopGetMain(), CFRunLoopGetCurrent());

通过打印结果我们可以发现, 当前线程的 RunLoop 和主线程的 RunLoop 内存地址是一样的

RunLoop 和线程的关系

两者是一一对应的. 只是主线程的 RunLoop 已经创建, 子线程的 RunLoop 需要手动创建.

[[[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil] start];

实现run 方法

- (void)run {
    // 创建子线程对应的 RunLoop.   currentRunLoop是懒加载方法,当第一次调用时先判断对应的 RunLoop 是否存在,如果不存在,自己创建并返回,如果存在就直接返回
    NSLog(@"%@", [NSRunLoop currentRunLoop]);
    
    NSLog(@"run - %@", [NSThread currentThread]);
}

RunLoop 相关类

Core Foundation 中关于 RunLoop 的5个类

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimeRef
  • CFRunLoopObserverRef

CFRunLoopModeRef

系统默认注册了5个 Mode:

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

我们平时主要用到的是前两种

每个 RunLoop 中可能有两个或者多个 Mode,

mode 元素:

  • Source - 事件源
  • Observer - 观察者
  • Timer - 定时器事件

注意:

  • 虽然 RunLoop 中有多个运行模式, 但是启动之后只能选择一种模式运行 ,这种 Mode 被称作 CurrentMode
  • mode 里至少要有一个 Timer 或者 Source
  • 如果需要切换 Mode, 只能先退出 Loop, 再重新指定一个 Mode 进入. 这样做的好处主要是为了分隔开不同组的 Source/Timer/Observer, 让其互不影响

当 RunLoop 中添加定时器时, 选择不同的 mode 会有不同的效果

NSDefaultRunLoopMode

// 1. 创建定时器
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run2) userInfo:nil repeats:YES];
    
    // 2. 添加到 RunLoop 中, 指定 RunLoop 的运行模式为默认模式
    /*
     第一个参数: 定时器
     第二个参数: RunLoop 的运行模式
     */
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; 

但是这时候有个问题, 当拖拽界面其他控件(例如 scrollView 和 textView)的时候,定时器不工作. 松开点击后才工作

UITrackingRunLoopMode

    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

这个模式下只有拖拽界面其他控件的时候,定时器才工作,但停止拖拽, 依然不工作

想要实现无论在什么情况下, 定时器都能正常工作,可以这样

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

确实能达到效果,但看起来并不是那么完美. 这是时候可以用另一种 mode
NSRunLoopCommonModes

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

我们先来看下NSRunLoopCommonModes里默认保存是什么 mode

NSLog(@"%@", [NSRunLoop currentRunLoop]);

打印结果:


由此可知,NSRunLoopCommonModes里面就有前面前面需要的两个 mode,这个时候在运行工程, 无论是拖拽还是不拖拽, 定时器都能正常工作了.

CFRunLoopTimerRef

  • CFRunLoopTimerRef 是基于时间的触发器, 基本说的都是 NSTimer
  • GCD 也有定时器功能, 和 NSTimer 定时器有一些区别, 这个在之前专门学习GCD使用的博客里也有提到(iOS - GCD 编程). 今天采用传统方法创建 GCD 来继续学习下
 // 1. 创建 GCD 中的定时器
    /*
     第一个参数:source 的类型,DISPATCH_SOURCE_TYPE_TIMER 表示定时器
     第二个参数:描述信息,线程 ID
     第三个参数:更详细的描述信息
     第四个参数:队列, 决定 GCD 定时器中的任务在哪个线程中执行
     */
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0)); // 程序并发队列
    
    // 2. 设置定时器-- 起始时间和间隔时间以及精准度
    /*
     第一个参数:timer, 定时器对象
     第二个参数:起始时间,DISPATCH_TIME_NOW 从现在开始几时
     第三个参数:间隔时间 GCD 中的时间单位是纳秒
     第四个参数:精准度 绝对精准 0
     */
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    
    // 3. 设置定时器要执行的任务
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"GCD - %@", [NSThread currentThread]);
    });
    
    // 4. 启动执行
    dispatch_resume(timer);

这个时候调用该方法,发现定时器并没有工作,需要先生命一个 定时器属性

@property (nonatomic, strong) dispatch_source_t timer;

然后在上面代码的最后给属性赋值

self.timer = timer;

这个时候定时器就正常工作了.

GCD 与 NSTimer 比较
优点: GCD 不受 RunLoop 运行运行模式的影响.

CFRunLoopSourceRef

CFRunLoopSourceRef 是事件源, 也就是输入源

以前的方法:

  • Port-Based Sources : 基于端口的事件
  • Custom Input Sources : 自定义的事件
  • Cocoa Perform Selector Sources : perform selector 事件

现在的方法:

  • Sources0: 非基于 Port 的, 可以理解为是用户主动触发的事件
  • Sources1: 基于 Port 的,可以理解为是系统事件

用户点击按钮事件触发的就是Source0, 应征了上面所说的


CFRunLoopObserverRef

CFRunLoopObserverRef 是观察者, 能够监听 RunLoop 的状态改变
可以监听的事件有以下几个:

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
};

声明一个方法

- (void)observer {
    
    // 1. 创建观察者
    /*
     第一个参数: 怎么分配存储空间
     第二个参数: 要监听 RunLoop 的哪些状态
     第三个参数: 是否要持续监听
     第四个参数: 优先级 0 就可以
     第五个参数: 当状态改变的时候会回调 block
     */
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler( CFAllocatorGetDefault(),  kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"即将进入 Loop");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"即将处理 Timer");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"即将处理 Source");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"即将进入休眠");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"刚从休眠中唤醒");
                break;
            case kCFRunLoopExit:
                NSLog(@"即将退出 Loop");
                break;
        }
        
    });
    
    // 2. 给 RunLoop 添加观察者
    /*
     第一个参数: 要监听哪个 RunLoop
     第二个参数: 观察者
     第三个参数: 运行模式, 需要传 C语言方法
     */
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode); 
}

在给 RunLoop 添加观察者的时候有提到, 运行mode 需要传 C语言方法,其实
NSDefaultRunLoopMode 就等同于 kCFRunLoopDefaultMode
NSRunLoopCommonModes 就等同于 kCFRunLoopCommonModes

运行结果:


我们可以看到 RunLoop 运行的各个状态,加上定时器,效果更加明显一些

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

会看到 RunLoop 状态会重复变化,说明 RunLoop 不会彻底退出,只要有任务,会一直执行


所以,我们可以得出一条结论: 通过监听 RunLoop 的状态可以让其进行相应的操作

RunLoop 处理逻辑

官方版文字介绍


这里找到了网上一个处理流程图文介绍


可以清晰的看出 RunLoop 的整个处理流程, 而 RunLoop 最终的状态就是 休眠 状态, 一旦有任务就会唤醒

RunLoop 应用

RunLoop 介绍的差不多了,来看下它都有哪些应用:

开启常驻线程

常驻线程就是让一个子线程不进入消亡状态, 等待其他线程发来消息,及时处理其他任务事件

下图是没有开启常驻线程时, 线程直接就 end 了,显然不能满足我们的需求


我们可以创建一个定时器,以保证线程不会结束,但这样显然也不完美,因为无论有没有任务,定时器都一直在工作


这时候,我们就可以让 RunLoop 调用 addPort: forMode:方法,这个时候在运行, 线程就不会 end 了,在有新任务要执行的时候也能及时处理,也就达到了我们的目的.

总结

  1. RunLoop 概念:
  • 从字面意思看:运行循环, 跑圈
  • 其实它内部就是一个 do-while 循环, 在这个循环内部不断的处理各种任务(比如 source, timer, observer)
  • 一个线程对应一个 RunLoop, 主线程的 RunLoop 默认已经启动, 子线程的 RunLoop 需要手动启动(调用 run 方法)
  • RunLoop 只能选个一个mode 启动,如果当前 mode 中没有任何 Source(Source0, Source1), Timer, Observer, 那么就直接退出 RunLoop
  1. RunLoop自动释放池什么时候创建
  • 启动 RunLoop 的时候就创建
  1. RunLoop自动释放池什么时候释放
  • RunLoop 退出的时候释放
  • RunLoop 即将进入睡眠的时候会销毁之前的自动释放池. 并重新创建一个新的
  1. 在开发中如何使用 RunLoop? 什么应用场景?
  • 开启一个常驻线程(让一个子线程不进入消亡状态, 等待其他线程发来消息,处理其他事件)
  • 在子线程中开启一个定时器
  • 在子线程中进行一些长期监控
  • 可以控制定时器在特定模式下执行
  • 可以让某些事情(行为或者任务)在特定模式下执行
  • 可以添加 Observer 监听 RunLoop 的状态, 比如监听点击事件的处理(在所有点击事件之前做一些事情)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容