美文网首页
RunLoop学习总结

RunLoop学习总结

作者: 斑驳的流年无法释怀 | 来源:发表于2018-07-26 15:55 被阅读10次

简介

Runloop可以保证程序会一直运行并且时时刻刻在等待用户的输入操作。RunLoop可以在需要的时候运行,在没有操作的时候就停下来休息,节省CPU资源,提高程序性能,RunLoop就是控制线程生命周期并接收事件进行处理的机制。

CFRunLoopRef的代码是开源的,可以直接查看源码 源码

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;          /* locked for accessing mode list */
    __CFPort _wakeUpPort;           // used for CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    pthread_t _pthread;
    uint32_t _winthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

RunLoop能干什么

  1. 线程保活
  2. 处理App中的各种事件

基本概念

  1. Runloop,顾名思义就是跑圈,他的本质就是一个do,while循环,当有事做时就做事,没事做时就休眠。
  2. 每个线程都由一个Run Loop,主线程的Run Loop会在App运行的时自动运行,子线程需要手动获取运行,第一次获取时,才会去创建
  3. 每个Run Loop都会以一个模式mode来运行,runMode:beforeDate:
  4. Run Loop的处理两大类事件源:TimerSource和InputSource(包括performSelector *方法簇、Port或者自定义的InputSource),每个事件源都会绑定在RunLoop的某个特定模式mode上,而且只有RunLoop在这个模式下运行的时候,才会触发Timer和InputSource
  5. 最后,如果没有任何事件源添加到RunLoop上,RunLoop就会立刻exit
架构
运行时

RunLoop机制

主线程 (有 RunLoop 的线程) 几乎所有函数都从以下六个之一的函数调起:

  • CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION
    CFRunloop is calling out to an abserver callback function
    用于向外部报告 RunLoop 当前状态的更改,框架中很多机制都由 RunLoopObserver 触发,如 CAAnimation

  • CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK
    CFRunloop is calling out to a block
    消息通知、非延迟的perform、dispatch调用、block回调、KVO

  • CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
    CFRunloop is servicing the main desipatch queue

  • CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION
    CFRunloop is calling out to a timer callback function
    延迟的perform, 延迟dispatch调用

  • CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
    CFRunloop is calling out to a source 0 perform function
    处理App内部事件、App自己负责管理(触发),如UIEvent、CFSocket。普通函数调用,系统调用

  • CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION
    CFRunloop is calling out to a source 1 perform function
    由RunLoop和内核管理,Mach port驱动,如CFMachPort、CFMessagePort

RunLoop的几种状态

  • kCFRunLoopEntry -- 进入runloop循环
  • kCFRunLoopBeforeTimers -- 处理定时调用前回调
  • kCFRunLoopBeforeSources -- 处理input sources的事件
  • kCFRunLoopBeforeWaiting -- runloop睡眠前调用
  • kCFRunLoopAfterWaiting -- runloop唤醒后调用
  • kCFRunLoopExit -- 退出runloop

如果没有添加任何输入源事件或Timer事件,线程会一直在无限循环空转中,会一直占用CPU时间片,没有实现资源的合理分配。
没有while循环且没有添加任何输入源或Timer的线程,线程会直接完成,被系统回收。

CFRunLoopMode

  • NSDefaultRunLoopMode:默认的运行模式,除了NSConnection对象的事件。
  • UITrackingRunLoopMode:用于跟踪触摸事件触发的模式(UIScrollView上下滚动), 主线程当触摸事件触发会设置为这个模式,可以用来在控件事件触发过程中设置Timer
  • NSRunLoopCommonModes:是一组常用的模式集合,将一个input source关联到这个模式集合上,等于将input source关联到这个模式集合中的所有模式上。在iOS系统中NSRunLoopCommonMode包含NSDefaultRunLoopMode、NSTaskDeathCheckMode、UITrackingRunLoopMode
  • NSConnectionReplyMode,用来监听处理网络请求NSConnection的事件
  • NSModalPanelRunLoopMode,OS X的Modal面板事件

假如我有个timer要关联到这些模式上,一个个注册很麻烦,我可以用:

