大数据

runLoop的终极大杀器

一. runLoop的概念

  • 运行在一个Thread上的一个do-while 死循环.
  • 这个循环专门用来接收事件源,通知 绑定的线程 去执行这个事件. iOS 中所有的事件监听全部由运行循环负责
  • 主线程默认开启事件接收循环,并自动创建 自动释放池. 一般自己手动创建的子线程的RunLoop默认没有开启,需要我们自己开启
  • 一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑是这样的:
  • 通过runLoop机制实现省电, 流畅, 响应速度快, 用户体验好
  • RunLoop 并不是线程安全的,所以需要避免在其他线程上调用当前线程的 RunLoop
  • RunLoop 负责管理 autorelease pools
  • RunLoop 负责处理消息事件,即输入源事件、计时器事件和网络请求事情
function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

这种模型通常被称作 Event Loop。 Event Loop 在很多系统和框架里都有实现,所以,RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。

二: RunLoop对象

OSX/iOS 系统中,提供了两个这样的对象:NSRunLoopCFRunLoopRef

  • CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
  • NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
    CFRunLoopRef 的代码是开源的,你可以在这里 http://opensource.apple.com/tarballs/CF/ 下载到整个 CoreFoundation 的源码来查看。

三: runLoop的实现原理

#3.1. RunLoop基本作用
  • 保持程序的持续运行
  • 处理App中的各种事件(比如触摸事件、定时器事件、Selector事件)
  • 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
#3.2. RunLoop与线程
  • 1, 每条线程都有唯一的一个与之对应的RunLoop对象
  • 2, 主线程的RunLoop已经自动创建好了,子线程的RunLoop需要主动创建
  • 3, RunLoop在第一次获取时创建,在线程结束时销毁.
#3.3.获得RunLoop对象

runLoop

线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。

  • Foundation

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

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

Snip20160802_6.png
#3.4. RunLoop相关类

Core Foundation中关于RunLoop的5个类

  • CFRunLoopRef // runloop对象
  • CFRunLoopModeRef //runloop的模式
  • CFRunLoopSourceRef //事件源
  • CFRunLoopTimerRef // 定时器
  • CFRunLoopObserverRef //观察者

runLoop.png
#3.5. RunLoop的运行模式(CFRunLoopModeRef )
  • 一个 RunLoop 包含若干个 Mode,每个Mode又包含若干个Source/Timer/Observer
  • 每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode
  • 如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入
  • 这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响
系统默认注册了5个Mode:
kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行

UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响

UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用

GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到

kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode
#3.6 CFRunLoopTimerRef是基于时间的触发器

CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

  • 基本上说的就是NSTimer,它会受到runloop的mode的影响
NSTimer *timer = [NSTimer timerWithTimeInterval:5 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];

[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
  • GCD的定时器不受Runloop的mode的影响
- (void)gcdTimerConfig{
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());

    dispatch_source_set_timer(self.timer, DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);

    dispatch_source_set_event_handler(self.timer, ^{
        NSLog(@"xsxsxsxs");
    });

    dispatch_resume(self.timer);
}
#3.7. CFRunLoopSourceRef是事件源(输入源)

Source0:非基于Port的,用于用户主动触发的事件
Source1:基于Port的,通过内核和其它线程相互发送消息

#3.8 CFRunLoopObserverRef

CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变
可以监听的时间点有以下几个

可以监听的时间点有以下几个
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), // 1
    kCFRunLoopBeforeTimers = (1UL << 1), // 2
    kCFRunLoopBeforeSources = (1UL << 2), // 4
    kCFRunLoopBeforeWaiting = (1UL << 5), // 32
    kCFRunLoopAfterWaiting = (1UL << 6), // 64
    kCFRunLoopExit = (1UL << 7), // 128
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

Observer的使用步骤

