RunLoop

文章概要:

  本文从4个方面讲解了RunLoop:(1)RunLoop介绍;(2)何时使用RunLoop;(3)RunLoop的使用;(4)常见问题。本文算是一个读别人博客的笔记,不是原创,只是按照自己的思路整理了一下。

参考资料:

深入理解RunLoop

iOS-RunLoop充满灵性的死循环

1. RunLoop介绍:

     RunLoop的本质:线程中的循环。

   它用来接受循环中的事件和安排线程工作,并在没有工作时,让线程进入睡眠状态(休息时,RunLoop会让CPU释放出来去做其他的事情)。

     以下是Event Loop的逻辑:

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

      RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 "接受消息->等待->处理" 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。

  RunLoop和线程的关系:

  线程和 Run Loop 之间是一一对应的。Run Loop是线程的基础架构部分。每个线程都有对应的Run Loop。

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

NSRunLoop   *runloop = [NSRunLoop currentRunLoop];

  ios程序启动执行的main函数如下

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

其中,UIApplicationMain()函数会为main thread设置NSRunLoop对象,也就是说程序启动时,主线程的RunLoop对象启动,所以,我们的App可以在无人操作时休息,有人操作时立马响应。对其它线程来说,run loop默认是没有启动的。也就是说,线程刚创建时并没有 RunLoop,如果不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。只能在一个线程的内部获取其 RunLoop(主线程除外)。  

 RunLoop Mode 一个集合(包括:所有要监视的事件源和要通知的Run Loop中注册的观察者)

    RunLoop Mode包含Source、Observer、Timer

  • Source(事件源,输入源,基于端口事件源例如键盘触摸等) 
  • Observer(观察者,观察当前RunLoop运行状态)
  • Timer(定时器事件源)

    RunLoop Mode的种类有: 

  • NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态
  • UITrackingRunLoopMode:使用这个Mode去跟踪来自用户交互的事件,比如 ScrollView滑动时。
  • UIInitializationRunLoopMode:启动时
  • NSRunLoopCommonModes(kCFRunLoopCommonModes):占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用。 

  RunLoop以一种固定的Mode运行,只会监控这个Mode下添加的Input source和Timer source,如果这个Mode下没有添加事件源,RunLoop就会立即返回。Run Loop被事件源触发,然后Run Loop中注册的观察者得到通知,执行对应的函数。

   Source:

    事件源的分类如下所示:

RunLoop事件源分类

      其中,Input source分为两种:

            Source0:非基于Port的,用于用户主动触发的事件(自定义输入源,点击button 或点击屏幕)
            Source1:基于Port的 通过内核和其他线程相互发送消息(基于端口的输入源,与内核相关)

  • 输入源传递异步事件,通常消息来自于其他线程或程序。
  • 定时源则传递同步事件,发生在特定时间或者重复的时间间隔。

  其中,Port-Based sources是系统的源,Custom input sources是自定义输入源,除了可以通过CFRunLoopSourceCreate创建自定义输入源,还有一种selector源也是属于Custom input sources:

//(1) 在主线程的Run Loop下执行指定的 @selector 方法
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:

//(2) 在当前线程的Run Loop下执行指定的 @selector 方法
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:

//(3) 在当前线程的Run Loop下延迟加载指定的 @selector 方法
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:

//(4) 取消当前线程的调用
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:

上面的(2)(3)performSelector方法,里面含有waitUntilDone、afterDelay字段,当调用NSObject的performSelector方法后,实际上其内部会创建一个 Timer并添加到当前线程的RunLoop中。所以如果当前线程没有RunLoop,则这个方法会失效。

举例:

