NSRunLoop

什么是RunLoop

RunLoop是一个对象,管理着需要处理的事件和消息,实现了让线程在需要处理消息时立刻被唤醒,不需要处理消息时休眠的机制。
OSX/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。
CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
苹果不允许直接创建 RunLoop,只能通过 CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。获取。

   //获取主线程的RunLoop
  [NSRunLoop mainRunLoop];
   //获取当前线程的RunLoop
  [NSRunLoop currentRunLoop];

RunLoop与线程

RunLoop 与线程是一一对应的,保存在一个全局的字典里,一个线程(pthread_t)key对应一个 RunLoop(CFRunLoopRef)对象值。线程创建时并没有 RunLoop,不获取就一直不会有 RunLoop,只有第一次获取时会创建 RunLoop,RunLoop 的销毁发生在线程结束时。只能在一个线程内部 pthread_self() 函数获取 RunLoop(主线程除外)。

CoreFoundation中提供的RunLoop类

CoreFoundation中提供了关于RunLoop的5个类:

  • CFRunLoopRef
  • CFRunLoopModeRef 只是对CFRunLoopRef 的接口进行了封装
  • CFRunLoopSourceRef 产生事件,包括下面两个版本:
  • Source0
    只包含一个回调,由App自己管理。CFRunLoopSourceSignal(source)标记为这个 source 待处理,然后手动调用 CFRunLoopWakeUp(runLoop)来唤醒 RunLoop,处理这个事件。
  • Source1
    包含了一个 mach_port 和一个回调,用于通过内核和其他线程相互发送消息。比如摇晃手机、锁屏等硬件操作。
  • CFRunLoopTimerRef
    包含了一个时间长度和一个回调,加入 RunLoop 时,RunLoop 会注册对应的时间点,时间点到了 RunLoop 会被唤醒执行回调。
  • CFRunLoopObserverRef
    包含了一个回调,用来监测 RunLoop 的状态变化。可以观测的时间点:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
  kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
  kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
  kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
  kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
  kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
  kCFRunLoopExit          = (1UL << 7), // 即将退出Loop
};

它们之间的关系如下:

NSRunLoop_第1张图片
RunLoop.png

一个RunLoop中可以包含多个Mode,每个Mode又可以包含多个Source/Timer/Observer(统称为Mode item)。每次调用RunLoop的主函数时,只能指定其中的一个Mode,这个Mode被称作为CurrentMode。 如果需要切换Mode,需要先退出loop,再重新指定Mode。这样可以保证不同Mode组的Source/Timer/Observer互不影响。

PS:一个Item可以被同时加入到多个Mode中,但是如果重复加入到Mode是不会有效果的。如果一个Mode中一个Item都没有,RunLoop会直接退出,不进入循环。

RunLoop和RunLoopMode

RunLoop只能添加Mode,不能删除。但Mode可以添加/删除内部的Item。只能通过modeName来操作Mode。如果传入一个新的modeName,但是RunLoop内部没有对应mode时,RunLoop会自动创建CFRunLoopModeRef。
一个Mode可以将自己标记为"Common"属性(通过modeName加入到RunLoop的CommonModes中)。当RunLoop内容发生改变时,RunLoop会自动将CommonModeItems里的source/observer/timer同步到具有"Common"标记的Mode里。

RunLoop的结构如下:

struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
    ...
};

CFRunLoop对外提供的管理Mode的接口只有两个:

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);

RunLoopMode的结构如下:


struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};

CFRunLoopMode对外提供的管理ModeItem的接口:


CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);

苹果提供两个Mode,可以用这两个 Mode Name 来操作其对应的 Mode:

  • kCFRunLoopDefaultMode (NSDefaultRunLoopMode)
  • UITrackingRunLoopMode

还提供了一个操作 Common 标记的字符串,可以用这个字符串来操作 Common Items,或标记一个 Mode 为 "Common"。使用时注意区分这个字符串和其他 modeName:

  • kCFRunLoopCommonModes (NSRunLoopCommonModes)

RunLoop的内部逻辑

RunLoop的内部其实是个do-while循环。当调用CFRunLoopRun()时,线程就会一直停留在这个循环里,直到超时或者被手动停止,才会返回。

RunLoop的底层实现

RunLoop的核心是基于 mach_port 的,进入休眠时调用 mach_msg()函数。如果没有别人发送port消息过来内核将线程置于等待状态。

RunLoop实现的功能

App 启动时,系统默认注册了 5 个 Mode :

  • kCFRunLoopDefaultMode 系统默认的 Mode ,通常主线程在这个 Mode 下运行
  • UITrackingRunLoopMode 界面追踪的 Mode ,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
  • UIInitializationRunLoopMode App刚启动第一次进入的 Mode,启动完成后便不再调用。
  • GSEventReceiveRunLoopMode 接受系统事件内部的 Mode,通常用不到
  • kCFRunLoopCommonModes 占位的 Mode,没有什么实际作用

