概念
RunLoop是通过内部维护事件循环来对事件/消息进行管理的一个对象
事件循环
- 没有事做就休眠,避免占用资源 (用户态 -> 内核态)
- 有消息需要处理,立刻被唤醒 (内核态 -> 用户态)
- RunLoop事件循环的关键点就是状态的切换,有事就做,无事就休眠
数据结构
CFRunLoop
-
NSRunLoop是CFRunLoop的封装,提供了面向对象的API
-
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:观测时间点
- kCFRunLoopEntry(入口时机)
- kCFRunLoopBeforeTimers(将要对timer事件进行观测)
- kCFRunLoopBeforeSources(将要对sources事件进行观测)
- kCFRunLoopBeforeWaiting(将要进入休眠状态,此处发生用户态到内核态的切换)
- kCFRunLoopAfterWaiting(唤醒,内核态切换到用户态)
- kCFRunLoopExit(RunLoop退出通知)
RunLoop数据结构之间的关系
- RunLoop和线程为一一对应关系
- RunLoop对Mode为一一对应关系
- Mode对Source/Timer/Observer为一对多关系
RunLoop的Mode
- RunLoop只能同时处理一个Mode下的事件,例如,在滑动时,timer定时器将会失效
事件循环机制
- 调用CFRunLoopRun()函数
- RunLoop启动之后,发送一个通知,告诉系统即将进入RunLoop
- 将要处理Timer/Source0事件
- 处理source0事件
- 假如有source1事件需要处理,执行一个goto语句,唤醒切换内核态到用户态,进行事件处理,跳转到步骤7
- 没有source1事假需要处理,线程即将进入休眠状态,并发送通知
- 处理唤醒时收到的信息,之后跳回步骤2
- 线程进入休眠状态,发送通知
- 即将退出RunLoop,发送通知
- RunLoop的核心
- 用户态 -> main函数 -> mach_msg() -> 系统调用 -> 内核态 -> mach_msg() -> 用户态
- 其核心为,用户态和内核态的切换,来完成休眠和唤醒事件
RunLoop和NSTimer事件
- 当我们在滑动界面时,会发现timer处于停顿状态,这是因为,timer事件位于defaultMode上,而滑动事件处于UITrackingMode上,而RunLoop同时只能处理一个事件,所以,在滑动tableview时,我们的timer事件就会失效停止
- 此时,我们将timer事件添加到CommonMode上,就能解决tableview滑动时,timer失效的问题。因为,UITrackingMode和defaultMode,都是注册在CommonMode中的事件,而timer添加到CommonMode上时,RunLoop,在滑动的时候,相当于只处理了CommonMode,就不会发生timer失效停顿问题
RunLoop和多线程
- RunLoop和线程之间是一一对应的
- 自己创建的线程,并没有RunLoop事件,需要自己手动添加RunLoop
- 实现一个常驻线程
3.1 为当前线程开启一个RunLoop
3.2 向该RunLoop中添加一个Port/Sourece等维持RunLoop的事件循环
3.3 启动该RunLoop(CFRunLoopRun()函数来启动) - 关于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时,不打断滑动效果
- 滑动的Mode在在UITrackingMode上的(在主线程上)
- 数据请求是在子线程进行,切换回主线程进行更新的,我们可以把这块的逻辑包装起来,放置于defaultMode下,在进行滑动的时候,defaultMode停止不会被处理,停止滑动时,不再处理UITrackingMode,这时再进行defaultMode的数据更新
- 这边也可以添加一个timer事件到RunLoop中,保活常驻线程,在timer的block回调中执行耗时操作,(操作原理同步骤2)这样也可使tableview滑动流畅(一个常驻线程使用的场景)
网友评论