OC-runloop

407 阅读10分钟

什么是runloop

  • 运行循环
  • 内部其实就是do-while循环,在这个循环内部不断地处理各种任务(比如Source、Timer、Observer)

高级回答: Runloop是通过内部维护的事件循环来对事件/消息进行管理的一个对象

这里有两个重点

  1. 事件循环

  2. 事件/消息进行管理

什么是事件循环呢?
事件循环(状态切换)

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

            用户态——>内核态

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

            用户态<—— 内核态
什么是事件/消息进行管理呢? 
RunLoop 通过 mach_msg()函数接收、发送消息来进行管理。 
它的本质是调用函数 mach_msg_trap(),相当于是一个系统调用,会触发内核状态切换。 
可以做到在有事做的时候做事,没事做的时候,会由用户态切换到内核态,避免资源浪费。

如何实现事件、消息的管理 
mach_msg() 函数实际上是调用了一个 Mach 陷阱 (trap), 即函数mach_msg_trap(),陷阱这个概念在 Mach 中等同于系统调用。 
当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到内核态;
内核态中内核实现的 mach_msg() 函数会完成实际的工作,

所以说 Runloop的核心就是一个 mach_msg(),RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap() 这个地方。

image.png

RunLoop 的基本作用

  • 保持程序的持续运行:
    如果没有RunLoopmain()函数一执行完,程序就会立刻退出。
    而我们的 iOS 程序能保持持续运行的原因就是在main()函数中调用了UIApplicationMain函数,这个函数内部会启动主线程的RunLoop
  • 处理 App 中的的各种事件(比如触摸事件、定时器事件等);
  • 节省 CPU 资源,提高程序性能:该做事时做事,该休息时休息。

runloop的底层数据结构

RunLoop 的本质是什么?

答:本质是一个OC对象,内部也有isa指针。

RunLoop 对象

  • iOS 中有 2 套 API 来访问和使用RunLoop
    ① Foundation:NSRunLoop(是CFRunLoopRef的封装,提供了面向对象的 API)
    ② Core Foundation:CFRunLoopRef

  • NSRunLoopCFRunLoopRef都代表着RunLoop对象

  • NSRunLoop不开源,而CFRunLoopRef是开源的:Core Foundation 源码

CFRunLoopRef

RunLoop对象的底层就是一个CFRunLoopRef结构体,它里面存储着:

  • _pthread:RunLoop与线程是一一对应关系
  • _commonModes:存储着 NSString 对象的集合(Mode 的名称)
  • _commonModeItems:存储着被标记为通用模式的Source0/Source1/Timer/Observer
  • _currentModeRunLoop当前的运行模式
  • _modes:存储着RunLoop所有的 Mode(CFRunLoopModeRef)模式

// CFRunLoop.h
typedef struct __CFRunLoop * CFRunLoopRef;
// CFRunLoop.c
struct __CFRunLoop {
    pthread_t _pthread;  // 与线程一一对应
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    ...
};

CFRunLoopModeRef

  • CFRunLoopModeRef代表RunLoop的运行模式;
  • 一个RunLoop包含若干个 Mode,每个 Mode 又包含若干个Source0/Source1/Timer/Observer
  • RunLoop启动时只能选择其中一个 Mode,作为 currentMode;
  • 如果需要切换 Mode,只能退出当前 Loop,再重新选择一个 Mode 进入,切换模式不会导致程序退出;
  • 不同 Mode 中的Source0/Source1/Timer/Observer能分隔开来,互不影响;
  • 如果 Mode 里没有任何Source0/Source1/Timer/ObserverRunLoop会立马退出。
// CFRunLoop.h
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
// CFRunLoop.c
struct __CFRunLoopMode {
    CFStringRef _name;             // mode 类型,如:NSDefaultRunLoopMode
    CFMutableSetRef _sources0;     // CFRunLoopSourceRef
    CFMutableSetRef _sources1;     // CFRunLoopSourceRef
    CFMutableArrayRef _observers;  // CFRunLoopObserverRef
    CFMutableArrayRef _timers;     // CFRunLoopTimerRef
    ...
};

