RunLoop

作者: 叔简 | 来源:发表于2021-06-17 16:40 被阅读0次

    概念

    RunLoop是通过内部维护事件循环来对事件/消息进行管理的一个对象

    事件循环

    1. 没有事做就休眠,避免占用资源 (用户态 -> 内核态)
    2. 有消息需要处理,立刻被唤醒 (内核态 -> 用户态)
    3. RunLoop事件循环的关键点就是状态的切换,有事就做,无事就休眠

    数据结构

    CFRunLoop

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

    2. CFRunLoop的几种Mode
      2.1 Pthread
      2.2 currentMode -> CFRunLoopMode
      2.3 Modes 是一个NSMutableSet<CFRunLoopMode *>集合体
      2.4 commonModes

      • 在系统公开的Mode中,有NSDefaultRunLoopMode和UITrackingRunLoopMode,这两个Mode都被标记为commonMode(这两个Mode都是在主线程之上的),UITrackingMode
      • 此外系统还提供了一个操作common标记字符串NSRunLoopCommonModes,可以通过将ModeName添加到RunLoop的CommonModes中(这并不是一个实际意义上的Mode,只是一个Mode的集合体)
      • DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态

      2.5 CommonModeItems(集合体)

      • 多个obsever
      • 多个Timer
      • 多个Source

    CFRunLoopMode(事件管家)

    • name NSDefaultRunLoopMode,通过这个名称来找到RunLoop模式
    • sources0
    • sources1
    • observers
    • timers

    Source/Timer/Observer

    • CFRunLoop具备两个事件:source0(需要手动唤醒线程,从内核态切换到用户态),source1(具备自主唤醒线程的能力)
    • CFRunLoopTimer:基于事件的定时器,和timer是toll_free bridge的
    • CFRunLoopObserver:观测时间点
      1. kCFRunLoopEntry(入口时机)
      2. kCFRunLoopBeforeTimers(将要对timer事件进行观测)
      3. kCFRunLoopBeforeSources(将要对sources事件进行观测)
      4. kCFRunLoopBeforeWaiting(将要进入休眠状态,此处发生用户态到内核态的切换)
      5. kCFRunLoopAfterWaiting(唤醒,内核态切换到用户态)
      6. kCFRunLoopExit(RunLoop退出通知)

    RunLoop数据结构之间的关系

    • RunLoop和线程为一一对应关系
    • RunLoop对Mode为一一对应关系
    • Mode对Source/Timer/Observer为一对多关系

    RunLoop的Mode

    • RunLoop只能同时处理一个Mode下的事件,例如,在滑动时,timer定时器将会失效

    事件循环机制

    1. 调用CFRunLoopRun()函数
    2. RunLoop启动之后,发送一个通知,告诉系统即将进入RunLoop
    3. 将要处理Timer/Source0事件
    4. 处理source0事件
    5. 假如有source1事件需要处理,执行一个goto语句,唤醒切换内核态到用户态,进行事件处理,跳转到步骤7
    6. 没有source1事假需要处理,线程即将进入休眠状态,并发送通知
    7. 处理唤醒时收到的信息,之后跳回步骤2
    8. 线程进入休眠状态,发送通知
    9. 即将退出RunLoop,发送通知
    • RunLoop的核心
    1. 用户态 -> main函数 -> mach_msg() -> 系统调用 -> 内核态 -> mach_msg() -> 用户态
    2. 其核心为,用户态和内核态的切换,来完成休眠和唤醒事件

    RunLoop和NSTimer事件

    1. 当我们在滑动界面时,会发现timer处于停顿状态,这是因为,timer事件位于defaultMode上,而滑动事件处于UITrackingMode上,而RunLoop同时只能处理一个事件,所以,在滑动tableview时,我们的timer事件就会失效停止
    2. 此时,我们将timer事件添加到CommonMode上,就能解决tableview滑动时,timer失效的问题。因为,UITrackingMode和defaultMode,都是注册在CommonMode中的事件,而timer添加到CommonMode上时,RunLoop,在滑动的时候,相当于只处理了CommonMode,就不会发生timer失效停顿问题

    RunLoop和多线程

    1. RunLoop和线程之间是一一对应的
    2. 自己创建的线程,并没有RunLoop事件,需要自己手动添加RunLoop
    3. 实现一个常驻线程
      3.1 为当前线程开启一个RunLoop
      3.2 向该RunLoop中添加一个Port/Sourece等维持RunLoop的事件循环
      3.3 启动该RunLoop(CFRunLoopRun()函数来启动)
    4. 关于performSelector方法
      4.1 在此方法中,把当前线程的对象传递给其他线程对象,此方法会被加入到目标线程的RunLoop中,该runloop使用默认mode--NSRunLoopCommonModes。当该线程的runloop执行的时候,它会以此出队列,然后执行想要执行的方法
    [self performSelector:@selector(testRun) onThread:self.thread withObject:nil waitUntilDone:NO];
    

    4.2 所以在它所添加的线程Thread中,我们需要手动开启RunLoop线程,否则testRun方法将不执行

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

    我们都知道,Runloop启动前内部必须要有一个Timer/Observer/Source,所以在这里的runloop执行run之前,先创建了一个NSPort添加进去了。通常情况下,调用者需要持有这个port,并在外部线程通过这个port发送消息到loop内,但这里添加port只是为了让runloop不至于退出,并没有实际的发送消息。

    如何保证子线程数据更细UI时,不打断滑动效果

    1. 滑动的Mode在在UITrackingMode上的(在主线程上)
    2. 数据请求是在子线程进行,切换回主线程进行更新的,我们可以把这块的逻辑包装起来,放置于defaultMode下,在进行滑动的时候,defaultMode停止不会被处理,停止滑动时,不再处理UITrackingMode,这时再进行defaultMode的数据更新
    3. 这边也可以添加一个timer事件到RunLoop中,保活常驻线程,在timer的block回调中执行耗时操作,(操作原理同步骤2)这样也可使tableview滑动流畅(一个常驻线程使用的场景)

    相关文章

      网友评论

          本文标题:RunLoop

          本文链接:https://www.haomeiwen.com/subject/zpecyltx.html