CFRunLoopAddCommonMode([[NSRunLoop currentRunLoop] getCFRunLoop],(__bridge CFStringRef) UITrackingRunLoopMode);

注意:Run Loop 运行时只能以一种固定的模式运行,如果我们需要它切换模式,只有停掉它,再重新开其它,Run Loop不能在运行在NSRunLoopCommonModes模式,因为NSRunLoopCommonModes其实是个模式集合,而不是一个具体的模式,我可以添加事件源的时候使用NSRunLoopCommonModes,只要Run Loop运行在NSRunLoopCommonModes中任何一个模式,这个事件源都可以被触发。

最早接触到的RunLoop用法如下:
AFN2.x

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

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

runloop开始跑起来,但是要注意,这种runloop,只有一种方式能停止。

[NSRunloop currentRunloop] removePort:forMode:]

在AFN3.x中,没有了常驻线程,都是用的run,结束的时候stop。

需要开启的时候:
CFRunLoopRun();

终止的时候:
CFRunloopStop(CFRunLoopGetCurrent());

操作RunLoop

Foundation层和Core Foundation层都有相应的接口可以操作Run Loop:Foundation层对应的是NSRunLoop,Core Foundation层对应的是CFRunLoopRef;

NSRunLoop相关方法:

// 运行NSRunLoop,运行模式为默认的NSDefaultRunLoopMode模式,没有超时限制,属于无条件运行,不建议使用会导致Run Loop永久性的在NSDefaultRunLoopMode模式,即使用CFRunLoopStop(runloopRef);也无法停止Run Loop的运行,除非能移除这个runloop上的所有事件源,包括定时器和source时间,不然这个子线程就无法停止,只能永久运行下去
- (void)run;
// 运行NSRunLoop:参数为时间期限,运行模式为默认的NSDefaultRunLoopMode模式,CFRunLoopStop(runloopRef), 也无法停止Run Loop的运行
- (void)runUntilDate:(NSDate *)limitDate;
// 运行NSRunLoop:参数为运行模式、时间期限,返回值为YES表示处理事件后返回的,NO表示是超时或者停止运行导致返回的,这种运行方式是可以被CFRunLoopStop(runloopRef)所停止的
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDtate *)limitDate;

runUntilDate:方法可以如下方式使用,Run Loop一段时间会退出给你检查运行条件的机会,如果需要可以再次运行Run Loop

while(!hasDone) {//hasDone使我们自己定义的
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow: 10]];
    NSLog(@"exiting runloop, ......");
}

注意:消息是一个 非timer的事件,所有runloop处理完就退出

CFRunLoopRef相关方法:**

// 运行CFRunLoopRef,CFRunLoopStop接口调用停止这个RunLoop,或者RunLoop的所有事件源被删除
void CFRunLoopRun();
// 运行CFRunLoopRef:参数为运行模式、时间和是否在处理Input Source后退出标志,返回值是exit原因
SInt32 CFRunLoopRunInMode(mode, second, returnAfterSourceHandled);
// 停止运行CFRunLoop
void CFRunLoopStop(CFRunLoopRef rl);
// 唤醒CFRunLoopRef
void CFRunLoopWakeUp(CFRunLoopRef rl);

停止RunLoop的方法

  1. 移除掉runloop种的所有事件源(timer和source)。
  2. 设置一个超时时间。
  3. 只要CFRunLoop运行起来就可以用:void CFRunLoopStop(CFRunLoopRef rl); 去停止, NSRunLoop的runMode:beforeDate方法也能使用void CFRunLoopStop(CFRunLoopRef rl); 停止,因为它是基于SInt32 CFRunLoopRunInMode(mode, second, returnAfterSourceHandled)的封装。
    NSRunLoop是线程不安全的,而CFRunLoop是线程安全的

用第二个参数控制,runloop的开始于停止

while(!cancel) {
    CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);//每次runloop只运行1秒就停止YES标识当有非timer事件进来也会立即开始下一次runloop,当然每次进来我们都可以去修改Mode的值,这样我们呢可以让runloop每次都运行在不同的模式下
}

输入源

线程间的通信(不仅限于通信,几乎所有iOS事件都是如此),实际上是各种输入源,触发runloop去处理对应的事件