添加Observer
添加观察者:监听RunLoop的状态
释放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);
-(void)observer{

    //创建一个监听对象
    /*
     第一个参数:分配存储空间的
     第二个参数:要监听的状态 kCFRunLoopAllActivities 所有状态
     第三个参数:是否要持续监听
     第四个参数:优先级
     第五个参数:回调
     */
   CFRunLoopObserverRef observer =  CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {

        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"runloop进入");
                break;

            case kCFRunLoopBeforeTimers:
                NSLog(@"runloop要去处理timer");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"runloop要去处理Sources");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"runloop要睡觉了");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"runloop醒来啦");
                break;

            case kCFRunLoopExit:
                NSLog(@"runloop退出");
                break;
            default:
                break;
        }
    });


    //给runloop添加监听者
    /*
     第一个参数:要监听哪个runloop
     第二个参数:监听者
     第三个参数:要监听runloop在哪种运行模式下的状态
     */
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

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

    CFRelease(observer);

}

上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

四. RunLoop 的内部逻辑

内部逻辑

在runloop中每一个runLoop对象同一个时间内, 对应一个mode, 当选中的mode中没有timer或者source时 runloop就会被销毁.

runLoop时间队列.png

RunLoop

可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

/// 用DefaultMode启动
void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}

/// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

/// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {

    /// 首先根据modeName找到对应mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
    /// 如果mode里没有source/timer/observer, 直接返回。
    if (__CFRunLoopModeIsEmpty(currentMode)) return;

    /// 1. 通知 Observers: RunLoop 即将进入 loop。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);

    /// 内部函数,进入loop
    __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {

        Boolean sourceHandledThisLoop = NO;
        int retVal = 0;
        do {

            /// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
            /// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
            /// 执行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);

            /// 4. RunLoop 触发 Source0 (非port) 回调。
            sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
            /// 执行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);

            /// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
            if (__Source0DidDispatchPortLastTime) {
                Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                if (hasMsg) goto handle_msg;
            }

            /// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
            if (!sourceHandledThisLoop) {
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
            }

            /// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
            /// • 一个基于 port 的Source 的事件。
            /// • 一个 Timer 到时间了
            /// • RunLoop 自身的超时时间到了
            /// • 被其他什么调用者手动唤醒
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
                mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
            }

            /// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);

            /// 收到消息,处理消息。
            handle_msg:

            /// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
            if (msg_is_timer) {
                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
            } 

            /// 9.2 如果有dispatch到main_queue的block,执行block。
            else if (msg_is_dispatch) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } 

            /// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
            else {
                CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                if (sourceHandledThisLoop) {
                    mach_msg(reply, MACH_SEND_MSG, reply);
                }
            }

            /// 执行加入到Loop的block
            __CFRunLoopDoBlocks(runloop, currentMode);


            if (sourceHandledThisLoop && stopAfterHandle) {
                /// 进入loop时参数说处理完事件就返回。
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout) {
                /// 超出传入参数标记的超时时间了
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(runloop)) {
                /// 被外部调用者强制停止了
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                /// source/timer/observer一个都没有了
                retVal = kCFRunLoopRunFinished;
            }

            /// 如果没超时,mode里没空,loop也没被停止,那继续loop。
        } while (retVal == 0);
    }

    /// 10. 通知 Observers: RunLoop 即将退出。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

10. RunLoop应用
10.1 应用 NSTimer:

鉴于我们的NSTimer默认工作在Default模式下, 当我们tableView滚动的时候, 会切换到tracking模式下,这样定时器不在工作, 直到回到default模式下, 会恢复工作,这样造成定时器停掉, 我们可以将定时器放在common这个标记下, common默认标记default和tracking模式,这样就可以正常工作了, 但是NStimer定时器也不是很准确, 推荐使用GCD的Timer

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSTimer *timer = [NSTimer timerWithTimeInterval:3.0
                                         invocation:nil
                                            repeats:YES];

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

}

@end
10.2 ImageView显示: PerformSelector

我们可以在设置下载图片的任务放在default模型下,但我们的tableView滚动的时候, 就不在下载数据, 这样的话, 就可以保证tableView的流畅性.

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    //指定该performSelector事件,注册到RunLoop的 default mode
    [self performSelector:@selector(downloadImage)
               withObject:nil
               afterDelay:3.0
                  inModes:@[NSDefaultRunLoopMode]];

}