/*
 (1) CFRunLoopSourceContext context = {0,op,NULL,NULL,NULL,NULL,NULL, ScheduleCallBack, CancelCallBack, PerformCallBack};
op即是我们实际加入到runloop的对象(包含我们要处理的task)剩下的三个回调函数分别在该source task 开始,取消,和结束的时候的情况下激发的,一般我们在schedulecallback里面就可以执行我们所需要执行的task。

(2) CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, kPriority, &context);
kPriority是source run loop的优先级,如果始终定义为0,可能会导致有些task被忽略,所以还是可以人为的设定一些优先级。
*/ 
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopSourceContext delegateSourceContext = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, HandleDelegateSource};
delegateSource = CFRunLoopSourceCreate(NULL, 0, &delegateSourceContext);
CFRunLoopAddSource(runLoop, delegateSource, delegateSourceRunLoopMode);
//将delegateSource 标记为待处理:
CFRunLoopSourceSignal(delegateSource);
//唤醒 RunLoop,让其处理这个事件:
CFRunLoopWakeUp(CFRunLoopGetMain());

Observer:

RunLoopObserver 

 Observer的创建和使用如下:

//创建监听者
     /*
     第一个参数 CFAllocatorRef allocator:分配存储空间 CFAllocatorGetDefault()默认分配
     第二个参数 CFOptionFlags activities:要监听的状态 kCFRunLoopAllActivities 监听所有状态
     第三个参数 Boolean repeats:YES:持续监听 NO:不持续
     第四个参数 CFIndex order:优先级,一般填0即可
     第五个参数 :回调 两个参数observer:监听者 activity:监听的事件
     */
     /*
     所有事件
     typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
     kCFRunLoopEntry = (1UL << 0),   //   即将进入RunLoop
     kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer
     kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source
     kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
     kCFRunLoopAfterWaiting = (1UL << 6),// 刚从休眠中唤醒
     kCFRunLoopExit = (1UL << 7),// 即将退出RunLoop
     kCFRunLoopAllActivities = 0x0FFFFFFFU
     };
     */
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"RunLoop进入");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"RunLoop要处理Timers了");
                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添加监听者
    /*
     第一个参数 CFRunLoopRef rl:要监听哪个RunLoop,这里监听的是主线程的RunLoop
     第二个参数 CFRunLoopObserverRef observer 监听者
     第三个参数 CFStringRef mode 要监听RunLoop在哪种运行模式下的状态
     */
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
     /*
     CF的内存管理(Core Foundation)
     凡是带有Create、Copy、Retain等字眼的函数,创建出来的对象,都需要在最后做一次release
     GCD本来在iOS6.0之前也是需要我们释放的,6.0之后GCD已经纳入到了ARC中,所以我们不需要管了
     */
    CFRelease(observer);

3. 何时使用RunLoop

     Run Loop的优点

  (1)NSRunLoop是一种消息处理模式,它对消息处理的过程进行了封装,使App程序员不用处理一些很琐碎很低层次的具体消息的处理,在NSRunLoop中每一个消息就被打包在input source或者是timer source中了。

  (2)使用run loop可以使你的线程在有工作的时候工作,没有工作的时候休眠,这可以大大节省系统资源。

     何时使用Run Loop?

     在创建辅助线程的时候,才显式的运行一个Run Loop。对于辅助线程,我们仍然需要判断是否需要启动Run Loop。下面是官方Document提供的使用Run Loop的几个场景:

  • 需要使用Port-Based Input Source或者Custom Input Source和其他线程通讯时
  • 需要在线程中使用Timer
  • 需要在线程中使用上文提到的selector相关方法(Cocoa框架为我们定义了一些Custom Input Sources,允许我们在线程中执行一系列selector方法)
  • 需要让线程执行周期性的工作

4. RunLoop的使用

(1)两个自动获取RunLoop的函数:CFRunLoopGetMain() 、CFRunLoopGetCurrent(),内部逻辑大致如下:

/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;
 