CFRunLoopSourceRef

  • RunLoop中有两个很重要的概念,一个是上面提到的模式,还有一个就是事件源事件源分为输入源(Input Sources)定时器源(Timer Sources)两种;
  • 输入源(Input Sources)又分为Source0Source1两种,以下__CFRunLoopSource中的共用体union中的version0version1就分别对应Source0Source1

Source0 和 Source1 的区别:

Input Sources区别
Source0需要手动唤醒线程:添加Source0RunLoop并不会主动唤醒线程,需要手动唤醒) ① 触摸事件处理 ② performSelector:onThread:
Source1具备唤醒线程的能力 ① 基于 Port 的线程间通信 ② 系统事件捕捉:系统事件捕捉是由Source1来处理,然后再交给Source0处理

CFRunLoopObserverRef

  • CFRunLoopObserverRef用来监听RunLoop的 6 种活动状态
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),          // 即将进入 RunLoop
    kCFRunLoopBeforeTimers = (1UL << 1),   // 即将处理 Timers
    kCFRunLoopBeforeSources = (1UL << 2),  // 即将处理 Sources
    kCFRunLoopBeforeWaiting = (1UL << 5),  // 即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),   // 刚从休眠中唤醒
    kCFRunLoopExit = (1UL << 7),           // 即将退出 RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU  // 表示以上所有状态
};

runloop 是怎么响应用户操作的, 具体流程是什么样的?

  • source1 捕捉用户触摸事件
  • source0去处理触摸时间