@end

那么该performSelector事件,只会当主线程RunLoop处于default时,才会被执行。如果不这么做,可能造成当TableView滚动时,又去下载图片可能造成卡顿。(不过其实一般的下载操作,但是放在其他子线程完成的,这里只是举个简单的例子).

延迟执行.png

//线程只能执行第一次封装的任务,不能尝试重新执行

10.3 常驻线程
  • 1 创建一个单例NSThread对象,并start
  • 2 NSThread线程入口函数中完成:
    创建@autoreleasepool{ … }
    然后开启Thread的RunLoop
    添加一个NSMachPort给Thread对象来防止runloop执行完毕之后会退出注意,不要讲NSMachPort对象发送给其他子线程
    我看到有一种使用while(1){ 开启RunLoop }
    这样的结构来防止runloop退出,虽然是可以,个人觉得太粗糙
  • 3 然后给单例NSThread的runloop上注册 Timer/Sources/Observer
  • 4 完成回调业务处理
-(void)show{
    /*
     1.子线程的ruanloop是需要自己手动创建的
     2.子线程的ruanloop是需要主动开启的
     3.子线程的ruanloop里面至少要有一个source或者是timer,observer不行的
     */

[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop]run];
}

使用port或是自定义的input source来和其他线程进行通信 (AFN就是例子)

#10.3.1 AFN在NSURLConnection中的使用

AFN在NSURLConnection中的使用. 其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}

RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。

- (void)start {
    [self.lock lock];
    if ([self isCancelled]) {
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } else if ([self isReady]) {
        self.state = AFOperationExecutingState;
        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    [self.lock unlock];
}

当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中。

#10.3.2 AFN在NSURLSession中的使用

AFN使用RunLoop的实例.png

Snip20160719_6.png
  • 自动释放池: AutoreleasePool
     1.自动释放池什么时候创建和释放
     第一次创建:第一次进入runloop的时候
     最后一次释放:runloop退出的时候
     其他情况:
        当runloop将要睡觉的时候会释放,然后创建一个新的
    _wrapRunLoopWithAutoreleasePoolHandler  activities = 0x1    1
    _wrapRunLoopWithAutoreleasePoolHandler activities = 0xa0 160
     kCFRunLoopBeforeWaiting 
     +
     kCFRunLoopExit

在主线程即将休眠时,释放自动释放池
在主线程即将唤醒时,再次创建自动释放池,并将之前的对象再次放入池中

kCFRunLoopEntry >>> 创建自动释放池
kCFRunLoopBeforeWaiting >>> 重建自动释放池
kCFRunLoopExit >>> 销毁自动释放池
11. RunLoop面试题
#11.1 什么是RunLoop?
  1. 从字面意思看:运行循环、跑圈.其实它内部就是do-while循环,在这个循环内部不断地处理各种任务(比如Source、Timer、Observer
  2. 一个线程对应一个RunLoop,主线程的RunLoop默认已经启动,子线程的RunLoop得手动启动(调用run方法)
  3. RunLoop只能选择一个Mode启动,如果当前Mode中没有任何Source(Sources0、Sources1)、Timer,那么就直接退出RunLoop
#11.2 自动释放池什么时候释放?

通过Observer监听RunLoop的状态
在主线程即将休眠时,释放自动释放池
在主线程即将唤醒时,再次创建自动释放池,并将之前的对象再次放入池中

#11.3 在开发中如何使用RunLoop?什么应用场景?
  1. 开启一个常驻线程(让一个子线程不进入消亡状态,等待其他线程发来消息,处理其他事件)
  2. 在子线程中开启一个定时器
  3. 在子线程中进行一些长期监控
  4. 可以控制定时器在特定模式下执行
  5. 可以让某些事件(行为、任务)在特定模式下执行
  6. 可以添加Observer监听RunLoop的状态,比如监听点击事件的处理(在所有点击事件之前做一些事情)

参考: 一个iOS菜菜的白话文记录
YY大神
百度孙源的runLoop视频

发表评论