/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    
    if (!loopsDic) {
        // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    
    /// 直接从 Dictionary 里获取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    
    if (!loop) {
        /// 取不到时,创建一个
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
    
    OSSpinLockUnLock(&loopsLock);
    return loop;
}
 
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
 
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

内部逻辑说明: 

(1.1)线程和RunLoop是一一对应的;

(1.2)创建:有线程不一定会有RunLoop,RunLoop的创建发生在第一次获取时;

(1.3)销毁:线程销毁时,销毁RunLoop;

(2) 启动RunLoop  和 退出RunLoop

启动RunLoop
(1)- (void)run;
(2)- (void)runUntilDate:(NSDate *)limitDate;设置超时时间(程序看整个生命周期)
(3)- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;特定的Mode(程序看启动停止)

退出RunLoop
(1)设置超时时间;(上面的2)
(2)使用CFRunLoopStop方法通知RunLoop停止。(上面的1、3)

6. 常见问题

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

  • (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

   主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为”Common”属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。

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

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

      同时因为mode还是可定制的,所以:Timer计时会被scrollView的滑动影响的问题可以通过将timer添加到NSRunLoopCommonModes(kCFRunLoopCommonModes)来解决。

验证代码如下,其中TestTimerAndScrollController是UITableViewController:

typedef NS_ENUM(NSUInteger, LVRunLoopTimerTestMethodType) {
    LVRunLoopTimerTestMethodTypeRunLoopModeTypeDefault,
    LVRunLoopTimerTestMethodTypeRunLoopModeTypeCommonModes,
};

@interface TestTimerAndScrollController ()
@property (nonatomic, strong) UILabel *testLabel;
@property (nonatomic) NSInteger count;
@property (nonatomic, strong) NSTimer *testTimer;
@end

@implementation TestTimerAndScrollController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.count = 0;
    
    self.testLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 100, self.tableView.frame.size.width, 50)];
    self.testLabel.backgroundColor = [UIColor lightGrayColor];
    self.testLabel.font = [UIFont systemFontOfSize:40];
    self.testLabel.textAlignment = NSTextAlignmentCenter;
    [self.view addSubview:self.testLabel];
    
    LVRunLoopTimerTestMethodType type = LVRunLoopTimerTestMethodTypeRunLoopModeTypeDefault;
    switch (type) {
        case LVRunLoopTimerTestMethodTypeRunLoopModeTypeDefault:
            self.testTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(updateTestLabel) userInfo:nil repeats:YES];
            break;
        case LVRunLoopTimerTestMethodTypeRunLoopModeTypeCommonModes:
            self.testTimer = [NSTimer timerWithTimeInterval:0.5 target:self selector:@selector(updateTestLabel) userInfo:nil repeats:YES];
            [[NSRunLoop currentRunLoop] addTimer:self.testTimer forMode:NSRunLoopCommonModes];
            break;
        default:
            break;
    }
}

- (void)updateTestLabel
{
    self.count ++;
    self.testLabel.text = [NSString stringWithFormat:@"%ld", self.count];
    if (self.count == 100) {
        [self.testTimer invalidate];
        self.testTimer = nil;
    }
}

同样的,imageView在设置image时也是在NSDefaultRunLoopMode下执行的,如果在UIScrollView中滑动,我们需要把其设置为UITrackingRunLoopMode模式,否则image不会展示:

[imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@""] afterDelay:2 inModes:@[NSDefaultRunLoopMode,UITrackingRunLoopMode]];

(2)runloop和线程有什么关系?

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

  runloop 和线程的关系:

  (2.1) 主线程的run loop默认是启动的。

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

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

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

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

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

NSRunLoop *runloop = [NSRunLoop currentRunLoop];

(3)runloop的mode作用是什么?

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

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

  UITrackingRunLoopMode:ScrollView滑动时

  UIInitializationRunLoopMode:启动时

  NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合 

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

   NSDefaultRunLoopMode(kCFRunLoopDefaultMode)

  NSRunLoopCommonModes(kCFRunLoopCommonModes)

  Run Loop Mode可以理解为一个集合中包括所有要监视的事件源和要通知的Run Loop中注册的观察者。每一次运行自己的Run Loop时,都需要显式或者隐式的指定其运行于哪一种Mode。在设置Run Loop Mode后,你的Run Loop会自动过滤和其他Mode相关的事件源,而只监视和当前设置Mode相关的源(通知相关的观察者)。 

原文地址:https://www.cnblogs.com/Xylophone/p/5237984.html