iOS 开发runLoop 机制详解

NSRunLoop机制对于iOS开发,runLoop机制还是很有必要了解一下的,最近在做一个广告图的功能正好需要了解下runtime机制问题,在查看了

官方文档API以及论坛贴吧博客各位大牛的文章后,整理下关于我自己的理解和总结.

  • 首先说介绍下NSRunLoopOSX/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

  • NSRunLoop 与线程的关系

  1. 其实runLoop就是一个do ... while()函数,每个runLoop对应一个线程他们是一一对应的关系,关系保存在一个全局的Dictionary里边,线程刚创建时没有RunLoop,如果不主动获取,是不会有的,RunLoop的创建发生在第一次获取时,RunLoop的销毁发生在线程结束,只能在一个线程的内部获取它的RunLoop(主线程除外)主线程默认有个RunLoop.
  2. Thread包含一个CFRunLoop,一个CFRunLoop包含一种CFRunLoopMode,mode包含CFRunLoopSource,CFRunLoopTimer和CFRunLoopObserver。
  3. RunLoop只能运行在一种mode下,如果要换mode当前的loop也需要停下重启成新的。利用这个机制,ScrollView过程中NSDefaultRunLoopMode的mode会切换UITrackingRunLoopMode来保证ScrollView的流畅滑动不受只能在NSDefaultRunLoopMode时处理的事件影响滑动。同时mode还是可定制的。
    NSDefaultRunLoopMode:默认,空闲状态
    UITrackingRunLoopMode:ScrollView滑动时UIInitializationRunLoopMode:启动时NSRunLoopCommonModes:Mode集合 Timer计时会被scrollView的滑动影响的问题可以通过将timer添加到NSRunLoopCommonModes来解决
//将timer添加到NSDefaultRunLoopMode中
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTick:) userInfo:nil repeats:YES];
//然后再添加到NSRunLoopCommonModes里
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTick:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRefstatic CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁static CFSpinLock_t loopsLock;
/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) { 
  OSSpinLockLock(&loopsLock); 
  if (!loopsDic) { // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
      loopsDic = CFDictionaryCreateMutable(); 
      CFRunLoopRef mainLoop = _CFRunLoopCreate(); CFDictionarySetValue(loopsDic,       pthread_main_thread_np(), mainLoop);
   }  
   /// 直接从 Dictionary 里获取。 
   CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));  
   if (!loop) { 
    /// 取不到时,创建一个 
    loop = _CFRunLoopCreate();
   CFDictionarySetValue(loopsDic, thread, loop); 
   /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
   _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop); 
   }  
  OSSpinLockUnLock(&loopsLock); 
  return loop;
}
CFRunLoopRef CFRunLoopGetMain() {
   return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
   return _CFRunLoopGet(pthread_self());
}

介绍完线程和RunLoop的关系后,主要看下RunLoop和RunLoopModel, RunLoop runLoop 在CoreFoundation对外一共有五大类最为对外的接口:

RunLoop

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

1. 其中 CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装
iOS 开发runLoop 机制详解_第1张图片
如图所示
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

2. CFRunLoopSourceRef 是事件产生的地方,Source包含了两个部分:Source0 和 Source1。source0:处理如UIEvent,CFSocket这样的事件source1:Mach port驱动,CFMachport,CFMessagePortSource0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。Source1 包含了一个 mach_port 和一个回调(函数指针),被用于和通过内核和其他 mach_port 相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。

3. CFRunLoopTimerRefNSTimer是对RunLoopTimer的封装是基于时间的触发器,它和 NSTimer 是toll-free bridge 的。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调

4. CFRunLoopObserverRefCocoa框架中很多机制比如CAAnimation等都是由RunLoopObserver触发的。observer到当前状态的变化进行通知观察者,每个 Ovserver 都包含了一个回调(函数指针),当 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
};

RunLoopModelCFRunLoopMode 和 CFRunLoop 的结构大致如下:

struct __CFRunLoopMode {
  CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
  CFMutableSetRef _sources0; // Set  
  CFMutableSetRef _sources1; // Set 
  CFMutableArrayRef _observers; // Array  
  CFMutableArrayRef _timers; // Array  ...
};
struct __CFRunLoop { 
  CFMutableSetRef _commonModes; // Set
  CFMutableSetRef _commonModeItems; // Set  
  CFRunLoopModeRef _currentMode; //
  Current Runloop Mode CFMutableSetRef _modes; // Set  ...
};

这里有个概念叫 "CommonModes":一个 Mode 可以将自己标记为"Common"属性(通过将其 ModeName 添加到 RunLoop 的 "commonModes" 中)。RunLoop 会自动将 _commonModeItems 加入到具有 "Common" 标记的所有Mode里。应用场景举例:主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为"Common"属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。有时你需要一个 Timer,在两个 Mode 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 "commonModeItems" 中。"commonModeItems" 被 RunLoop 自动更新到所有具有"Common"属性的 Mode 里去。

RunLoop底层实现从上面代码可以看到,RunLoop的核心是基于 mach port 的,其进入休眠时调用的函数是 mach_msg()。为了解释这几个概念,下面稍微介绍一下OSX/iOS的系统架构。

iOS 开发runLoop 机制详解_第2张图片
这里写图片描述

