Runloop
1. 引子
Runloop是iOS开发的一个老生常谈的话题,网上的资料也是很多,但是我想了解的关键地方总是一笔带过,因此自己看源码,进行了资料补充,从头开始。
不再想以教程的方式来写总结了,真正的教程在各大知名网站上均有,日后博文将抽丝剥茧,关键的地方进行雕琢。
2. RunLoop
为什么需要RunLoop。
1 | void func1() { |
上面这个程序,在线程执行完main函数后,就结束了,什么也没了。但是手机APP不行,手机APP需要保活,在任何时候需要响应用户的操作以及系统的事件。让程序不结束的方案就是循环。此时Loop就出现了。
1 | do |
另外一个问题就是:不能一直空循环,因为空循环是对CPU的浪费。
走进源码了解RunLoop,NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。我们可看CFRunLoop源码,这里不介绍整个CFRunLoop.h,CFRunLoop.c的源码,主要是关键的部分。
RunLoop不仅仅需要了解loop的部分,还需要有基础的铺垫,先将一些必须了解的数据结构先认识一下。
3. RunLoopSource
事件源,它是唤醒线程和线程任务关键之一,事件源有两种,一个是输入事件源,一个是定时器事件源。首先看看输入事件源。
线程在一个loop执行完后,若没有其他任务。将会进入休眠,此时需要其他事件源进行唤醒
3.1 __CFRunLoopSource
1 | struct __CFRunLoopSource { |
3.2 __CFRuntimeBase
1 | typedef struct __CFRuntimeBase { |
3.3 CFRunLoopSourceContext
1 | typedef struct { |
在输入事件源__CFRunLoopSource
的结构体中可以看到一个version0
和一个version1
。后面我们把version0
版本的source
称之为source0
,反之为source1
。version0
:简单理解就是自定义的source,我们可以自己定义一个source0
。按钮点击,手势等都已经被苹果帮我们定义好了(这个我们后面讨论一下), Cocoa 还定义了一个自定义输入源,允许在任何线程上执行选择器(selector)。就是performSelector
系列方法。这里不列举了。version1
:这个是和内核相关了,基于Mach por
t。其中需要到一个port,每个进程都有一个port,就是进程间通信需要用的。
说到这个就要说到一个点,就是我们知道屏幕触摸是硬件相关的,当我们点击屏幕时,此时并不知道是哪个APP的。因此呢这个时候内核先通过source1来这接收个硬件event,后面再分发到source0处理。因此如果追根溯源,点击时间终究还是source1。
1 | __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(rls->_context.version0.perform, rls->_context.version0.info); |
4. __CFRunLoopTimer
1 | // 仔细和__CFRunLoopSource对比就会发现有一些一样的成员 |
4.1 CFRunLoopTimerCallBack
1 | typedef void (*CFRunLoopTimerCallBack)(CFRunLoopTimerRef timer, void *info); |
4.2 CFRunLoopTimerContext
1 | typedef struct { |
可以通过如下函数使用 CFRunLoopTimerContext
:
1 | // 这个函数符合 CFRunLoopTimerCallBack |
可自行阅读如下函数
1 | CFRunLoopTimerRef CFRunLoopTimerCreate(CFAllocatorRef allocator, CFAbsoluteTime fireDate, CFTimeInterval interval, CFOptionFlags flags, CFIndex order, CFRunLoopTimerCallBack callout, CFRunLoopTimerContext *context) { |
Timer 最后的回调会在这里
1 | static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(CFRunLoopTimerCallBack func, CFRunLoopTimerRef timer, void *info) { |
5. Runloop Observer
Observer 是非常重要的。观察下面的结构体,感觉像是的timer的结构体搬过来的,这样也可以猜到有一部分和timer是一样的,就是它的回调。其中_activities是observer独有的,代表观察的RunLoop的状态。如果RunLoop的状态为这个状态就会发起通知,调用回调。
5.1 __CFRunLoopObserver
1 | typedef struct __CFRunLoopObserver * CFRunLoopObserverRef; |
5.2 _activities
1 | /* Run Loop Observer Activities */ |
这里使用CF_OPTIONS
后,后面进行位与操作后,就可以直接得到状态。
1 | // 截取其中一段代码可以看到 |
5.3 _callout
在__CFRunLoopDoObservers
方法中会调用下面的宏,通知事件
1 | __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ |
5.4 应用
5.4.1 AutoreleasePool
App 启动后,苹果在主线程 RunLoop 里注册了下面的 Observer:
- 通知 Observers:即将进入 Loop => 调用 _objc_autoreleasePoolPush() 创建自动释放池
- do while
- …
- 通知 Observers:即将进入休眠 => 调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池
- …
- 通知 Observers: 即将退出 => 调用 _objc_autoreleasePoolPop() 来释放自动释放池
5.4.2 事件响应
苹果注册了一个 Source1 来接收触摸、加速、传感器等系统事件,随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。
5.4.3 手势识别
当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断,随后系统将对应的 UIGestureRecognizer 标记为待处理,苹果注册了一个 Observer 监测 即将进入休眠,其回调函数会获取所有刚被标记为待处理的 UIGestureRecognizer,并执行UIGestureRecognizer 的回调,当有 UIGestureRecognizer 的状态变化时,这个回调都会进行相应处理。
5.4.4 界面更新
当在操作 UI 时,这个 UIView 或 CALayer 就被标记为待处理,并被提交到一个全局的容器去,苹果注册了一个 Observer 监测 即将进入休眠 和 即将退出,回调去执行一个很长的函数,这个函数里会遍历所有待处理的 UIView 或 CALayer 以执行实际的绘制和调整,并更新 UI 界面。
5.4.5 关于 GCD
当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block,也就是对应 handle_msg 处理消息:如果 dispatch 就执行 block 。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。
5.4.6 PerformSelecter
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会 创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。当调用 performSelector:onThread: 时,实际上其会 创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
6. RunLoop Mode
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
6.1 __CFRunLoopMode
1 | struct __CFRunLoopMode { |
6.2 __CFRunLoop
1 | struct __CFRunLoop { |
7. RunLoop 入口
1 | // 默认的 |
7.1 CFRunLoopRunSpecific
1 | SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ |
7.2 __CFRunLoopRun
一段伪代码
1 | { |
精简部分代码
1 | static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) { |
7.3 RunLoop 运用
apple官方的运用,对比RunLoop结构体
1 | CFRunLoop { |