likes
comments
collection
share

RunLoop的二三事

作者站长头像
站长
· 阅读数 28

Runloop的简介

RunLoop 是与线程相关的事件处理循环,我们可以在这里安排操作和协调对从外面传进来的事件的接收。RunLoop 的目的是让我们的线程在有活干的时候工作,在没活干的时候进入睡眠。所以 RunLoop 能让程序保持活着,在没有消息可处理时休眠来节省 CPU 资源。

Runloop的详情

Runloop的作用:

RunLoop的二三事

此时的“结束”将不会被打印,因为当程序启动从main函数进来之后,UIApplicationMain(argc, argv, nil, appDelegateClassName);就创建了一个和主线程绑定的Runloop。Runloop 的存在,保住App的生命,让App 可以随时待命,处理用户的操作以及其他事件。否则的话,App刚启动就结束了!对于主线程的Runloop来说,这个描述很恰当,但是Runloop是和线程绑定的,其实这么说有点狭隘了。Runloop应该是用来保证当前线程的生命周期不被终结的一种方式。

Runloop的构成:

我们平时用NSRunloop是对CFRunLoopRef的封装,提供的是面向对象的 API。从开发层面来说,我们还是应该走进CFRunLoopRef来研究Runloop。

在 CoreFoundation 中关于 RunLoop 有 5 个类:CFRunloopRef、CFRunLoopModeRef、CFRunLoopSourceRef、CFRunLoopTimerRef、CFRunLoopObserverRef。执行NSLog(@"%@", [NSRunLoop currentRunLoop]);的打印缩略:RunLoop的二三事

一个Runloop对应多个mode,一个mode对应多个source、timer、observer。同一个时刻,RunLoop只能是在一个mode上面的运行。如果需要切换mode,只能是退出currentMode ,切换到指定的 mode 。对应关系如下图:RunLoop的二三事

CFRunLoopModeRef:

一个 RunloopMode 是若干个 source、timer 和 observer 的集合,mode过滤掉一些不想要的事件。

一个 RunLoop 在某个 mode 下运行时,不会接收和处理其他 mode 的事件 。要保持一个 mode 活着,就必须往里面添加至少一个 source、timer 或 observer 。

苹果公开的 mode 有两个:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。前者是默认的模式,程序运行的大多时候都处于该 mode 下,后者是滚动视图scrollerView及其子类滚动时为了界面流畅而用的 mode,这两个mode在执行的时候,相互独立,互不干扰。让不同的mode各司其职,对于程序运行的解耦很有好处。

CFRunLoop里面有一个伪mode叫做 kCFRunLoopCommonModes,它不是一个真正的 mode,而是若干个 mode 的集合。只要把事件加入到了 CommonModes 里面,就相当于添加到了它里面所有的 mode 中。

CFRunLoopSourceRef:

source就是事件输入源,分为三类:Port-Based Sources,Custom Input Sources,Cocoa Perform Selector Sources。现实情况下只有两种事件来源:source0 和 source1。

source0 是app内部的消息机制,使用时需要调用 CFRunLoopSourceSignal()来把这个 source 标记为待处理,然后掉用 CFRunLoopWakeUp() 来唤醒 RunLoop,让其处理这个事件。source1 是基于 mach_ports 的,用于通过内核和其他线程互相发送消息。

CFRunLoopTimerRef:

timer就是我们熟悉的OC里面的计时器NSTimer。我们用NSTimer的时候会有几个注意的点,一般情况下用scheduledTimerWithTimeInterval初始化的timer不用手动添加到Runloop,而用timerWithTimeInterval创建的timer要手动添加到Runloop里面。同时有一个经典的面试题目:

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { 
   dispatch_async(dispatch_get_global_queue(0, 0), ^{   
        NSLog(@"开始");  
        [self performSelector:@selector(testFunc) withObject:nil afterDelay:0];
        NSLog(@"结束");
    });}

- (void)testFunc {
    NSLog(@"调用");
}

此时是不会打印“调用”的,[self performSelector:@selector(testFunc) withObject:nil afterDelay:0];虽然添加进了当前子线程的Runloop,但是子线程的Runloop是默认不开启的。所以在[self performSelector:@selector(testFunc) withObject:nil afterDelay:0];之后要添加[[NSRunLoop currentRunLoop] run];来开启子线程的Runloop。

CFRunLoopObserverRef:

observer既观察者,这个观察者可以观察Runloop的7种状态:

RunLoop的二三事RunLoop的二三事

我从Demo的测试总得出的结论是:

主线程的Runloop在处理了启动后的timer、source、observer输入的事件之后,
进入休眠kCFRunLoopBeforeWaiting状态。
直到我自己写的定时器唤起Runloop到kCFRunLoopAfterWaiting状态,
每次处理完定时器testFunc的回调,就进入kCFRunLoopBeforeWaiting状态,
直至被下次计时器唤醒进去kCFRunLoopAfterWaiting状态。
往复不停的循环,直到定时器结束,Runloop进入休眠。

Runloop与线程的关系:

主线程有与之对应的mainRunLoop,可以通过[NSRunLoop mainRunLoop]或者在主线程里[NSRunLoop currentRunLoop]获取。子线程如果不主动或者通过某些方法隐式获取当前线程的Runloop的话,那么这个线程就没有Runloop。苹果没有创建runloop的方法,但是可以通过currentRunloop在当前线程创建Runloop。RunLoop和线程是一一对应的,可以有线程没有runloop,但是有runloop一定会有线程与之对应。 Runloop是以线程为key值放在一个全局Map里的

获取的方法大致如下:

 // 全局的 dictionary, key 是 pthread_t, value 是 CFRunLoopRefstatic CFMutableDictionaryRef __CFRunLoops = NULL;

CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {

    // 第一次进入时,创建全局 dictionary

    if (!__CFRunLoops) {

        // 创建可变字典

        CFMutableDictionaryRef dict = CFDictionaryCreateMutable();

        // 先创建主线程的 RunLoop

        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());

        // 主线程的 RunLoop 存进字典中

        CFDictionarySetValue(dict, pthread_main_thread_np(), mainLoop);
    }
    
    // 用 传进来的线程 作 key,获取对应的 RunLoop

    CFRunLoopRef loop = CFDictionaryGetValue(__CFRunLoops, t);
    

    // 如果获取不到,则新建一个,并存入字典

    if (!loop) {

        CFRunLoopRef newLoop = __CFRunLoopCreate(t);

        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);

    }

    return loop;

}

// 获取主线程的 RunLoop
CFRunLoopRef CFRunLoopGetMain(void) {
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np());
    return __main;
}

Runloop运行流程:

RunLoop的二三事

转载自:https://juejin.cn/post/6981821257507913742
评论
请登录