苹果官方将整个系统大致划分为上述4个层次:应用层包括用户能接触到的图形应用,例如 Spotlight、Aqua、SpringBoard等。应用框架层即开发人员接触到的 Cocoa 等框架。核心框架层包括各种核心框架、OpenGL 等内容。Darwin 即操作系统的核心,包括系统内核、驱动、Shell 等内容,这一层是开源的,其所有源码都可以在 opensource.apple.com 里找到。

CFRunLoop {
 current mode = kCFRunLoopDefaultMode common modes = {
 UITrackingRunLoopMode kCFRunLoopDefaultMode } common mode items = { // source0 (manual) CFRunLoopSource {order =-1, 
{ callout = _UIApplicationHandleEventQueue}}
 CFRunLoopSource {order =-1, 
{ callout = PurpleEventSignalCallback }} 
CFRunLoopSource {order = 0, {
 callout = FBSSerialQueueRunLoopSourceHandler}} 
// source1 (mach port) CFRunLoopSource {order = 0,
 {port = 17923}} 
CFRunLoopSource {order = 0, {port = 12039}} CFRunLoopSource {order = 0, {port = 16647}} CFRunLoopSource {order =-1, { callout = PurpleEventCallback}} CFRunLoopSource {order = 0, {port = 2407, callout = _ZL20notify_port_callbackP12__CFMachPortPvlS1_}} CFRunLoopSource {order = 0, {port = 1c03, callout = __IOHIDEventSystemClientAvailabilityCallback}} CFRunLoopSource {order = 0, {port = 1b03, callout = __IOHIDEventSystemClientQueueCallback}} CFRunLoopSource {order = 1, {port = 1903, callout = __IOMIGMachPortPortCallback}} 
// Ovserver CFRunLoopObserver {order = -2147483647, activities = 0x1,
// Entry callout = _wrapRunLoopWithAutoreleasePoolHandler} CFRunLoopObserver {order = 0, activities = 0x20, // BeforeWaiting callout = _UIGestureRecognizerUpdateObserver} CFRunLoopObserver {order = 1999000, activities = 0xa0, // BeforeWaiting | Exit callout = _afterCACommitHandler} CFRunLoopObserver {order = 2000000, activities = 0xa0, // BeforeWaiting | Exit callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv} CFRunLoopObserver {order = 2147483647, activities = 0xa0, // BeforeWaiting | Exit callout = _wrapRunLoopWithAutoreleasePoolHandler} // Timer CFRunLoopTimer {firing = No, interval = 3.1536e+09, tolerance = 0, next fire date = 453098071 (-4421.76019 @ 96223387169499), callout = _ZN2CAL14timer_callbackEP16__CFRunLoopTimerPv (QuartzCore.framework)} }, modes = { CFRunLoopMode { sources0 = { /* same as 'common mode items' */ }, sources1 = { /* same as 'common mode items' */ }, observers = { /* same as 'common mode items' */ }, timers = { /* same as 'common mode items' */ }, }, CFRunLoopMode { sources0 = { /* same as 'common mode items' */ }, sources1 = { /* same as 'common mode items' */ }, observers = { /* same as 'common mode items' */ }, timers = { /* same as 'common mode items' */ }, }, CFRunLoopMode { sources0 = { CFRunLoopSource {order = 0, { callout = FBSSerialQueueRunLoopSourceHandler}} }, sources1 = (null), observers = { CFRunLoopObserver >{activities = 0xa0, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv} )}, timers = (null), },
 CFRunLoopMode { //这是一个占位的 Mode,没有实际作用。
 sources0 = { CFRunLoopSource {order = -1, { callout = PurpleEventSignalCallback}} },
 sources1 = { CFRunLoopSource {order = -1, { callout = PurpleEventCallback}} },
 observers = (null), timers = (null), },
  CFRunLoopMode { 
  sources0 = (null), sources1 = (null), observers = (null), timers = (null), } }}

这里能看到苹果所有的Model

事件响应

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考 这里 。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。####手势识别当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

界面更新

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

NSRunLoop的应用

AFNetworking 创建一个常驻服务线程去处理数据返回使用NSOperation+NSURLConnection并发模型都会面临NSURLConnection下载完成前线程退出导致NSOperation对象接收不到回调的问题。AFNetWorking解决这个问题的方法是按照官方的guid上写的NSURLConnection的delegate方法需要在connection发起的线程runloop中调用,于是AFNetWorking直接借鉴了Apple自己的一个Demo的实现方法单独起一个global thread,内置一个runloop,所有的connection都由这个runloop发起,回调也是它接收,不占用主线程,也不耗CPU资源。##TableView中实现平滑滚动延迟加载图片利用CFRunLoopMode的特性,可以将图片的加载放到NSDefaultRunLoopMode的mode里,这样在滚动UITrackingRunLoopMode这个mode时不会被加载而影响到。

UIImage *downloadedImage = ...;
[self.avatarImageView performSelector:@selector(setImage:) withObject:downloadedImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];

使用RunLoop阻塞线程并且同时能够处理其他source事件

CFRunLoopRunInMode(kCFRunLoopDefaultMode,second, NO);
CFRunLoopStop(CFRunLoopGetMain());

你可能感兴趣的:(iOS 开发runLoop 机制详解)