RunLoop浅析

什么是Runloop

RunLoop浅析_第1张图片
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 相关类

RunLoop浅析_第2张图片
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浅析_第3张图片
RunLoop处理逻辑(官方示意图).png

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

  • 输入源 (事件源), 即 CFRunLoopSourceRef, 基于端口的输入源 (port) 和 自定义输入源 (custom), 当然还包含 performSelector:onThread...
  • 拥有添加在 RunLoop 内的定时器
RunLoop浅析_第4张图片
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 *)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 研究资料

  • 苹果官方文档
    https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html
  • CFRunLoopRef 是开源的
    http://opensource.apple.com/source/CF/CF-1151.16/

参考资料

  • 李明杰关于 RunLoop 的研究
  • sunnyxx-http://blog.sunnyxx.com/2014/10/15/behind-autorelease/

你可能感兴趣的:(RunLoop浅析)