RunLoop浅析

什么是Runloop

RunLoop01.png
  • 运行循环
  • 跑圈
  • 内部类似一个 do-while 循环, 在循环内部不断处理各种任务 (Source, Observe, Timer)
  • 一个线程对应一个 RunLoop

用途

  • 保持程序持续运行
  • 处理 APP 各种事件 (触摸事件, 定时器事件, Selector事件)
  • 节省 CPU 资源, 提高程序性能: 该做事情的时候做事情, 该休息时休息

没有RunLoop

程序一启动就结束了

int main(int argc, char * argv[]) {
    NSLog(@"execute main function");
    return 0;
}

如果有了 RunLoop

程序大致是这样子,但是要更加复杂

int main(int argc, char * argv[]) {
    BOOL running = YES;
    do {
        // 执行各种任务,处理各种事件
             // ......
    } while (running);
    return 0;
}

由于 main 函数里面启动了一个 RunLoop, 因此程序不会马上退出, 会保持程序的运行状态

main 函数中的 RunLoop

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

  • UIApplicationMain 函数内部启动了一个 RunLoop 对象
  • UIApplicationMain 函数一直没有返回, 保持了程序的运行
  • 这个默认启动的 RunLoop 是与主线程相关

程序一旦启动

  • 执行UIApplicationMain 函数
  • 默认启动一个 RunLoop
  • 这个 RunLoop 会一直处理主线程相关的事情
  • 这个 RunLoop 会一直遍历, 监听用户事件
  • 这就是主线程的事件响应的这么快的原因

RunLoop 要想跑圈

  • 模式(Mode)里面要有东西 (事件源 / Observer / 定时器)
  • RunLoop 要启动 (主线程默认创建并启动, 子线程需要手动启动)
  • 没有事件源, 没有定时器, RunLoop 就会进入睡眠状态

RunLoop 对象

iOS 中提供了两套 API 来访问和使用 RunLoop

  • Foundation : NSRunLoop
  • Core Foundation : CFRunLoopRef

NSRunLoop 是基于 CFRunLoopRef 的OC 包装, 如果研究 RunLoop 内部结构, 需要研究 CFRunLoopRef

RunLoop 与线程

  • 每条线程都有唯一一个与之对应的 RunLoop 对象
  • 主线程的 RunLoop 已经创建好, 子线程的 RunLoop 需要手动创建
  • RunLoop 在第一次获取时创建, 在线程结束时销毁
  • RunLoop 对象是使用字典存储, 以线程作为 key

RunLoop 相关类

RunLoop02.png

01 - CFRunLoopModeRef

  • CFRunLoopModeRef 代表着RunLoop的运行模式
  • 一个RunLoop包含若干个Mode,每个Mode又包含若干个 Source/Timer/Observer
  • 每次RunLoop启动时, 都会指定其中一个Mode, 这个Mode被称作CurrentMode
  • 如果需要切换 Mode, 只能退出 RunLoop, 再重新指定一个 Mode 进入

系统默认注册了 5 个 Mode :

  • kCFRunLoopDefultMode : APP 的默认 Mode, 通常主线程是在这个 Mode下
  • UITrackingRunLoopMode :
    界面跟踪 Mode, 用于 scrollView 跟踪触摸滑动, 保证界面不受其他 Mode 影响 (添加定时器不好使)
  • UIInitializationRunLoopMode :
    在刚启动 APP 时进入的第一个 Mode, 启动完就不再使用
  • GSEventReceiveRunLoopMode :
    接收系统事件的内部 Mode, 通常用不到
  • kCFRunLoopCommonModes :
    这是一个占位用的 Mode, 不是一个真正的 Mode (也就说 RunLoop 无法启动此模式)

02 - CFRunLoopTimerRef

  • CFRunLoopTimerRef 是基于时间的触发器
  • 基本上相当于 NSTimer
  • 定时器会跑在 common modes 模式下
  • 标记为 common modes 的模式有:
    • kCFRunLoopDefultMode
    • UITrackingRunLoopMode

定时器添加到 kCFRunLoopDefultMode

NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

将定时器添加到 NSDefaultRunLoopMode , 滑动 scollView 的时候, 定时器就会停止执行, RunLoop 此时会自动切换到 UITrackingRunLoopMode 模式, 定时器就会停止执行

定时器添加到 NSRunLoopCommonModes

NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

将定时器添加到 NSRunLoopCommonModes, 此时就不会停止执行

03 - CFRunLoopSourceRef

  • CFRunLoopSourceRef是事件源(输入源)

以前的分法

  • Port-Based Sources
  • Custom Input Sources
  • Cocoa Perform Selector Sources

现在的分法

  • Source0:非基于Port的
  • Source1:基于Port的