RunLoopObserver与AutoreleasePool的关系
UIKit 通过 RunLoopObserver 在 RunLoop 两次 Sleep 间对 Autorelease Pool 进行 Pop 和 Push 将这次 Loop 中产生的 Autorelease 对象释放。

RunLoop的挂起与唤醒
指定用于唤醒的 mach_port 端口 调用 mach_msg 监听唤醒端口,被唤醒前系统内核将这个线程挂起,停留在mach_msg_trap状态。 由另一个线程向内核发送这个端口的msg后,trap状态被唤醒,RunLoop继续工作。

基于端口的输入源

实践

线程保活

/**
 保活
 */
- (void)keepAlive {
    __block NSThread *thread;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        NSLog(@"当前线程开始");
        thread = [NSThread currentThread];
        NSRunLoop *runloop = [NSRunLoop currentRunLoop];
        [runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runloop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        
        NSLog(@"当前线程结束");
    });
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        [self performSelector:@selector(test)  onThread:thread withObject:self waitUntilDone:YES];
    });
}

- (void)test {
    NSLog(@"我还活着呢");
}

给Timer指定RunLoop

/**
 timer
 */
- (void)startTimer {
    count = 10;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(exeTimer:) userInfo:@"timerTest" repeats:YES];
        //[timer fire];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
        
        CFRunLoopRunResult result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, MAXFLOAT, NO);
        
        //kCFRunLoopRunFinished = 1, //Run Loop结束,没有Timer或者其他Input Source
        //kCFRunLoopRunStopped = 2, //Run Loop被停止,使用CFRunLoopStop停止Run Loop
        //kCFRunLoopRunTimedOut = 3, //Run Loop超时
        //kCFRunLoopRunHandledSource = 4 ////Run Loop处理完事件,注意Timer事件的触发是不会让Run Loop退出返回的,即使CFRunLoopRunInMode的第三个参数是YES也不行
        
        switch (result) {
            case kCFRunLoopRunFinished:
                NSLog(@"kCFRunLoopRunFinished");
                
                break;
            case kCFRunLoopRunStopped:
                NSLog(@"kCFRunLoopRunStopped");
                
            case kCFRunLoopRunTimedOut:
                NSLog(@"kCFRunLoopRunTimedOut");
                
            case kCFRunLoopRunHandledSource:
                NSLog(@"kCFRunLoopRunHandledSource");
            default:
                break;
        }
    });
}

- (void)exeTimer:(NSTimer *)timer{
    
    NSLog(@"第%d次调用",count);
    count--;
    
    //    if (count == 8) {//验证停止
    //        CFRunLoopRef runloop = CFRunLoopGetCurrent();
    //        CFRunLoopStop(runloop);
    //    }
    if (count == 0) {//验证正常结束,timer停止,也就意味着runloop没有事件源
        [timer invalidate];
        timer = nil;
        NSLog(@"定时器结束");
    }
}

基于NSPort的线程通讯

/**
 基于NSPort的线程通讯
 */
- (void)sendMessage {
    
    NSMachPort *mainPort = [[NSMachPort alloc]init];
    NSPort *threadPort = [NSMachPort port];
    threadPort.delegate = self;
    
    [[NSRunLoop currentRunLoop] addPort:mainPort forMode:NSDefaultRunLoopMode];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"线程开始");
        [[NSRunLoop currentRunLoop] addPort:threadPort forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        NSLog(@"线程结束");
    });
    
    NSString *s1 = @"message";
    NSData *data = [s1 dataUsingEncoding:NSUTF8StringEncoding];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSMutableArray *array = [NSMutableArray arrayWithObjects:mainPort,data,nil];
        NSLog(@"发送消息");
        [threadPort sendBeforeDate:[NSDate date] msgid:1000 components:array from:mainPort reserved:0];
    });
}
#pragma mark - NSPortDelegate

- (void)handlePortMessage:(id)message {//此处需要改写为id类型
    
    //只能用KVC的方式取值
    NSArray *array = [message valueForKeyPath:@"components"];
    NSData *data =  array[1];
    NSString *s1 = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"%@",s1);
}

基于Source线程通讯

