美文网首页
RunLoop详解

RunLoop详解

作者: iOS扫地僧 | 来源:发表于2018-05-09 08:59 被阅读0次

    RunLoop详解

    RunLoop运行循环(死循环)

    RunLoop模式

    • NSDefaultRunLoopMode -- 默认模式,苹果建议该模式放时钟,网络事件
    • UITrackingRunLoopMode -- UI模式,只能被触摸事件所唤醒! !
    • NSRunLoopCommonModes -- 并不是一个真正的RunLoop的模式! 而是一个占位模式
    • UIInitializationRunLoopMode 在刚启动 App 时进入的第一个 Mode,启动完成后就不再使用。
    • GSEventReceiveRunLoopMode 接受系统事件的内部 Mode,通常用不到。

    主线程一开起来,就会跑一个和主线程对应的RunLoop,那么RunLoop一定是在程序的入口main函数中开启。第三个参数是当前app的单利对象,填写nil相当于填写了一个UIApplication

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

    进入UIApplicationMain

    UIKIT_EXTERN int UIApplicationMain(int argc, char *argv[], NSString * __nullable principalClassName, NSString * __nullable delegateClassName);
    

    我们发现它返回的是一个int数,那么我们对main函数做一些修改

    int main(int argc, char * argv[]) {
        @autoreleasepool {
            NSLog(@"开始");
            int re = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
            NSLog(@"结束");
            return re;
        }
    }
    

    运行程序,我们发现只会打印开始,并不会打印结束,这说明在UIApplicationMain函数中,开启了一个和主线程相关的RunLoop,导致UIApplicationMain不会返回,一直在运行中,也就保证了程序的持续运行。
    我们来看到RunLoop的源码

    // 用DefaultMode启动
    void CFRunLoopRun(void) {    /* DOES CALLOUT */
        int32_t result;
        do {
            result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
            CHECK_FOR_FORK();
        } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
    }
    

    我们发现RunLoop确实是do while通过判断result的值实现的。因此,我们可以把RunLoop看成一个死循环。如果没有RunLoop,UIApplicationMain函数执行完毕之后将直接返回,也就没有程序持续运行一说了

    CFRunLoopSourceRef事件源(输入源)

    按照函数调用栈可以把Source分为两种

    Source0:非基于Port的 用于用户主动触发的事件(点击button 或点击屏幕)
    Source1:基于Port的 通过内核和其他线程相互发送消息(与内核相关)
    注意:Source1在处理的时候会分发一些操作给Source0去处理

    image.png

    RunLoop退出

    • 主线程销毁RunLoop退出
    • Mode中有一些Timer 、Source、 Observer,这些保证Mode不为空时保证RunLoop没有空转并且是在运行的,当Mode中为空的时候,RunLoop会立刻退出
      我们在启动RunLoop的时候可以设置什么时候停止
    [NSRunLoop currentRunLoop]runUntilDate:<#(nonnull NSDate *)#>
    [NSRunLoop currentRunLoop]runMode:<#(nonnull NSString *)#> beforeDate:<#(nonnull NSDate *)#>
    

    RunLoop应用

    1. 常驻线程

    常驻线程的作用:我们知道,当子线程中的任务执行完毕之后就被销毁了,那么如果我们需要开启一个子线程,在程序运行过程中永远都存在,那么我们就会面临一个问题,如何让子线程永远活着,这时就要用到常驻线程:给子线程开启一个RunLoop
    注意:子线程执行完操作之后就会立即释放,即使我们使用强引用引用子线程使子线程不被释放(强引用只是引用的对象不被释放,但不能使线程保活),也不能给子线程再次添加操作,或者再次开启。

        HKThread *thread = [[HKThread alloc] initWithBlock:^{
            _timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
    //        while (true) {
    //            //啥都不干,只为线程保活
    //        }
            [[NSRunLoop currentRunLoop]addTimer:_timer forMode:NSDefaultRunLoopMode];
            //每条线程上面都有一个runloop,只不过,默认是不开启的
            [[NSRunLoop currentRunLoop] run];//这里是一个死循环,下面的这一句代码不走
            NSLog(@"来了!!!!");
        }];
        [thread start];
    

    第二种方法

    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
       // 创建子线程并开启
        NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(show) object:nil];
        self.thread = thread;
        [thread start];
    }
    -(void)show
    {
        // 注意:打印方法一定要在RunLoop创建开始运行之前,如果在RunLoop跑起来之后打印,RunLoop先运行起来,已经在跑圈了就出不来了,进入死循环也就无法执行后面的操作了。
        // 但是此时点击Button还是有操作的,因为Button是在RunLoop跑起来之后加入到子线程的,当Button加入到子线程RunLoop就会跑起来
        NSLog(@"%s",__func__);
        // 1.创建子线程相关的RunLoop,在子线程中创建即可,并且RunLoop中要至少有一个Timer 或 一个Source 保证RunLoop不会因为空转而退出,因此在创建的时候直接加入
        // 添加Source [NSMachPort port] 添加一个端口
        [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        // 添加一个Timer
        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];    
        //创建监听者
        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添加监听者
        CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
        // 2.子线程需要开启RunLoop
        [[NSRunLoop currentRunLoop]run];
        CFRelease(observer);
    }
    

    注意:创建子线程相关的RunLoop,在子线程中创建即可,并且RunLoop中要至少有一个Timer 或 一个Source 保证RunLoop不会因为空转而退出,因此在创建的时候直接加入,如果没有加入Timer或者Source,或者只加入一个监听者,运行程序会崩溃

    2. 自动释放池

    Timer和Source也是一些变量,需要占用一部分存储空间,所以要释放掉,如果不释放掉,就会一直积累,占用的内存也就越来越大,这显然不是我们想要的。
    那么什么时候释放,怎么释放呢?
    RunLoop内部有一个自动释放池,当RunLoop开启时,就会自动创建一个自动释放池,当RunLoop在休息之前会释放掉自动释放池的东西,然后重新创建一个新的空的自动释放池,当RunLoop被唤醒重新开始跑圈时,Timer,Source等新的事件就会放到新的自动释放池中,当RunLoop退出的时候也会被释放。
    注意:只有主线程的RunLoop会默认启动。也就意味着会自动创建自动释放池,子线程需要在线程调度方法中手动添加自动释放池。

    @autorelease{  
          // 执行代码 
    }
    

    附: 使用GCD也可是创建计时器,而且更为精确我们来看一下代码

    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        //创建队列
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
        //1.创建一个GCD定时器
        /*
         第一个参数:表明创建的是一个定时器
         第四个参数:队列
         */
        dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        // 需要对timer进行强引用,保证其不会被释放掉,才会按时调用block块
        // 局部变量,让指针强引用
        self.timer = timer;
        //2.设置定时器的开始时间,间隔时间,精准度
        /*
         第1个参数:要给哪个定时器设置
         第2个参数:开始时间
         第3个参数:间隔时间
         第4个参数:精准度 一般为0 在允许范围内增加误差可提高程序的性能
         GCD的单位是纳秒 所以要*NSEC_PER_SEC
         */
        dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
        
        //3.设置定时器要执行的事情
        dispatch_source_set_event_handler(timer, ^{
            NSLog(@"---%@--",[NSThread currentThread]);
        });
        // 启动
        dispatch_resume(timer);
    }
    

    引用文章 https://www.jianshu.com/p/b9426458fcf6

    相关文章

      网友评论

          本文标题:RunLoop详解

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