04 - CFRunLoopObserverRef

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

    // 添加观察者:监听RunLoop的状态
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

    // 释放Observer
    CFRelease(observer);
}
  • 可以监听的时间点有以下几个
/* 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 处理逻辑

下图简介了 RunLoop 处理过程, 一个线程的 RunLoop 在存在事件源 / 定时器的条件下, 会不断的处理事件, 处理的事件包括

  • 处理基于 port 的 CFRunLoopSourceRef
  • 处理 customer 自定义事件源
  • 处理 selector 事件
  • 处理定时器执行
RunLoop处理逻辑(官方示意图).png

官方的图解很清楚, RunLoop 在不停的跑圈, 跑圈的前提是满足以下条件之一:

  • 输入源 (事件源), 即 CFRunLoopSourceRef, 基于端口的输入源 (port) 和 自定义输入源 (custom), 当然还包含 performSelector:onThread...
  • 拥有添加在 RunLoop 内的定时器
RunLoop处理逻辑(网友整理).png

RunLoop 实际应用

(1) 常驻线程

即让子线程处于 "不消亡" 的状态, 一直在后台处理某些频发事件 / 等待其他线程发来消息

  • 在子线程监控网络状态
  • 在子线程开启一个定时器
  • 在子线程长期监控其他行为
+ (void)networkRequestThreadEntryPoint:(id)__unused object { 
    @autoreleasepool { 
        [[NSThread currentThread] setName:@"AFNetworking"];        
         NSRunLoop *runLoop = [NSRunLoop currentRunLoop];       
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; 
    }
}

摘自 AFNetworking 源代码, AFN这样做的原理在于子线程下默认不开启 RunLoop, 需要手动开启, 而 RunLoop 不断跑圈需要满足以下条件之一 :

  • RunLoop有事件源(输入源), 包含基于端口 (port) 的事件源 / custom 事件源等
  • RunLoop存在定时器
    因此, AFN为RunLoop的default模式增加了一个NSMachPort端口(实际上也可以是其他端口),也就相当于为RunLoop添加了事件源, 因此RunLoop可以不断的跑圈, 保证线程的不死状态
    顺便提一下, AFN保持一个常驻线程的原因, 第一是因为子线程默认不会开启RunLoop, 它会像一个C语言程序一样运行完所有代码后退出线程, 而网络请求是异步的, 这就可能会出现通过网络请求获取到数据之后, 线程已经退出, 无法执行请求成功/失败的代理方法, 因此AFN开启了一个RunLoop, 保活了线程

(2) 控制定时器在特定模式下运行

即可以将计时器 timer 添加到 kCFRunLoopDefultMode 下, 如果 RunLoop 切换到 UITrackingRunLoopMode (UIScrollView 滚动过程中), 那么定时器就会暂停执行, 等到滚动结束, 定时器就会继续执行
也可以将定时器 timer 添加到 NSRunLoopCommonModes 下, 此时不管有无 scrollView 滑动, 都不会影响 timer 的执行

(3) 控制某些事件在特定模式下执行

即可以让某个 selector 在某个线程 (key) 的 RunLoop 下的特定模式下执行 (数组中包含 Mode)

通过以下的 API :

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array NS_AVAILABLE(10_5, 2_0);

(4) 添加 Observer 监听 RunLoop 状态, 可以监听点击事件的处理 (在所有点击事件之前做一些事情)

调用 C 语言函数 CFRunLoopObserverCreateWithHandler () 创建 Observer, 监听某个 RunLoop 状态, 注意要手动释放

关于自动释放池与 RunLoop

Autorelease pool

在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop

也就意味着, 在@autorelasepool 中的代码, 默认都是加在了一个自动释放池当中, 这个自动释放池是与主线程的 RunLoop 相关, 内部所有对象会在自动释放池释放的时候对内部所有对象进行一次 release 操作

至于主线程 RunLoop 下的自动释放池什么时候释放, 是在主线程 RunLoop 迭代 (睡眠)之前释放, 这个 RunLoop 什么时候睡眠呢? 是在没有接收任何输入源(事件源)/定时器的条件下

自动释放池什么时候释放?

在 RunLoop 睡眠之前释放 (KCFRunLoopBeforeWaiting), 也有人说 Autorelease对象是在当前的runloop迭代结束时释放的, 实际是一个意思

什么时候用@autoreleasepool

根据Apple的文档,使用场景如下:

  • 写基于命令行的的程序时,就是没有UI框架,如AppKit等Cocoa框架时。
  • 写循环,循环里面包含了大量临时创建的对象。(本文的例子)
  • 创建了新的线程。(非Cocoa程序创建线程时才需要)
  • 长时间在后台运行的任务。

RunLoop 研究资料

参考资料

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

推荐阅读更多精彩内容