RunLoop详解

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • Runloop 是和线程紧密相关的一个基础组件,是很多线程有关功能的幕后功臣。尽管在平常使用中几乎不太会直接用到,...
    jackyshan阅读 10,043评论 10 75
  • 此文转载自深入理解RunLoop,为了方便自己阅读,发在简书上RunLoop 是 iOS 和 OS X 开发中非常...
    渐行渐远ty阅读 469评论 0 0
  • 不得不说,人的惰性是真可怕啊。从上周六就到写runLoop的建议开始,星期三告诉自己从星期四开始着手写这篇博客。然...
    老司机Wicky阅读 7,286评论 20 137
  • 概述 RunLoop作为iOS中一个基础组件和线程有着千丝万缕的关系,同时也是很多常见技术的幕后功臣。尽管在平时多...
    阳明AI阅读 1,190评论 0 17
  • 感赏前夫昨天在我下班后快十一点的时候发信息关心我是否快到家了。 感赏昨天洗完澡到床上的时候,他跟我说我明天早班,让...
    离不若阅读 430评论 0 0

友情链接更多精彩内容