自动释放池AutoreleasePool

App启动后,系统在主线程RunLoop中注册了两个Observer:

  • 监视进入RunLoop的事件的Observer
    回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池,优先级最高,保证创建释放池发生在其他所有回调之前

  • 监视RunLoop进入休眠和退出时事件的Observer
    RunLoop准备进入休眠时(BeforeWaiting)会先释放旧池,在创建新池,分别调用 ___objc_autoreleasePoolPop() __ (释放自动释放池)和 _objc_autoreleasePoolPush()(创建自动释放池)函数。RunLoop退出时(Exit)会调用___objc_autoreleasePoolPop() __ 来释放自动释放池,优先级最低,保证其发生在其他回调之后

事件响应

苹果注册了一个 Source1(基于mach_port)用来接收系统事件,回调函数为__IOHIDEventSystemClientQueueCallback()。
当硬件发生触摸/摇晃/锁屏等事件后,会先通过IOKit.framework生成一个IOHIDEvent事件并由SpringBoard(用户能接触到的图形应用)接收。SpringBoard只接收按键(锁屏/静音等),触摸、加速、接近传感器等几种事件,随后用 mach_port 转发给需要的App进程。随后苹果注册的Source1会出发回调,并调用 _UIApplicationHandleEventQueue()函数进行应用内部的分发。

 ___UIApplicationHandleEventQueue()__ 会把IOHIDEvent处理并包装成UIEvent进行处理或分发,其中包括识别手势/处理屏幕旋转/发送给UIWindow等。通常事件UIButton点击、touchesBegin/move/end/cancel事件都是在这个回调中完成的。

  ####手势识别

当 _UIApplicationHandleEventQueue() 识别了一个手势时,会先调用 Cancel 将当前的touchesBegin/move/end/cancel回调打断,随后系统将对应的手势 标记为待处理。
苹果注册了一个监测RunLoop即将进入休眠(BeforeWaiting)事件的Observer,对应的回调函数是 _UIGestureRecognizerUpdateObserver() ,该函数内部会获取所有标记为待处理的手势,并执行手势的回调。当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
####界面更新
操作UI时(改变frame、更新UIView/CALayer层次),或者手动调用了UIView/CALayer的setNeedsLayout/setNeedsDisplay方法后,这个UIView/CALayer就被标记为待处理,并被提交到一个全局的容器去。
苹果注册了一个监视RunLoop进入休眠(BeforeWaiting)和退出时事件的Observer,回调一个很长的函数,这个函数里会遍历所有待处理的 UIView/CALayer 以执行实际的绘制和调整,并更新 UI 界面。
####定时器
一个NSTimer注册到RunLoop后,RunLoop会为它重复的时间点注册好事件。RunLoop为了节省资源,并不会在非常准确的时间回调这个Timer,因为Timer有宽容度,可以允许有一定的误差。如果错过了某个时间点,那么该时间点的回调也会跳过,等待下一个时间点再执行。
####PerformSelecter
当调用 NSObject 的 performSelecter:afterDelay: 后,内部会创建一个Timer并添加到当前线程的 RunLoop 中,调用 performSelector:onThread: 时,是创建一个 Timer 加在线程中,如果对应线程没有 RunLoop ,这两个方法都没啥卵用。
####关于GCD
GCD提供的一些接口也用到了 RunLoop,如 Dispatch_async()。dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。
####关于网络请求
iOS中的网络请求的接口:

  • CFSocket
    最底层的接口,只负责socket通信。
  • CFNetwork
    基于CFSocket的封装,ASIHttpRequest 应用于这层。
  • NSURLConnection
    基于CFNetwork的封装,AFNetworking应用于这层。
  • NSURLSession
    iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底层仍然用到了 NSURLConnection 的部分功能。
    NSURLConnection工作的过程如下:
    使用 NSURLConnection 时,会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。其实start函数内部会获取当前线程的RunLoop,然后再默认Mode中添加了4个Source0(需要手动出发的Source),CFMultiplexerSource 负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 。
    开始网络传输时,NSURLConnection会创建两个线程:com.apple.CFSocket.private 和 com.apple.NSURLConnectionLoader 。com.apple.CFSocket.private 线程用来处理底层socket连接。 __ com.apple.NSURLConnectionLoader 线程内部会使用RunLoop来接收底层soket的事件,并通过之前添加的Source)通知到上层的 Delegate 。__
    com.apple.NSURLConnectionLoader中的RunLoop通过一些基于Mach_port的Source接收来自底层的CFSocket的通知。收到通知后,会在合适的时机向CFMultiplexerSource等Source0发送通知,同时唤醒Delegate线程的RunLoop来让其处理这些通知。CFMultiplexerSource 会在 Delegate 线程的 RunLoop 对Delegate 执行实际的回调。

摘自:
http://blog.ibireme.com/2015/05/18/runloop/

你可能感兴趣的:(NSRunLoop)