iOS 底层探索之Runloop

654 阅读9分钟

本篇是探索底层Runloop,目的是能够深入理解Runloop是干什么用的?什么时候用?怎么用?

1、什么是runloop?

runloop是一个循环,它在持续不断的跑圈,iOS应用程序刚打开时,就创建了一个主线程,并默认创建了Runloop保持主线程的持续运行。

我们到官方文档搜索一下Runloop,如图所示

image.png
发现找不到Runloop,再尝试搜索thread,发现在线程介绍里面竟然出现了Runloop字样,如图
image.png

如此可见,Runloop和线程之间有着不清不楚的关系。

再来看一下CFRunloop源码中的CFRunLoopRun函数

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循环。 结合官方文档提供的运行循环结构看一下,Runloop是如何执行的。

image.png

从上图中我们可以看出,Runloop就是依附在线程上的循环,通过输入源(Input sources)和定时源(Timer sources)接收事件,然后交给线程去处理事件。

所以什么是Runloop? Runloop就是一个循环,为了线程而生,它的本质是一个do...while循环

2、Runloop的作用

  • 1、保持程序持续的运行 一般情况下,线程在执行完任务就会退出,如果我们不希望线程退出,还想让它执行更多的任务,就需要用到Runloop了。
  • 2、接收并处理App中的各种事件 Runloop在循环时,通过输入源(input source)和定时源(timer source)接收App事件(触摸事件、UI刷新时间、定时器、performSelector)。
  • 3、提升性能 在线程不工作时休眠,节省CPU资源。

以上就是Runloop的作用,这里只是概括一下,后面会有具体到用法的Runloop应用。

3、Runloop和线程的关系

接下来我们结合Runloop源码看看它和线程之间的关系,找到_CFRunLoopGet0这个函数,它的作用是获取runloop对象

//CFRunloop.c
//这个类是获取Runloop对象的
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (!__CFRunLoops) {//CFRunloops是存放runloop和线程对应关系的字典
        //创建存放runloop和线程的字典
        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        //获取主线程runloop
        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
        //将主线程和主线程对应的Runloop对象mainLoop添加到字典中
        CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
        if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
            CFRelease(dict);
        }
        CFRelease(mainLoop);
        __CFSpinLock(&loopsLock);
    }
    //获取子线程对应的runloop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFSpinUnlock(&loopsLock);
    if (!loop) {
        //依据线程t创建Runloop对象
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFSpinLock(&loopsLock);
        
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        if (!loop) {
            //绑定线程和runloop到字典中
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            loop = newLoop;
        }
    }
}

上述代码中有一段CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);,意思是以线程为key,runloop为value,将runloop存储到一个全局的字典中。 至此我们得出了第一个结论:线程和Runloop是一一对应的关系。

再看这段获取主线程runloop的代码CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());,意思是创建一个主线程的Runloop。 至此得出第二个结论:Runloop是以线程为参数创建的,并保存在全局的字典里。

再看后半段代码

//获取子线程对应的runloop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFSpinUnlock(&loopsLock);
    if (!loop) {
        //依据线程t创建Runloop对象
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFSpinLock(&loopsLock);
        
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        if (!loop) {
            //绑定线程和runloop到字典中
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            loop = newLoop;
        }
    }

这段代码的意思是先从__CFRunLoops字典中获取Runloop对象,若没有,则以线程为参数创建一个,并存储到__CFRunLoops字典里,至此我们知道了第三个结论:主线程的Runloop由系统自动创建,子线程的Runloop需要在子线程里手动获取Runloop时创建。

综上我们知道了Runloop和线程的关系:
1、线程和Runloop是一一对应的关系。
2、Runloop是以线程为参数创建的,并保存到全局的字典里。
3、主线程的Runloop由系统自动创建,子线程的Runloop需要在子线程里手动获取Runloop时创建。
4、Runloop在第一次获取时创建,在线程销毁时随之销毁。

4、Runloop的五个对象

  1. __CFRunLoop * CFRunLoopRef;
  2. __CFRunLoopSource * CFRunLoopSourceRef;
  3. __CFRunLoopObserver * CFRunLoopObserverRef;
  4. __CFRunLoopTimer * CFRunLoopTimerRef;
  5. CFRunloopModeRef(为什么这个这么写呢,因为Runloop并没有暴露RunloopMode这个对象)

下面逐一讲一下Runloop这几个对象的含义和它们之间的关系,如图

上图就是Runloop对象、Mode、Source、Observer、Timer之间的关系。 一个Runloop包含若干个CFRunloopModeRef(运行模式),一个CFRunloopModeRef又包含若干个CFRunLoopSourceRef(输入源)/CFRunLoopTimerRef(定时源)/CFRunLoopObserverRef(观察源),但是Runloop同一时间只能指定一个CFRunloopModeRef(运行模式),如果要切换CFRunloopModeRef,需要先退出Runloop,再指定一个CFRunloopModeRef(运行模式)进入。