@interface ViewController ()<NSMachPortDelegate>
{
    CFRunLoopRef _runLoopRef;
    CFRunLoopSourceRef _source;
    CFRunLoopSourceContext _source_context;
}
@end

- (void)source {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        NSLog(@"线程开始");
        
        _runLoopRef = CFRunLoopGetCurrent();
        //初始化_source_context。
        //bzero(&_source_context, sizeof(_source_context));
        //这里创建了一个基于事件的源,绑定了一个函数
        _source_context.perform = fire;
        //参数
        _source_context.info = "你好";
        //创建一个source
        _source = CFRunLoopSourceCreate(NULL, 0, &_source_context);
        //将source添加到当前RunLoop中去
        CFRunLoopAddSource(_runLoopRef, _source, kCFRunLoopDefaultMode);
        
        //开启runloop 第三个参数设置为YES,执行完一次事件后返回
        CFRunLoopRunInMode(kCFRunLoopDefaultMode, 111111, YES);
        
        NSLog(@"线程结束");
    });
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        if (CFRunLoopIsWaiting(_runLoopRef)) {
            NSLog(@"RunLoop 正在等待事件输入");
            //添加输入事件
            CFRunLoopSourceSignal(_source);
            //唤醒线程,线程唤醒后发现由事件需要处理,于是立即处理事件
            CFRunLoopWakeUp(_runLoopRef);
        }else {
            NSLog(@"RunLoop 正在处理事件");
            //添加输入事件,当前正在处理一个事件,当前事件处理完成后,立即处理当前新输入的事件
            CFRunLoopSourceSignal(_source);
        }
    });
}

代码地址

应用场景:

  1. 维护线程的生命周期,让线程不自动退出,isFinished为Yes时退出。
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
while (!self.isCancelled && !self.isFinished) {
    @autoreleasepool {
            [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
    }
}
  1. 指定时长执行一次
@autoreleasepool {
    NSRunLoop * runLoop = [NSRunLoop currentRunLoop];
    NSTimer * udpateTimer = [NSTimer timerWithTimeInterval:60
                                                    target:self
                                                  selector:@selector(onTimerFired:)
                                                  userInfo:nil
                                                   repeats:YES];
    [runLoop addTimer:udpateTimer forMode:NSRunLoopCommonModes];
    [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:60*60]];
}

未完待续:只把自己理解和实践过的写出来了,还只是冰山一角啊。
参考

官方文档

CFRunLoopRef源码
大神的博客
源码解析
源码剖析
https://www.cnblogs.com/zy1987/p/4582466.html

相关文章

  • NSRunLoop

    前言 RunLoop的初期学习总结,后续会持续研究更新。 一、Runloop定义及作用 1. 什么是Runloop...

  • RunLoop学习总结

    RunLoop的定义 当有持续的异步任务需求时,我们会创建一个独立的生命周期可控的线程。RunLoop就是控制线程...

  • RunLoop学习总结

    简介 Runloop可以保证程序会一直运行并且时时刻刻在等待用户的输入操作。RunLoop可以在需要的时候运行,在...

  • Runloop学习总结

    什么是Runloop · 一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让...

  • RunLoop学习总结

    什么是RunLoop 从字面上看,就是运行循环,跑圈 其实它内部就是do-while循环,在这个循环内部不断地处理...

  • RunLoop学习总结

    通过以下文章学习记录 关于Runloop的原理探究及基本使用 深入理解RunLoop RunLoop完全指南 Ru...

  • GeekBand - iOS 多线程和RunLoop 总结

    iOS 开发高级进阶 第三周 多线程 Runloop iOS 多线程以及 RunLoop 学习总结 基础知识 什么...

  • RunLoop的一些学习与总结

    最近在学习一些OC底层的东西, 下面是学习了RunLoop的一些总结和感受^^ 首先,RunLoop的作用 从字面...

  • RunLoop学习与总结

    RunLoop一个运行循环保持程序的持续运行监听处理 APP 各种事件(触摸,定时器,selector)节省 CP...

  • RunLoop

    RunLoop 文章已经很多了,结合各大文章做个总结 什么是 RunLoop RunLoop 人如其名,run 跑...

网友评论

      本文标题:RunLoop学习总结

      本文链接:https://www.haomeiwen.com/subject/tjpbmftx.html