iOS Runloop

前言:

每一个技术都是一种问题的解决思路和方案,所以学习一个技术的时候,首先要去思考一下这个技术是基于什么样的问题场景下产生的,这样才能对该技术的特点以及应用有比较好的理解;

 


 

Runloop 的出现是为了解决什么样的问题:

每一个应用程序是一个进程,每个应用程序中会开启多个线程执行执行各种任务;iOS的主线程是一个UI线程,除了视图渲染和用户交互事件的响应,比较消耗CPU计算的任务尽量开辟新的线程去执行,这个方案是众所周知的;

这样的处理带来的好处很明显那就是可以防止主线程阻塞对用户的体验造成影响,但是也会带来一些问题,那就是开辟新的线程是很消耗内存资源的,让一个新线程保持活力也是会消耗计算能力的,当子线程比较少的时候问题还不大,但是积少成多的时候,就会造成内存问题和计算压力,所以采用多线程技术如何进行线程的管理、调度还能最低程度的降低内存的消耗和计算的消耗就成为一个需要思考的重要问题,而Runloop就是OC基于这样的问题而产生的一个线程管理方案;


 

Runloop 基于什么技术:

iOS Runloop

oc目前支持以上几种多线程方案,其中pthread是底层的线程多线程方案,而我们常用的经过多次封装,更加具有面向对象特征的线程方案; Runloop就是基于pthread来实现线程的管理;


 

Runloop 与线程之间的关系:

二者是一一对应的关系的,一个Runloop对应一个核心线程,如果该线程中还有其他的子线程,则会有新的Runloop与其子线程对应;


 

Runloop 生命周期:

创建:Runloop是懒加载模式,除了主线程是默认创建启动的,其他的子线程都需要手动去调用Runloop;

休眠:被激活的Runloop在对应线程中没有任务需要处理的时候处于休眠状态;

活跃:当对应线程中有任务需要执行的时候,Runloop就会被激活去进行任务的调度,任务执行完毕,Runloop就会再次进入休眠状态;

销毁;当对应线程销毁的时候也会被随之销毁;


 

Runloop的构成:

iOS Runloop

CFRunLoopSourceRef

source是RunLoop的数据源(输入源)的抽象类(protocol),Source有两个版本:Source0 和 Source1

  • source0:只包含了一个回调(函数指针),使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。处理App内部事件,App自己负责管理(出发),如UIEvent(Touch事件等,GS发起到RunLoop运行再到事件回调到UI)、CFSocketRef。

  • Source1:由RunLoop和内核管理,由mach_port驱动(特指port-based事件),如CFMachPort、CFMessagePort、NSSocketPort。特别要注意一下Mach port的概念,它是一个轻量级的进程间通讯的方式,可以理解为它是一个通讯通道,假如同时有几个进程都挂在这个通道上,那么其它进程向这个通道发送消息后,这些挂在这个通道上的进程都可以收到相应的消息。这个Port的概念非常重要,因为它是RunLoop休眠和被唤醒的关键,它是RunLoop与系统内核进行消息通讯的窗口。

CFRunLoopTimerRef 

是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用(底层基于使用mk_timer实现)。它受RunLoop的Mode影响(GCD的定时器不受RunLoop的Mode影响),当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。如果线程阻塞或者不在这个Mode下,触发点将不会执行,一直等到下一个周期时间点触发。

CFRunLoopObserverRef 

观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个

1
2
3
4
5
6
7
8
9
10
enum CFRunLoopActivity {
    kCFRunLoopEntry              = (1 << 0),    // 即将进入Loop   
    kCFRunLoopBeforeTimers      = (1 << 1),    // 即将处理 Timer        
    kCFRunLoopBeforeSources     = (1 << 2),    // 即将处理 Source  
    kCFRunLoopBeforeWaiting     = (1 << 5),    // 即将进入休眠     
    kCFRunLoopAfterWaiting      = (1 << 6),    // 刚从休眠中唤醒   
    kCFRunLoopExit               = (1 << 7),    // 即将退出Loop  
    kCFRunLoopAllActivities     = 0x0FFFFFFFU  // 包含上面所有状态  
};
typedef enum CFRunLoopActivity CFRunLoopActivity;

这里要提一句的是,timer和source1(也就是基于port的source)可以反复使用,比如timer设置为repeat,port可以持续接收消息,而source0在一次触发后就会被runloop移除。

上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。


 

Runloop的几种模式:

一个RunLoop包含了多个Mode,每个Mode又包含了若干个Source/Timer/Observer。每次调用 RunLoop的主函数时,只能指定其中一个Mode,这个Mode被称作CurrentMode。如果需要切换 Mode,只能退出Loop,再重新指定一个Mode进入。这样做主要是为了分隔开不同Mode中的Source/Timer/Observer,让其互不影响。下面是5种Mode

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

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

  • UIInitializationRunLoopMode 在刚启动App时第进入的第一个Mode,启动完成后就不再使用。

  • GSEventReceiveRunLoopMode 接受系统事件的内部Mode,通常用不到。

  • kCFRunLoopCommonModes 这是一个占位用的Mode,不是一种真正的Mode。

 


 

Runloop工作流程:

iOS Runloop

当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。每次线程运行RunLoop都会自动处理之前未处理的消息,并且将消息发送给观察者,让事件得到执行。RunLoop运行时首先根据modeName找到对应mode,如果mode里没有source/timer/observer,直接返回。 

 


 

 

Runloop应用:

事件响应

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。

当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。

触摸事件其实是Source1接收系统事件后在回调 __IOHIDEventSystemClientQueueCallback()内触发的 Source0,Source0 再触发的 _UIApplicationHandleEventQueue()。source0一定是要唤醒runloop及时响应并执行的,如果runloop此时在休眠等待系统的 mach_msg事件,那么就会通过source1来唤醒runloop执行。

 

UI更新

Core Animation 在 RunLoop 中注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件 。当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。当Oberver监听的事件到来时,回调执行函数中会遍历所有待处理的UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

如果此处有动画,通过 DisplayLink 稳定的刷新机制会不断的唤醒runloop,使得不断的有机会触发observer回调,从而根据时间来不断更新这个动画的属性值并绘制出来。

iOS Runloop

上一篇:七日Python之路--第九天(blog与Django)


下一篇:基于Vue实现图片在指定区域内移动