为什么是这样的结构? 答:这样做主要是为了分离Source/Timer/Observer,让其互不影响。

4.1、CFRunLoopRef(Runloop对象)

CFRunLoopRef 是 Core Foundation 框架下 RunLoop 对象类,可通过如下方式获取

// 获得当前线程的 RunLoop 对象
CFRunLoopGetCurrent(); 
// 获得主线程的 RunLoop 对象
CFRunLoopGetMain(); 

也可以使用Foundation框架中的NSRunloop获取封装过的Runloop,NSRunloop是对CFRunLoopRef的封装

// 获得当前线程的 RunLoop 对象
[NSRunLoop currentRunLoop]; 
// 获得主线程的 RunLoop 对象
[NSRunLoop mainRunLoop]; 
4.2、CFRunLoopSourceRef(输入源)

先看一下源码

struct __CFRunLoopSource {
    CFRuntimeBase _base;
    uint32_t _bits;
    pthread_mutex_t _lock;
    CFIndex _order;            /* immutable */
    CFMutableBagRef _runLoops;
    union {
        //对应Source0
        CFRunLoopSourceContext version0;    /* immutable, except invalidation */
        //对应Source1
        CFRunLoopSourceContext1 version1;    /* immutable, except invalidation */
    } _context;
};

从源码看出Source分为2个版本

  • 1、Source0:这种版本的Source不能主动触发事件,得是用户或开发者手动触发,比如触摸事件和performSelector等App内部事件(UIEvent),需要先进行CFRunLoopSourceSignal标记,再通过CFRunLoopWakeUp唤醒Runloop处理事件。
  • 2、Source1:基于Port,用于通过内核和线程之间通信的,这种Source可以主动唤醒Runloop线程,一会儿用例子看一下。

先看一个触摸事件触发Source0的例子,创建个工程,上面放一个按钮,在点击事件的回调中打断点,如图

image.png
在左侧栏目中是触发的方法,如图
image.png
我们可以看到,用户的触摸事件,果然触发的是Source0。

下面是Source0的使用例子

要创建一个Source0输入源,需要执行六步走, 1、创建Context上下文 2、创建Source0输入源对象 3、获取Runloop 4、绑定Runloop、Source0和mode 5、标记执行信号CFRunloopSourceSignal 6、唤醒CFRunLoopWakeUp

- (void)source0Demo{
    //1、创建Context上下文
    CFRunLoopSourceContext context = {
        0,
        NULL,
        NULL,
        NULL,
        NULL,
        NULL,
        NULL,
        schedule,
        cancel,
        perform,
    };
    /**
     2、创建CFRunLoopSourceRef
     参数一:传递NULL或kCFAllocatorDefault以使用当前默认分配器。
     参数二:优先级索引,指示处理运行循环源的顺序。这里我传0为了的就是自主回调
     参数三:为运行输入源保存上下文信息的结构
     */
    CFRunLoopSourceRef source0 = CFRunLoopSourceCreate(CFAllocatorGetDefault(), 0, &context);
    //3、获取Runloop
    CFRunLoopRef rlp = CFRunLoopGetCurrent();
    //4、绑定Source、Runloop、Mode,此时我们的source就进入待绪状态
    CFRunLoopAddSource(rlp, source0, kCFRunLoopDefaultMode);
    //5、 一个执行信号
    CFRunLoopSourceSignal(source0);
    //6、 唤醒 run loop 防止沉睡状态
    CFRunLoopWakeUp(rlp);
    // 取消 移除
//    CFRunLoopRemoveSource(rlp, source0, kCFRunLoopDefaultMode);
    CFRelease(rlp);
}

void schedule(void *info, CFRunLoopRef rl, CFRunLoopMode mode){
    NSLog(@"准备处理事件");
}

void perform(void *info){
    NSLog(@"少年,执行事件吧");
}

void cancel(void *info, CFRunLoopRef rl, CFRunLoopMode mode){
    NSLog(@"移除输入源,事件停止执行");
}

从代码中可以看出,若要让Runloop执行Source0的事件,需要先发出一个执行信号CFRunLoopSourceSignal,再调用CFRunLoopWakeUp唤醒Runloop执行任务。

控制台打印效果如下

image.png

下面是Source1使用Port进行线程间通讯的例子

创建

