全方位剖析iOS高级技术问题(七)之RunLoop相关问题

3,174 阅读8分钟

本文主要内容

一.RunLoop的概念
二.RunLoop的数据结构
三.事件循环的实现机制
四.RunLoop与NSTimer
五.RunLoop与多线程

截屏2022-08-23 11.14.22.png

一.RunLoop的概念

RunLoop是通过内部维护的事件循环来对事件/消息进行管理的一个对象。 问题1:什么是事件循环?

  • 没有消息需要处理时,休眠以避免资源占用;

截屏2022-08-23 15.01.48.png

  • 有消息需要处理时,立刻被唤醒。

截屏2022-08-23 15.01.56.png

`扩展`
内核态:在一个进程中,如果有系统调用,此时进程处于内核态,执行文件操作、网络数据发送等任务,此时特权级别比较高,为0级。
用户态:当一个进程执行用户自己的代码时,处于用户态,此时特权级别比较低,为3级。
所有用户程序都是运行在用户态的,但是有时候程序确实需要做一些内核态的事情,例如执行文件操作、网络数据发送等操作;而唯一可以做这些事情的就是操作系统,所以此时程序就需要操作系统请求以程序的名义来执行这些操作。
以下是操作系统内存空间分布图:

截屏2022-08-23 15.24.20.png

问题2:为什么程序中的main函数能够保证不退出?

  • 在main函数中,调用了UIApplicationMain函数,此函数中会启动主线程的runloop 。runloop是对事件循环的维护机制,可以做到"有事做的时候去做事,没事做的时候从用户态切换到内核态,避免资源的占用,使当前线程处于一个休眠状态"。

二.RunLoop的数据结构

NSRunLoop是CFRunLoop的封装,提供了面向对象的API。

NSRunLoop位于Foundation中,位于Core Foundation。CFRunLoop的源码是开源的!

image.png

2.1 RunLoop的数据结构

  • CFRunLoop
  • CFRunLoopMode
  • Source/Timer/Observer

CFRunLoop

  • pthread:一一对应,RunLoop和线程的关系;
  • currentMode:CFRunLoopMode数据结构;
  • modes:NSMutableSet集合<CFRunLoopMode*>;
  • commonModes:NSMutableSet集合<NSString*>;
  • cmomonModeltems:多个Observer、多个Timer、多个Source组成。

截屏2022-08-23 15.57.58.png

问题3:RunLoop和线程之间的关系
1.线程和RunLoop之间的关系是一一对应的;
2.主线程的RunLoop是默认开启的,子线程的RunLoop默认是不开启的;
3.子线程的RunLoop在主动获取的情况下,才会创建;
4.子线程的RunLoop在线程结束时,才会销毁。

问题4:commonModes的作用
1.在iOS上对应的是NSRunLoopCommonModes; 2.commonMode不是实际存在的一种Mode; 3.是同步Source/Timer/Observer到多个Model中的一种技术方案;

CFRunLoopMode

  • name:NSDefaultRunLoopMode
  • sources0:MutableSet
  • source1:MutableSet
  • observers:MutableArray
  • timers:MutableArray

截屏2022-08-23 16.35.36.png

CFRunLoopSource

  • source0:需要手动唤醒线程;
  • source1:具备唤醒线程的能力。

CFRunLoopTimer
基于事件的定时器,和NSTimer是toll-free birdged(免费桥转换)的。

CFRunLoopObserver
观测时间点:

  • kCFRunLoopEntry:入口时机;
  • kCFRunLoopBeforeTimers:通知观察者RunLoop将对timer相关事件进行处理;
  • kCFRunLoopBeforeSources:将要处理source事件;
  • kCFRunLoopBeforeWaiting:将要进入休眠状态,从用户态切换到内核态;
  • kCFRunLoopAfterWaiting:从用户态切换到内核态不久后
  • kCFRunLoopExit:RunLoop退出通知

2.2 各个数据结构之间的关系

截屏2022-08-23 17.35.57.png

问题5:RunLoop、Model、Source/Timer/Observer关系

  • RunLoop和Model是一对多的关系(从CFRunLoop的数据结构modes)
  • Model和Source/Timer/Observer是一对多的关系。

问题6:RunLoop为什么有多个Model
这样设计的原因是为了起到事件屏蔽的效果。

截屏2022-08-23 17.49.13.png

当RunLoop运行在Mode1时,只能接收处理Mode1上的source1、observer、timers事件回调,不能接收其他Mode上source/observer/timer事件回调,起到了事件屏蔽的效果。

三.事件循环的实现机制

无论是NSRunLoop还是CFRunLoop最终都会调用到CFRunLoopRun()。

3.1 事件循环机制的实现流程

  • 1.在RunLoop启动后,先发送通知告知观察者当前RunLoop即将启动;
  • 2.之后RunLoop将要处理Timer/Source0事件的通知发送;
  • 3.接着进入Source0事件的处理;
  • 4.此时如果有Source1要处理,会通过goto语句实现来进行代码逻辑的调转,处理唤醒时收到的消息(第8步);
  • 5.如果没有Source1需要处理,线程将要休眠(第6步),同时发送通知到Observer;
  • 6.从用户态到内核态的切换,线程休眠,等待唤醒;
    • 唤醒条件:Source1、Timer事件、外部手动唤醒
  • 7.线程刚被唤醒,发送通知给观察者;
  • 8.处理唤醒时收到的消息;
  • 9.再次回到第2步。

截屏2022-08-24 09.43.07.png