常见的几种Mode:

  • Default : App的默认Mode,通常主线程是在这个Mode下运行

  • UITracking: 界面跟踪Mode,用于ScrollView`追踪触摸滑动,保证界面滑动时不受其他Mode影响

  • Common :并不是一个真的模式,它只是一个标记,如:被标记的 Timer可以在Default模式和UITracking下运行。

基本用不到的Mode:

  • UIInitialization :私有的mode,App启动的时候的状态,加载出第一个页面后,就转成了Default

  • GSEventReceive系统的内部 Mode,通常用不到

runloop和线程的关系?

  • 线程和RunLoop是一一对应的关系.

  • 一个线程对应一个RunLoop,主线程的RunLoop默认在底层启动通过UIApplicationMain,子线程的RunLoop必须得创建,还得调用run方法来进行启动

  • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value 

  • RunLoop创建时机:线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建;

  • RunLoop销毁时机:RunLoop会在线程结束时销毁;

  • RunLoop一直循环不退出,必须得有运行循环模式,并且在这个模式中得存在Source(Source0,Source1)、Timer中的任意一个

runloop和自动释放池

  • 1)runloop启动的时候,会创建一个自动释放池
  • 2)runloop退出和即将休眠的时候,会销毁自动释放池

Event Loop

一个APP在前台静止着,此时,用户用手指点击了一下APP界面,那么过程就是下面这样的:

我们触摸屏幕,先摸到硬件(屏幕),屏幕表面的事件会先包装成Event,Event先告诉source(mach_port),source1唤醒RunLoop,然后将事件Event分发给source0,然后由source0来处理。

如果没有事件,也没有timer,则runloop就会睡眠,如果有,则runloop就会被唤醒,然后跑一圈。

主线程的 RunLoop 的启动过程

iOS 程序能保持持续运行的原因就是在main()函数中调用了UIApplicationMain函数,这个函数内部会启动主线程的RunLoop

image.png

RunLoop 与 NSTimer

  • NSTimer是由RunLoop来管理的,NSTimer其实就是CFRunLoopTimerRef
  • 如果我们在子线程上使用NSTimer,就必须开启子线程的RunLoop,否则定时器无法生效

runloop应用场景

在开发中,RunLoop 是一个让线程能随时处理事件但并不退出的机制,在不同的开发平台有着广泛的使用场景,以下为你详细介绍:

iOS/macOS 开发

  • 定时器(NSTimer)NSTimer 依赖于 RunLoop 来触发,只有当 RunLoop 处于运行状态,且注册了定时器的模式被激活时,定时器才会触发。例如,你可以使用 NSTimer 实现一个每隔一段时间更新 UI 的功能。

objc

// 创建一个定时器,每隔 1 秒触发一次
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                  target:self
                                                selector:@selector(updateUI)
                                                userInfo:nil
                                                 repeats:YES];
// 将定时器添加到当前 RunLoop 的默认模式中
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
  • 事件响应:当用户点击屏幕、滑动页面等操作产生的事件,会先被系统捕获,然后通过 RunLoop 分发给相应的处理函数。RunLoop 会不断地从事件队列中取出事件并处理,确保界面的流畅响应。
  • 手势识别:手势识别器(UIGestureRecognizer)也是依赖 RunLoop 来工作的。当用户进行手势操作时,RunLoop 会不断地检测触摸事件,并将其传递给手势识别器进行识别和处理。
  • 网络请求:在进行网络请求时,RunLoop 可以用于处理异步回调。例如,使用 NSURLSession 进行网络请求时,当请求完成后,会通过回调函数将结果返回。这些回调函数会在 RunLoop 的特定模式下被调用,确保在合适的时机处理网络响应。

其他开发场景

  • 线程保活:在某些情况下,你可能需要让一个线程一直保持运行状态,随时处理任务。这时可以使用 RunLoop 来实现线程的保活。例如,创建一个自定义线程,并在该线程中启动 RunLoop,使其不断循环处理事件。

objc

- (void)startCustomThread {
    self.customThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadEntryPoint) object:nil];
    [self.customThread start];
}

- (void)threadEntryPoint {
    @autoreleasepool {
        // 创建并启动 RunLoop
        [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
    }
}

  • 异步任务处理:可以利用 RunLoop 在后台线程中处理一些耗时的异步任务,避免阻塞主线程,保证界面的流畅性。例如,在后台线程中进行数据的加载和处理,处理完成后通过回调通知主线程更新 UI。

  • 控制线程生命周期(线程保活)

self.thread = [[NSThread alloc]initWithBlock:^{

        // 在线程里面开启RunLoop,self.thread就变成了常驻线程,可随时添加任务,并交于RunLoop处理

        [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];

        [[NSRunLoop currentRunLoop] run];
    }];

    // 开启线程

    [self.thread start];
  • 解决NSTimer在滑动时停止工作的问题

  • 监控应用卡顿

  • 性能优化

  • 定时器(Timer)、PerformSelector

  • GCD:dispatch_async(dispatch_get_main_queue(), ^{ });

  • 事件响应、手势识别、界面刷新

  • 网络请求

  • AutoreleasePool

NSTimer 和 CADisplayLink 存在的问题

不准时:NSTimeCADisplayLink底层都是基于RunLoopCFRunLoopTimerRef的实现的,也就是说它们都依赖于RunLoop。如果RunLoop的任务过于繁重,会导致它们不准时。
比如NSTimer每1.0秒就会执行一次任务,Runloop每进行一次循环,就会看一下NSTimer的时间是否达到1.0秒,是的话就执行任务。但是由于Runloop每一次循环的任务不一样,所花费的时间就不固定。假设第一次循环所花时间为 0.2s,第二次 0.3s,第三次 0.3s,则再过 0.2s 就会执行NSTimer的任务,这时候可能Runloop的任务过于繁重,第四次花了0.5s,那加起来时间就是 1.3s,导致NSTimer不准时。
解决方法:使用 GCD 的定时器。GCD 的定时器是直接跟系统内核挂钩的,而且它不依赖于RunLoop,所以它非常的准时。示例如下:


dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);
    
    //创建定时器
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    //设置时间(start:几s后开始执行; interval:时间间隔)
    uint64_t start = 2.0;    //2s后开始执行
    uint64_t interval = 1.0; //每隔1s执行
    dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0);
    //设置回调
    dispatch_source_set_event_handler(timer, ^{
       NSLog(@"%@",[NSThread currentThread]);
    });
    //启动定时器
    dispatch_resume(timer);
    NSLog(@"%@",[NSThread currentThread]);
    
    self.timer = timer;
/*
2020-02-01 21:34:23.036474+0800 多线程[7309:1327653] <NSThread: 0x600001a5cfc0>{number = 1, name = main}
2020-02-01 21:34:25.036832+0800 多线程[7309:1327705] <NSThread: 0x600001acb600>{number = 7, name = (null)}
2020-02-01 21:34:26.036977+0800 多线程[7309:1327705] <NSThread: 0x600001acb600>{number = 7, name = (null)}
2020-02-01 21:34:27.036609+0800 多线程[7309:1327707] <NSThread: 0x600001a1e5c0>{number = 4, name = (null)}
 */