@interface ViewController ()<NSPortDelegate>
@property (nonatomic, strong) NSPort* subThreadPort;//主线程Port
@property (nonatomic, strong) NSPort* mainThreadPort;//子线程Port
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self portCommunicateTest];
}
- (void)portCommunicateTest{
    self.mainThreadPort = [NSPort port];
    self.mainThreadPort.delegate = self;
    // port - source1 -- runloop
    //port是操作Source1的,所以同样依赖于runloop
    [[NSRunLoop currentRunLoop] addPort:self.mainThreadPort forMode:NSDefaultRunLoopMode];

    //创建子线程
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //实例化子线程对应的Port
        self.subThreadPort = [NSPort port];
        self.subThreadPort.delegate = self;
        
        [[NSRunLoop currentRunLoop] addPort:self.subThreadPort forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
    });
}

//NSPort的代理方法,线程间通信的回调
- (void)handlePortMessage:(id)message {
   NSLog(@"当前线程是 == %@", [NSThread currentThread]); // 3 1
    NSLog(@"传来的消息内容 = %@", [[NSString alloc] initWithData:[message valueForKey:@"components"][0] encoding:NSUTF8StringEncoding]);
    sleep(1);
    if (![[NSThread currentThread] isMainThread]) {
        //像子线程的Port发送消息
        NSMutableArray* components = [NSMutableArray array];
        NSData* data = [@"world" dataUsingEncoding:NSUTF8StringEncoding];
        [components addObject:data];

        [self.mainThreadPort sendBeforeDate:[NSDate date] components:components from:self.subThreadPort reserved:0];
    }
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    //注意,component必须以NSData的形式传递
    NSMutableArray* components = [NSMutableArray array];
    NSData* data = [@"Hello" dataUsingEncoding:NSUTF8StringEncoding];
    [components addObject:data];
    
    [self.subThreadPort sendBeforeDate:[NSDate date] components:components from:self.mainThreadPort reserved:0];
}

点击屏幕后,打印结果如下

image.png
在回调消息处打断点,发现如图所示
image.png
看到左侧显示的输入源是Source1,至此使用NSPort进行线程间通信的例子执行完毕,真可爱,线程还能这么玩。

4.3、CFRunloopModeRef

Runloop有五种运行模式

    1. UIInitializationRunLoopMode :在App刚启动进入的运行模式,启动完成后会切换到kCFRunLoopDefaultMode,从此不再使用。
    1. kCFRunLoopDefaultMode: 默认运行模式,在主线程运行在这个模式下。
    1. UITrackingRunLoopMode:界面跟踪模式,当见面滚动时,会切换到这个运行模式下,保证不收其他模式的影响。
    1. GSEventReceiveRunLoopMode: 接受系统事件的内部运行模式。
    1. kCFRunLoopCommonModes: 占位模式,通常用来标记kCFRunLoopDefaultModeUITrackingRunLoopMode,如果NSTimer加入这个模式,将不受运行模式切换的影响。
4.4、CFRunloopTimerRef

CFRunloopTimerRef是一个时间触发器,它包含一个时间长度和一个回调(函数回调),在加入Runloop时,Runloop会注册一个时间点,经过时间长度后,Runloop会被唤醒执行回调。Timer的底层就是一个CFRunloopTimerRef,它受Mode切换的影响。如果把Timer加入到kCFRunLoopCommonModes就不会受切换影响了,像下面这样

NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0f repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"log NSTimer runloop");
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
4.5、CFRunloopObserverRef

CFRunloopObserverRef是一个观察者,用来监控Runloop的状态的,它分为以下状态

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop 1
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer 2
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source 4
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠 32
    kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒 64
    kCFRunLoopExit          = (1UL << 7), // 即将退出Loop 128
};

举个例子

- (void)obseverDemo{
    //1、创建观察者上下文
    CFRunLoopObserverContext context = {
        0,
        ((__bridge void *)self),
        NULL,
        NULL,
        NULL
    };
    //2、获取当前Runloop对象
    CFRunLoopRef rlp = CFRunLoopGetCurrent();
    //3、创建观察者CFRunLoopObserverRef
    CFRunLoopObserverRef observerRef = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, runLoopObserverCallBack, &context);
    //4、将观察者observer和runloop对象、Mode关联起来
    CFRunLoopAddObserver(rlp, observerRef, kCFRunLoopDefaultMode);
}

void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
    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;
    }
}

在我们滚动视图时,控制台打印如下

image.png

由此可见,官方说的对☺,确实可以通过CFRunloopObserverRef监听Runloop的状态。

Runloop的应用有很多,之前在项目中应用的比较深的是将对CPU压力比较大的UI任务拆分成多个小任务,通过监听Runloop的Observer空闲时机,在空闲时强制其执行小任务,高效利用系统资源提升性能。 RunLoopWorkDistribution了解一下(😏)