问题7:一个处于休眠状态的RunLoop通过哪些事件能唤醒它?

  • Source1回调;
  • Timer事件
  • 外部手动唤醒

问题8:点击APP图标,从程序启动、运行、退出这个过程当中,系统都发生了什么?

  • 程序启动后,调用main函数后,会调用UIApplicationmain函数,此函数内部会启动主线程的RunLoop,经过一系列处理,最终主线程RunLoop处于休眠状态;
  • 如果此时点击了屏幕,会产生一个mach_port,基于mach_port最终转成Source1,唤醒主线程,运行处理;
  • 当把程序杀死后,RunLoop退出,并且发送通知给观察者。RunLoop退出后线程即刻销毁。

3.2 RunLoop的核心

  • 在main函数中,经过一些列处理,会调用系统函数mach_msg(),就发生了系统调用,此时会从用户态转为核心态;
  • 在内核态下,一定条件时(Source1/Timer事件/外部手动唤醒),mach_msg()会返回给调用方,此时程序会从核心态转为用户态。 截屏2022-08-24 10.02.33.png

四.RunLoop与NSTimer

问题9:滑动TableView的时候定时器还会生效吗?
不会生效

  • 1.TableView正常情况是运行在kCFRunLoopDefaultMode模式下,当滑动时会发生Mode的切换,切换到UITrackingRunLoopMode模式。
  • 2.当把Timer/Source/Observer添加到某一个Mode(kCFRunLoopDefaultMode)上面后,如果当前RunLoop运行在另一个Mode(UITrackingRunLoopMode)上,对应的Timer/Source/Observer无法进行处理和回调。

截屏2022-08-24 10.09.10.png

解决方案:使用如下函数将Timer/Source/Observer同步到多个Mode中
void CFRunLoopAddTimer(runLoop, timer, commonMode)

五.RunLoop与多线程

  • 线程和RunLopp一一对应
  • 自己创建的线程默认没有RunLoop

问题10:怎样实现一个常驻线程?

  • 为当前线程开启一个RunLoop;
  • 向该RunLoop中添加一个Port/Source等维持RunLoop的事件循环;
  • 启动该RunLoop。

关键代码逻辑
HGObject.h

#import "HGObject.h"

#implementation HGObject

// 自定义线程
static NSThread *thread = nil;
// 标记是否要继续事件循环
static BOOL runAlways = YES;

+ (NSThread *)threadForDispatch {
    if (thread == nil) {
        // 创建线程:线程安全 
        @synchronized(self) {
            if (thread == nil) {
                thread = [[NSThread alloc] initWithTarget: self selector: @selector(runRequest) object: nil];
                [thread setName: @"com.tonlyele.xxx"];
                // 启动
                [thread start];
            }
        }
    }
    return thread;
}

// 常驻线程实现
+ (void)runRequest {
    
    // 创建一个Source
    CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
    CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
    
    // 创建RunLoop,同时向RunLoop的DefaultMode下面添加Source
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    
    // 如果可以运行
    while(runAlways) {
        @autoreleasepool {
            // 令当前RunLoop运行在DefaultMode下面
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
        }
    }
    
    // 某一时机静态变量runAlways = NO时,可以保证跳出RunLoop,线程退出
    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    CGRelease(source);
}

@end

实现常驻线程代码

// 创建线程
NSThread *subThread = [[NSThread alloc] initWithTarget: self selector: @selector(subThreadEntryPoint) object: nil];
[subThread setName: @"HGThread"];
[subThread start];

// 开启RunLoop
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 如果注释如下行,子线程中的任务无法执行
[runLoop addPort: [NSMachPort port] forMode: NSRunLoopCommonModes];
[runLoop run];

本文总结

问题1:什么是RunLoop,它是怎样做到有事做事,没事休息的?

  • RunLoop是一个通过内部循环对事件或消息进行管理的对象;
  • 程序运行会调用main函数,在main函数中调用UIApplicationMain,在此函数中会启动主线程的RunLoop;
  • RunLoop运行后,会调用系统mac_msg()方法,发生系统调用,会使程序从用户态转为核心态,此时线程处于休眠状态;
  • 当外界条件(source回调/Timer事件/Observer)变化时,mach_msg()函数会使得程序从核心态转为用户态,此时线程处于唤醒状态。

问题2:RunLoop与线程的关系

  • RunLoop与线程是一一对应的;
  • 一个线程默认是没有RunLoop的,需要手动创建(主线程除外)。

问题3:如何实现一个常驻线程?

  • 1.自定义线程,并为线程开启一个RunLoop;
  • 2.向RunLoop中添加一个Port/Source等维持RunLoop的事件循环;
  • 3.启动该RunLoop

问题4:怎样保证子线程数据回来更新UI的时候,不打断用户的滑动操作?

  • 1.用户滑动时,当前的RunLoop运行在UITrackingRunLoopMode模式下;
  • 2.而网络请求一般放在子线程进行,子线程返回给主线程的数据要抛给主线程用来更新UI。可以把子线程请求数据抛回给主线程进行UI更新的逻辑,包装起来提交到主线程的NSDefaultRunLoopMode模式下;
  • 3.当当前用户正在滑动操作时处于UITrackingRunLoopMode模式下,分派到NSDefaultRunLoopMode模式下的任务不会执行;
  • 4.当停止滑动后,当前线程会切换到NSDefaultRunLoopMode模式下,处理子线程上抛给主线程的UI更新的任务,这样就不会打断用户的滑动操作。

有任何问题,欢迎👏各位评论指出!觉得博主写的还不错的麻烦点个赞喽👍