最近看了很多RunLoop的文章,看完很懵逼,决心整理一下,文章中大部分内容都是引用大神们的,但好歹对自己有个交代了,花了一个周天加几个晚上熬夜完成的,有个产出还是很爽的,不多比比了,下面开始吧。 什么是RunLoop?
RunLoop实际上是一个对象,这个对象在循环中用来处理程序运行过程中出现的各种事件(比如说触摸事件、UI刷新事件、定时器事件、Selector事件)和消息,从而保持程序的持续运行;而且在没有事件处理的时候,会进入睡眠模式,从而节省CPU资源,提高程序性能。 Event Loop模型伪代码 int main(int argc, char * argv[]) { mach kernel属于苹果内核,RunLoop依靠它实现了休眠和唤醒而避免了CPU的空转。 Runloop是基于pthread进行管理的,pthread是基于c的跨平台多线程操作底层API。它是mach thread的上层封装(可以参见Kernel Programming Guide),和NSThread一一对应(而NSThread是一套面向对象的API,所以在iOS开发中我们也几乎不用直接使用pthread)。 RunLoop的组成 CFRunLoop对象可以检测某个task或者dispatch的输入事件,当检测到有输入源事件,CFRunLoop将会将其加入到线程中进行处理。比方说用户输入事件、网络连接事件、周期性或者延时事件、异步的回调等。 RunLoop可以检测的事件类型一共有3种,分别是CFRunLoopSource、CFRunLoopTimer、CFRunLoopObserver。可以通过CFRunLoopAddSource, CFRunLoopAddTimer或者CFRunLoopAddObserver添加相应的事件类型。 要让一个RunLoop跑起来还需要run loop modes,每一个source, timer和observer添加到RunLoop中时必须要与一个模式(CFRunLoopMode)相关联才可以运行。 上面是对于CFRunLoop官方文档的解释 RunLoop的主要组成 RunLoop共包含5个类,但公开的只有Source、Timer、Observer相关的三个类。 CFRunLoopRef 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 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个 enum CFRunLoopActivity { 这里要提一句的是,timer和source1(也就是基于port的source)可以反复使用,比如timer设置为repeat,port可以持续接收消息,而source0在一次触发后就会被runloop移除。 上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。 RunLoop主要处理以下6类事件 static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(); RunLoop的Mode CFRunLoopMode 和 CFRunLoop的结构大致如下: struct __CFRunLoopMode { 一个RunLoop包含了多个Mode,每个Mode又包含了若干个Source/Timer/Observer。每次调用 RunLoop的主函数时,只能指定其中一个Mode,这个Mode被称作CurrentMode。如果需要切换 Mode,只能退出Loop,再重新指定一个Mode进入。这样做主要是为了分隔开不同Mode中的Source/Timer/Observer,让其互不影响。下面是5种Mode
其中kCFDefaultRunLoopMode、UITrackingRunLoopMode是苹果公开的,其余的mode都是无法添加的。那为何我们又可以这么用呢 [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; 什么是CommonModes? 一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里 主线程的 RunLoop 里有 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode,这两个Mode都已经被标记为”Common”属性。当你创建一个Timer并加到DefaultMode时,Timer会得到重复回调,但此时滑动一个 scrollView 时,RunLoop 会将 mode 切换为TrackingRunLoopMode,这时Timer就不会被回调,并且也不会影响到滑动操作。 如果想让scrollView滑动时Timer可以正常调用,一种办法就是手动将这个 Timer 分别加入这两个 Mode。另一种方法就是将 Timer 加入到CommonMode 中。 怎么将事件加入到CommonMode? 我们调用上面的代码将 Timer 加入到CommonMode 时,但实际并没有 CommonMode,其实系统将这个 Timer 加入到顶层的 RunLoop 的 commonModeItems 中。commonModeItems 会被 RunLoop 自动更新到所有具有”Common”属性的 Mode 里去。 这一步其实是系统帮我们将Timer加到了kCFRunLoopDefaultMode和UITrackingRunLoopMode中。 在项目中最常用的就是设置NSTimer的Mode,比较简单这里就不说了。 RunLoop运行机制 当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。每次线程运行RunLoop都会自动处理之前未处理的消息,并且将消息发送给观察者,让事件得到执行。RunLoop运行时首先根据modeName找到对应mode,如果mode里没有source/timer/observer,直接返回。 /// 用DefaultMode启动 RunLoop的挂起和唤醒 RunLoop的挂起 RunLoop的挂起是通过_CFRunLoopServiceMachPort —call—> mach_msg —call—> mach_msg_trap这个调用顺序来告诉内核RunLoop监听哪个mach_port(上面提到的消息通道),然后等待事件的发生(等待与InputSource、Timer描述内容相关的事件),这样内核就把RunLoop挂起了,即RunLoop休眠了。 RunLoop的唤醒 这几种情况下会被唤醒
当RunLoop被挂起后,如果之前监听的事件发生了,由另一个线程(或另一个进程中的某个线程)向内核发送这个mach_port的msg后,trap状态被唤醒,RunLoop继续运行 处理事件
事件处理完成进行判断
RunLoop 的底层实现 关于这个大家可以看ibireme的深入理解RunLoop一文 https://blog./2015/05/18/runloop/,我这里选择一些觉得比较重要又不是那么难懂的。 Mach消息发送机制看这篇文章Mach消息发送机制: https://www.jianshu.com/p/a764aad31847 为了实现消息的发送和接收,mach_msg() 函数实际上是调用了一个 Mach 陷阱 (trap),即函数mach_msg_trap(),陷阱这个概念在 Mach 中等同于系统调用。当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到内核态;内核态中内核实现的 mach_msg() 函数会完成实际的工作,如下图: RunLoop 的核心就是一个 mach_msg() (见上面代码的第7步),RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap() 这个地方。 RunLoop和线程 RunLoop和线程是息息相关的,我们知道线程的作用是用来执行特定的一个或多个任务,但是在默认情况下,线程执行完之后就会退出,就不能再执行任务了。这时我们就需要采用一种方式来让线程能够处理任务,并不退出。所以,我们就有了RunLoop。 iOS开发中能遇到两个线程对象: pthread_t和NSThread,pthread_t和NSThread 是一一对应的。比如,你可以通过 pthread_main_thread_np()或 [NSThread mainThread]来获取主线程;也可以通过pthread_self()或[NSThread currentThread]来获取当前线程。CFRunLoop 是基于 pthread 来管理的。 线程与RunLoop是一一对应的关系(对应关系保存在一个全局的Dictionary里),线程创建之后是没有RunLoop的(主线程除外),RunLoop的创建是发生在第一次获取时,销毁则是在线程结束的时候。只能在当前线程中操作当前线程的RunLoop,而不能去操作其他线程的RunLoop。
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef 开发过程中需要RunLoop时,则需要手动创建和运行RunLoop(尤其是在子线程中, 主线程中的Main RunLoop除外),我看到别人举了这么个例子,很有意思 调用[NSTimer scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:]带有schedule的方法簇来启动Timer. 此方法会创建Timer并把Timer放到当前线程的RunLoop中,随后RunLoop会在Timer设定的时间点回调Timer绑定的selector或Invocation。但是,在主线程和子线程中调用此方法的效果是有差异的,即在主线程中调用scheduledTimer方法时timer可以在设定的时间点触发,但是在子线程里则不能触发。 这是因为子线程中没有创建RunLoop且更没有启动RunLoop,而主线程中的RunLoop默认是创建好的且一直运行着。所以,子线程中需要像下面这样调用。 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 我的分析是:scheduledTimerWithTimeInterval内部在向RunLoop传递Timer时是调用与线程实例相关的单例方法[NSRunLoop currentRunLoop]来获取RunLoop实例的,即RunLoop实例不存在就创建一个与当前线程相关的RunLoop并把Timer传递到RunLoop中,存在则直接传Timer到RunLoop中即可。 而在RunLoop开始运行后再向其传递Timer时,由于dispatch_async代码块里的两行代码是顺序执行,[[NSRunLoop currentRunLoop] run]是一个没有结束时间的RunLoop,无法执行到“[NSTimer scheduledTimerWithTimeInterval:…”这一行代码,Timer也就没有被加到当前RunLoop中,所以更不会触发Timer了。 苹果用 RunLoop 实现的功能 AutoreleasePool App启动之后,系统启动主线程并创建了RunLoop,在main thread中注册了两个observer,回调都是_wrapRunLoopWithAutoreleasePoolHandler() 第一个Observer监视的事件 即将进入Loop(kCFRunLoopEntry),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其order是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。 第二个Observer监视了两个事件
我们知道AutoRelease对象是被AutoReleasePool管理的,那么AutoRelease对象在什么时候被回收呢? 第一种情况:在我们自己写的for循环或线程体里,我们都习惯用AutoReleasePool来管理一些临时变量的autorelease,使得在for循环或线程结束后回收AutoReleasePool的时候来回收AutoRelease临时变量。 另一种情况:我们在主线程里创建了一些AutoRelease对象,这些对象可不能指望在回收Main AutoReleasePool时才被回收,因为App一直运行的过程中Main AutoReleasePool是不会被回收的。那么这种AutoRelease对象的回收就依赖Main RunLoop的运行状态,Main RunLoop的Observer会在Main RunLoop结束休眠被唤醒时(kCFRunLoopAfterWaiting状态)通知UIKit,UIKit收到这一通知后就会调用_CFAutorleasePoolPop方法来回收主线程中的所有AutoRelease对象。
NSTimer(timer触发) 上文说到了CFRunLoopTimerRef,其实NSTimer的原型就是CFRunLoopTimerRef。一个Timer注册 RunLoop 之后,RunLoop 会为这个Timer的重复时间点注册好事件。有两点需要注意:
在RunLoop的Mode中也有说到,NSTimer使用的时候注意Mode,比如我之前开发时候用NSTimer写一个Banner图片轮播框架,如果不设置Timer的Mode为commonModes那么在滑动TableView的时候Banner就停止轮播 DispatchQueue.global().async { 和GCD的关系
//实验GCD Timer 与 Runloop的关系,只有当dispatch_get_main_queue时才与RunLoop有关系 当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。 但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。同理,GCD的dispatch_after在dispatch到main_queue时的timer机制才与RunLoop相关。 PerformSelecter NSObject的performSelecter:afterDelay: 实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。 NSObject的performSelector:onThread: 实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。 其实这种方式有种说法也叫创建常驻线程(内存),AFNetworking也用到这种技法。举个例子,如果把RunLoop去掉,那么test方法就不会执行。 class SecondViewController: UIViewController { 网络请求 iOS中的网络请求接口自下而上有这么几层 其中CFSocket和CFNetwork偏底层,早些时候比较知名的网络框架AFNetworking是基于NSURLConnection编写的,iOS7之后新增了NSURLSession,NSURLSession的底层仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 线程),之后AFNetworking和Alamofire就是基于它封装的了。 通常使用 NSURLConnection 时,会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0 (即需要手动触发的Source)。CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 的。 开始网络传输时,NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。 其中 CFSocket 线程是处理底层 socket 连接的,NSURLConnectionLoader中的RunLoop通过一些基于mach port的Source1接收来自底层CFSocket的通知。当收到通知后,其会在合适的时机向CFMultiplexerSource等Source0发送通知,同时唤醒Delegate线程的RunLoop来让其处理这些通知。CFMultiplexerSource会在Delegate线程的RunLoop对Delegate执行实际的回调。 事件响应 苹果注册了一个 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执行。 _UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。 手势识别 当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。 苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。 当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。 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回调,从而根据时间来不断更新这个动画的属性值并绘制出来。 函数内部的调用栈 _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv() 绘图和动画有两种处理的方式:CPU(中央处理器)和GPU(图形处理器) CPU: CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等 GPU: GPU 进行变换、合成、渲染. 关于CADisplayLink的描述有两种
不管怎么样CADisplayLink和NSTimer是有很大不同的,详情可以参考这篇文章CADisplayLink:https://www.jianshu.com/p/72fedadf92e3 ibireme根据CADisplayLink的特性写了个FPS指示器YYFPSLabel,代码非常少 原理是这样的:既然CADisplayLink可以以屏幕刷新的频率调用指定selector,而且iOS系统中正常的屏幕刷新率为60Hz(60次每秒),所以使用 CADisplayLink 的 timestamp 属性,配合 timer 的执行次数计算得出FPS数 作者:大神Q |
|