聊聊Runloop

1.什么是Runloop 

在开始聊RunLoop之前,我们先来了解一下程序的执行原理。一般来说,程序是在线程中执行,一个线程一次只能执行一个任务(关于GCD,可看上篇文章介绍),执行完成后线程就会退出。类似这样:

1 int main(int argc, char * argv[]) {
2     @autoreleasepool {
3         NSLog(@"我开始干活了");
4         
5         return 0;
6     }
7 }

在我们的App中,我们需要的是这样一个机制:线程能随时处理事件但不退出。这种机制叫做Event Loop,例如Windows系统下的消息循环,OSX/iOS里的Run Loop。

还是先看看我们实际App中的Main函数:

1 int main(int argc, char * argv[]) {
2     @autoreleasepool {
3         return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
4     }
5 }

我们稍微改造一下,来细致的分析:

1 int main(int argc, char * argv[]) {
2     @autoreleasepool {
3         int iRet = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
4         NSLog(@"%d", iRet);  //这个不会打印
5         return iRet;
6     }
7 }

为什么那个NSLog没有打印?我们看一下运行堆栈信息,从而分析一下UIApplicationMain都干了什么事:

可以看到,有一系列CFRunLoop的相关信息,这些后面分析原理的时候来细谈。我们根据上面的堆栈信息,来大致对RunLoop有个初步总结:

  • UIApplicationMain函数中启动了一个RunLoop。这个RunLoop是和主线程相关联的,它是主线程的一部分(其实每一个线程都有一个RunLoop,但其他线程的RunLoop默认没有开启。如果想让某个线程一直活着,那么需要开启RunLoop);
  • RunLoop管理了需要处理的事件和消息。并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 "接受消息->等待->处理" 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回;
  • RunLoop保持程序的持续运行(主线程没有退出);
  • RunLoop能够处理App中的各种事件(如触摸事件、定时器事件、Selector事件);
  • 使用RunLoop机制,能够有效的节省CPU资源,提高程序性能(有事做就做事,没事做就休息)。

2.RunLoop原理分析

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

2.1RunLoop和线程

苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。我们可以看一下系统实现这两个方法的源码:

//全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef __CFRunLoops = NULL;
//访问 loopsDic 时的锁
static CFLock_t loopsLock = CFLockInit;

// 获取一个 pthread 对应的 RunLoop。
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
    t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
       CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
       CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
       CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
       if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
           CFRelease(dict);
       }
       CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    // 直接从 Dictionary 里获取。
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    //  取不到时,创建一个
    if (!loop) {
       CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
       loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
       if (!loop) {
           CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            loop = newLoop;
       }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFUnlock(&loopsLock);
       CFRelease(newLoop);
    }
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        // 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    static CFRunLoopRef __main = NULL; // no retain needed
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main;
}

CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

从上面的代码,我们总结一下线程和RunLoop的关系:

  • 每条线程都有唯一的一个与之对应的RunLoop对象,它们的关系保存在一个全局的 Dictionary 里;
  • 主线程的RunLoop已经自动创建和启动了,子线程的RunLoop需要手动创建和启动,如果不主动获取,那它一直都不会有;
  • RunLoop是线程的一部分,它的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时;
  • 只能在一个线程的内部获取其 RunLoop(主线程除外)。

 2.2RunLoop相关类

在 CoreFoundation 里面关于 RunLoop 有5个类:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

 它们的关系如下图所示:

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

2.2.1CFRunLoopRef

先看看它的源码:

 1 头文件CFRunLoop.h
 2 typedef struct __CFRunLoop * CFRunLoopRef;
 3 
 4 CFRunLoop.c
 5 struct __CFRunLoop {
 6     CFRuntimeBase _base;
 7     pthread_mutex_t _lock;            /* locked for accessing mode list */
 8     __CFPort _wakeUpPort;            // used for CFRunLoopWakeUp 
 9     Boolean _unused;
10     volatile _per_run_data *_perRunData;              // reset for runs of the run loop
11     pthread_t _pthread;
12     uint32_t _winthread;
13     CFMutableSetRef _commonModes;
14     CFMutableSetRef _commonModeItems;
15     CFRunLoopModeRef _currentMode;
16     CFMutableSetRef _modes;
17     struct _block_item *_blocks_head;
18     struct _block_item *_blocks_tail;
19     CFAbsoluteTime _runTime;
20     CFAbsoluteTime _sleepTime;
21     CFTypeRef _counterpart;
22 };

关于_commonModes和_commonModeItems,这里做一个说明:一个 Mode 可以将自己标记为"Common"属性(通过将其 ModeName 添加到 RunLoop 的 "commonModes" 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 "Common" 标记的所有Mode里。

咱们通过一个示例来说明:

1     self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(startTimer) userInfo:nil repeats:YES];
2     [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

对上面的示例做个解释:主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为"Common"属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪用户交互事件时的状态。当创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。

有时我们需要一个 Timer,在两个 Mode 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 "commonModeItems" 中。"commonModeItems" 被 RunLoop 自动更新到所有具有"Common"属性的 Mode 里去。

在Core Foundation框架中提供了两个方法来获取CFRunLoop对象(CFRunLoop.h文件):

1 CF_EXPORT CFRunLoopRef CFRunLoopGetCurrent(void);
2 CF_EXPORT CFRunLoopRef CFRunLoopGetMain(void);

在Foundation框架中提供了相应的两个方法来获取RunLoop:

1 [NSRunLoop currentRunLoop];  //当前线程的RunLoop
2 [NSRunLoop mainRunLoop];  //主线程的RunLoop

 下面打印了一个RunLoop运行时的Log:

<CFRunLoop 0x7fb4a9422dc0 [0x1046fceb0]>{wakeup port = 0xf03, stopped = false, ignoreWakeUps = false, 

current mode = UIInitializationRunLoopMode,

common modes = <CFBasicHash 0x7fb4a9614860 [0x1046fceb0]>{type = mutable set, count = 2,
entries =>
    0 : <CFString 0x105fc8370 [0x1046fceb0]>{contents = "UITrackingRunLoopMode"}
    2 : <CFString 0x1046d8f30 [0x1046fceb0]>{contents = "kCFRunLoopDefaultMode"}
},

common mode items = <CFBasicHash 0x7fb4a950a4d0 [0x1046fceb0]>{type = mutable set, count = 16,
entries =>
    0 : <CFRunLoopSource 0x7fb4a9808a10 [0x1046fceb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x0, callout = PurpleEventSignalCallback (0x109ab46e2)}}
    1 : <CFRunLoopSource 0x7fb4a9634160 [0x1046fceb0]>{signalled = Yes, valid = Yes, order = 0, context = <CFRunLoopSource context>{version = 0, info = 0x0, callout = FBSSerialQueueRunLoopSourceHandler (0x10bc34033)}}
    2 : <CFRunLoopObserver 0x7fb4a942b830 [0x1046fceb0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1054326a3), context = <CFArray 0x7fb4a942b800 [0x1046fceb0]>{type = mutable-small, count = 0, values = ()}}
    4 : <CFRunLoopSource 0x7fb4a942b5e0 [0x1046fceb0]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource MIG Server> {port = 11527, subsystem = 0x105f95d20, context = 0x0}}
    6 : <CFRunLoopSource 0x7fb4a950a9c0 [0x1046fceb0]>{signalled = No, valid = Yes, order = 1, context = <CFMachPort 0x7fb4a96263b0 [0x1046fceb0]>{valid = Yes, port = 1803, source = 0x7fb4a950a9c0, callout = __IOMIGMachPortPortCallback (0x10a46e572), context = <CFMachPort context 0x7fb4a9626340>}}
    9 : <CFRunLoopSource 0x7fb4a9823240 [0x1046fceb0]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource MIG Server> {port = 15879, subsystem = 0x105fb43b0, context = 0x7fb4a9816980}}
    10 : <CFRunLoopSource 0x7fb4a96267c0 [0x1046fceb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 1, info = 0x2103, callout = PurpleEventCallback (0x109ab6e6b)}}
    11 : <CFRunLoopSource 0x7fb4a942ba10 [0x1046fceb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x7fb4a9626b30, callout = _UIApplicationHandleEventQueue (0x105432a2a)}}
    12 : <CFRunLoopSource 0x7fb4a950a3f0 [0x1046fceb0]>{signalled = No, valid = Yes, order = 0, context = <CFMachPort 0x7fb4a9808950 [0x1046fceb0]>{valid = Yes, port = 1a03, source = 0x7fb4a950a3f0, callout = __IOHIDEventSystemClientQueueCallback (0x10a4665ef), context = <CFMachPort context 0x7fb4a98086b0>}}
    13 : <CFRunLoopObserver 0x7fb4a98172d0 [0x1046fceb0]>{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x1057e3965), context = <CFRunLoopObserver context 0x0>}
    14 : <CFRunLoopObserver 0x7fb4a942b6a0 [0x1046fceb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 1999000, callout = _afterCACommitHandler (0x105459751), context = <CFRunLoopObserver context 0x7fb4a9626b30>}
    15 : <CFRunLoopSource 0x7fb4a950a890 [0x1046fceb0]>{signalled = No, valid = Yes, order = 0, context = <CFMachPort 0x7fb4a99002e0 [0x1046fceb0]>{valid = Yes, port = 1b03, source = 0x7fb4a950a890, callout = __IOHIDEventSystemClientAvailabilityCallback (0x10a466850), context = <CFMachPort context 0x7fb4a98086b0>}}
    18 : <CFRunLoopObserver 0x7fb4a9816180 [0x1046fceb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x105226a94), context = <CFRunLoopObserver context 0x0>}
    19 : <CFRunLoopObserver 0x7fb4a962e270 [0x1046fceb0]>{valid = Yes, activities = 0xfffffff, repeats = No, order = 0, callout = orderOutContextObserverCallout (0x10544c1d9), context = <CFRunLoopObserver context 0x0>}
    20 : <CFRunLoopSource 0x7fb4a980d1c0 [0x1046fceb0]>{signalled = No, valid = Yes, order = 0, context = <CFMachPort 0x7fb4a980cf30 [0x1046fceb0]>{valid = Yes, port = 2307, source = 0x7fb4a980d1c0, callout = _ZL20notify_port_callbackP12__CFMachPortPvlS1_ (0x1052e7328), context = <CFMachPort context 0x0>}}
    22 : <CFRunLoopObserver 0x7fb4a942b8d0 [0x1046fceb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1054326a3), context = <CFArray 0x7fb4a942b800 [0x1046fceb0]>{type = mutable-small, count = 0, values = ()}}
},

modes = <CFBasicHash 0x7fb4a9613d70 [0x1046fceb0]>{type = mutable set, count = 5,
entries =>
    2 : <CFRunLoopMode 0x7fb4a950a5d0 [0x1046fceb0]>{name = UITrackingRunLoopMode, port set = 0x1d03, timer port = 0x1e03, 
        sources0 = <CFBasicHash 0x7fb4a950a530 [0x1046fceb0]>{type = mutable set, count = 3,
        entries =>
            0 : <CFRunLoopSource 0x7fb4a9808a10 [0x1046fceb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x0, callout = PurpleEventSignalCallback (0x109ab46e2)}}
            1 : <CFRunLoopSource 0x7fb4a9634160 [0x1046fceb0]>{signalled = Yes, valid = Yes, order = 0, context = <CFRunLoopSource context>{version = 0, info = 0x0, callout = FBSSerialQueueRunLoopSourceHandler (0x10bc34033)}}
            2 : <CFRunLoopSource 0x7fb4a942ba10 [0x1046fceb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x7fb4a9626b30, callout = _UIApplicationHandleEventQueue (0x105432a2a)}}
        },
        sources1 = <CFBasicHash 0x7fb4a950a570 [0x1046fceb0]>{type = mutable set, count = 7,
        entries =>
            1 : <CFRunLoopSource 0x7fb4a96267c0 [0x1046fceb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 1, info = 0x2103, callout = PurpleEventCallback (0x109ab6e6b)}}
            3 : <CFRunLoopSource 0x7fb4a950a3f0 [0x1046fceb0]>{signalled = No, valid = Yes, order = 0, context = <CFMachPort 0x7fb4a9808950 [0x1046fceb0]>{valid = Yes, port = 1a03, source = 0x7fb4a950a3f0, callout = __IOHIDEventSystemClientQueueCallback (0x10a4665ef), context = <CFMachPort context 0x7fb4a98086b0>}}
            6 : <CFRunLoopSource 0x7fb4a9823240 [0x1046fceb0]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource MIG Server> {port = 15879, subsystem = 0x105fb43b0, context = 0x7fb4a9816980}}
            9 : <CFRunLoopSource 0x7fb4a942b5e0 [0x1046fceb0]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource MIG Server> {port = 11527, subsystem = 0x105f95d20, context = 0x0}}
            10 : <CFRunLoopSource 0x7fb4a980d1c0 [0x1046fceb0]>{signalled = No, valid = Yes, order = 0, context = <CFMachPort 0x7fb4a980cf30 [0x1046fceb0]>{valid = Yes, port = 2307, source = 0x7fb4a980d1c0, callout = _ZL20notify_port_callbackP12__CFMachPortPvlS1_ (0x1052e7328), context = <CFMachPort context 0x0>}}
            11 : <CFRunLoopSource 0x7fb4a950a9c0 [0x1046fceb0]>{signalled = No, valid = Yes, order = 1, context = <CFMachPort 0x7fb4a96263b0 [0x1046fceb0]>{valid = Yes, port = 1803, source = 0x7fb4a950a9c0, callout = __IOMIGMachPortPortCallback (0x10a46e572), context = <CFMachPort context 0x7fb4a9626340>}}
            12 : <CFRunLoopSource 0x7fb4a950a890 [0x1046fceb0]>{signalled = No, valid = Yes, order = 0, context = <CFMachPort 0x7fb4a99002e0 [0x1046fceb0]>{valid = Yes, port = 1b03, source = 0x7fb4a950a890, callout = __IOHIDEventSystemClientAvailabilityCallback (0x10a466850), context = <CFMachPort context 0x7fb4a98086b0>}}
        },
        observers = <CFArray 0x7fb4a942b740 [0x1046fceb0]>{type = mutable-small, count = 6, values = (
            0 : <CFRunLoopObserver 0x7fb4a942b830 [0x1046fceb0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1054326a3), context = <CFArray 0x7fb4a942b800 [0x1046fceb0]>{type = mutable-small, count = 0, values = ()}}
            1 : <CFRunLoopObserver 0x7fb4a98172d0 [0x1046fceb0]>{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x1057e3965), context = <CFRunLoopObserver context 0x0>}
            2 : <CFRunLoopObserver 0x7fb4a962e270 [0x1046fceb0]>{valid = Yes, activities = 0xfffffff, repeats = No, order = 0, callout = orderOutContextObserverCallout (0x10544c1d9), context = <CFRunLoopObserver context 0x0>}
            3 : <CFRunLoopObserver 0x7fb4a942b6a0 [0x1046fceb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 1999000, callout = _afterCACommitHandler (0x105459751), context = <CFRunLoopObserver context 0x7fb4a9626b30>}
            4 : <CFRunLoopObserver 0x7fb4a9816180 [0x1046fceb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x105226a94), context = <CFRunLoopObserver context 0x0>}
            5 : <CFRunLoopObserver 0x7fb4a942b8d0 [0x1046fceb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1054326a3), context = <CFArray 0x7fb4a942b800 [0x1046fceb0]>{type = mutable-small, count = 0, values = ()}}
        )},
        timers = (null),
        currently 511066692 (6525220218952) / soft deadline in: 1.84467375e+10 sec (@ -1) / hard deadline in: 1.84467375e+10 sec (@ -1)
    },
    3 : <CFRunLoopMode 0x7fb4a9808bf0 [0x1046fceb0]>{name = GSEventReceiveRunLoopMode, port set = 0x1f03, timer port = 0x2003, 
        sources0 = <CFBasicHash 0x7fb4a98083c0 [0x1046fceb0]>{type = mutable set, count = 1,
        entries =>
            0 : <CFRunLoopSource 0x7fb4a9808a10 [0x1046fceb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x0, callout = PurpleEventSignalCallback (0x109ab46e2)}}
        },
        sources1 = <CFBasicHash 0x7fb4a9808ca0 [0x1046fceb0]>{type = mutable set, count = 1,
        entries =>
            0 : <CFRunLoopSource 0x7fb4a9626970 [0x1046fceb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 1, info = 0x2103, callout = PurpleEventCallback (0x109ab6e6b)}}
        },
        observers = (null),
        timers = (null),
        currently 511066692 (6525220881743) / soft deadline in: 1.84467375e+10 sec (@ -1) / hard deadline in: 1.84467375e+10 sec (@ -1)
    },
    4 : <CFRunLoopMode 0x7fb4a9613aa0 [0x1046fceb0]>{name = kCFRunLoopDefaultMode, port set = 0x1003, timer port = 0x1103, 
        sources0 = <CFBasicHash 0x7fb4a950a770 [0x1046fceb0]>{type = mutable set, count = 3,
        entries =>
            0 : <CFRunLoopSource 0x7fb4a9808a10 [0x1046fceb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x0, callout = PurpleEventSignalCallback (0x109ab46e2)}}
            1 : <CFRunLoopSource 0x7fb4a9634160 [0x1046fceb0]>{signalled = Yes, valid = Yes, order = 0, context = <CFRunLoopSource context>{version = 0, info = 0x0, callout = FBSSerialQueueRunLoopSourceHandler (0x10bc34033)}}
            2 : <CFRunLoopSource 0x7fb4a942ba10 [0x1046fceb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x7fb4a9626b30, callout = _UIApplicationHandleEventQueue (0x105432a2a)}}
        },
        sources1 = <CFBasicHash 0x7fb4a950a7b0 [0x1046fceb0]>{type = mutable set, count = 7,
        entries =>
            1 : <CFRunLoopSource 0x7fb4a96267c0 [0x1046fceb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 1, info = 0x2103, callout = PurpleEventCallback (0x109ab6e6b)}}
            3 : <CFRunLoopSource 0x7fb4a950a3f0 [0x1046fceb0]>{signalled = No, valid = Yes, order = 0, context = <CFMachPort 0x7fb4a9808950 [0x1046fceb0]>{valid = Yes, port = 1a03, source = 0x7fb4a950a3f0, callout = __IOHIDEventSystemClientQueueCallback (0x10a4665ef), context = <CFMachPort context 0x7fb4a98086b0>}}
            6 : <CFRunLoopSource 0x7fb4a9823240 [0x1046fceb0]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource MIG Server> {port = 15879, subsystem = 0x105fb43b0, context = 0x7fb4a9816980}}
            9 : <CFRunLoopSource 0x7fb4a942b5e0 [0x1046fceb0]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource MIG Server> {port = 11527, subsystem = 0x105f95d20, context = 0x0}}
            10 : <CFRunLoopSource 0x7fb4a980d1c0 [0x1046fceb0]>{signalled = No, valid = Yes, order = 0, context = <CFMachPort 0x7fb4a980cf30 [0x1046fceb0]>{valid = Yes, port = 2307, source = 0x7fb4a980d1c0, callout = _ZL20notify_port_callbackP12__CFMachPortPvlS1_ (0x1052e7328), context = <CFMachPort context 0x0>}}
            11 : <CFRunLoopSource 0x7fb4a950a9c0 [0x1046fceb0]>{signalled = No, valid = Yes, order = 1, context = <CFMachPort 0x7fb4a96263b0 [0x1046fceb0]>{valid = Yes, port = 1803, source = 0x7fb4a950a9c0, callout = __IOMIGMachPortPortCallback (0x10a46e572), context = <CFMachPort context 0x7fb4a9626340>}}
            12 : <CFRunLoopSource 0x7fb4a950a890 [0x1046fceb0]>{signalled = No, valid = Yes, order = 0, context = <CFMachPort 0x7fb4a99002e0 [0x1046fceb0]>{valid = Yes, port = 1b03, source = 0x7fb4a950a890, callout = __IOHIDEventSystemClientAvailabilityCallback (0x10a466850), context = <CFMachPort context 0x7fb4a98086b0>}}
        },
        observers = <CFArray 0x7fb4a942b7a0 [0x1046fceb0]>{type = mutable-small, count = 6, values = (
            0 : <CFRunLoopObserver 0x7fb4a942b830 [0x1046fceb0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1054326a3), context = <CFArray 0x7fb4a942b800 [0x1046fceb0]>{type = mutable-small, count = 0, values = ()}}
            1 : <CFRunLoopObserver 0x7fb4a98172d0 [0x1046fceb0]>{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x1057e3965), context = <CFRunLoopObserver context 0x0>}
            2 : <CFRunLoopObserver 0x7fb4a962e270 [0x1046fceb0]>{valid = Yes, activities = 0xfffffff, repeats = No, order = 0, callout = orderOutContextObserverCallout (0x10544c1d9), context = <CFRunLoopObserver context 0x0>}
            3 : <CFRunLoopObserver 0x7fb4a942b6a0 [0x1046fceb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 1999000, callout = _afterCACommitHandler (0x105459751), context = <CFRunLoopObserver context 0x7fb4a9626b30>}
            4 : <CFRunLoopObserver 0x7fb4a9816180 [0x1046fceb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x105226a94), context = <CFRunLoopObserver context 0x0>}
            5 : <CFRunLoopObserver 0x7fb4a942b8d0 [0x1046fceb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1054326a3), context = <CFArray 0x7fb4a942b800 [0x1046fceb0]>{type = mutable-small, count = 0, values = ()}}
        )},
        timers = <CFArray 0x7fb4a9809170 [0x1046fceb0]>{type = mutable-small, count = 1, values = (
            0 : <CFRunLoopTimer 0x7fb4a96349b0 [0x1046fceb0]>{valid = Yes, firing = No, interval = 0, tolerance = 0, next fire date = 511066694 (1.441432 @ 6526663103544), callout = (Delayed Perform) UIApplication _accessibilitySetUpQuickSpeak (0x104889162 / 0x105749b63) (/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 8.2.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/UIKit.framework/UIKit), context = <CFRunLoopTimer context 0x7fb4a9629600>}
        )},
        currently 511066692 (6525220919500) / soft deadline in: 1.44218403 sec (@ 6526663103544) / hard deadline in: 1.442184 sec (@ 6526663103544)
    },
    5 : <CFRunLoopMode 0x7fb4a9634350 [0x1046fceb0]>{name = UIInitializationRunLoopMode, port set = 0x2703, timer port = 0x2803, 
        sources0 = <CFBasicHash 0x7fb4a9634400 [0x1046fceb0]>{type = mutable set, count = 1,
        entries =>
            0 : <CFRunLoopSource 0x7fb4a9634160 [0x1046fceb0]>{signalled = Yes, valid = Yes, order = 0, context = <CFRunLoopSource context>{version = 0, info = 0x0, callout = FBSSerialQueueRunLoopSourceHandler (0x10bc34033)}}
        },
        sources1 = <CFBasicHash 0x7fb4a9634440 [0x1046fceb0]>{type = mutable set, count = 0,
        entries =>
        },
        observers = <CFArray 0x7fb4a9816280 [0x1046fceb0]>{type = mutable-small, count = 1, values = (
            0 : <CFRunLoopObserver 0x7fb4a9816180 [0x1046fceb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x105226a94), context = <CFRunLoopObserver context 0x0>}
        )},
        timers = (null),
        currently 511066692 (6525221705051) / soft deadline in: 1.84467375e+10 sec (@ -1) / hard deadline in: 1.84467375e+10 sec (@ -1)
    },
    6 : <CFRunLoopMode 0x7fb4a9b00ab0 [0x1046fceb0]>{name = kCFRunLoopCommonModes, port set = 0x3403, timer port = 0x3503, 
        sources0 = (null),
        sources1 = (null),
        observers = (null),
        timers = (null),
        currently 511066692 (6525221827512) / soft deadline in: 1.84467375e+10 sec (@ -1) / hard deadline in: 1.84467375e+10 sec (@ -1)
    },
}
}

2.2.2CFRunLoopModeRef

还是从源码来分析结构:

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;    /* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    dispatch_source_t _timerSource;
    dispatch_queue_t _queue;
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
    DWORD _msgQMask;
    void (*_msgPump)(void);
#endif
    uint64_t _timerSoftDeadline; /* TSR */
    uint64_t _timerHardDeadline; /* TSR */
};

关于CFRunLoopModeRef,先有几个思维概念:

  • CFRunLoopModeRef代表RunLoop的运行模式,如果没有一个Mode,那么RunLoop就会直接退出;
  • 一个RunLoop包含若干个Mode,每个Mode又包含了若干个Source、Timer、Observer,这些Source、Timer、Observer可以理解为item;如果某个Mode没有一个item,那么RunLoop在该Mode下运行会直接退出;
  • 每次RunLoop启动时,只能指定其中一个Mode,这个Mode被称作CurrentMode;
  • 如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入。

从上一小节的运行Log可以看到,启动App时,系统默认注册了5个Mode:

  • kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行;
  • UITrackingRunLoopMode:用户交互模式,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响;只要有用户交互事件,那么RunLoop就会切换到该模式。
  • kCFRunLoopCommonModes:占位模式。占有了上面两种模式,在这两种模式下都有效果。
  • GSEventReceiveRunLoopMode:接受系统事件的内部Mode,通常用不到;
  • UIInitializationRunLoopMode:在刚启动App时进入的第一个Mode,启动完成后就不再使用;

CFRunLoop对外暴露的管理 Mode 接口只有下面2个:

CF_EXPORT void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef mode);
CF_EXPORT SInt32 CFRunLoopRunInMode(CFStringRef mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);

Mode 暴露的管理 item 的接口有下面几个:

CF_EXPORT Boolean CFRunLoopContainsSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode);
CF_EXPORT void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode);
CF_EXPORT void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode);

CF_EXPORT Boolean CFRunLoopContainsObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef mode);
CF_EXPORT void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef mode);
CF_EXPORT void CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef mode);

CF_EXPORT Boolean CFRunLoopContainsTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CF_EXPORT void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CF_EXPORT void CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);

我们只能通过 mode name 来操作内部的 mode,当传入一个新的 mode name 但 RunLoop 内部没有对应 mode 时,RunLoop会自动创建对应的 CFRunLoopModeRef。对于一个 RunLoop 来说,其内部的 mode 只能增加不能删除

苹果公开提供的 Mode 有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode,可以用这两个 Mode Name 来操作其对应的 Mode。

我们可以通过下面的方式,来获取当前运行的Mode和所有的Mode:

CFRunLoopRef currentRunloop = CFRunLoopGetCurrent();
//获取当前运行的Mode
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(currentRunloop);
NSLog(@"%@", mode);
//获取所有的Mode    
CFArrayRef allModes = CFRunLoopCopyAllModes(currentRunloop);
NSLog(@"%@", allModes);

2.2.3CFRunLoopSourceRef

先看源码:

//CFRunLoop.h文件
typedef struct __CFRunLoopSource * CFRunLoopSourceRef;

//version0
typedef struct {
    CFIndex    version;
    void *    info;
    const void *(*retain)(const void *info);
    void    (*release)(const void *info);
    CFStringRef    (*copyDescription)(const void *info);
    Boolean    (*equal)(const void *info1, const void *info2);
    CFHashCode    (*hash)(const void *info);
    void    (*schedule)(void *info, CFRunLoopRef rl, CFStringRef mode);
    void    (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode);
    void    (*perform)(void *info);
} CFRunLoopSourceContext;

//version1
typedef struct {
    CFIndex    version;
    void *    info;
    const void *(*retain)(const void *info);
    void    (*release)(const void *info);
    CFStringRef    (*copyDescription)(const void *info);
    Boolean    (*equal)(const void *info1, const void *info2);
    CFHashCode    (*hash)(const void *info);
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
    mach_port_t    (*getPort)(void *info);
    void *    (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#else
    void *    (*getPort)(void *info);
    void    (*perform)(void *info);
#endif
} CFRunLoopSourceContext1;

//CFRunLoop.c
struct __CFRunLoopSource {
    CFRuntimeBase _base;
    uint32_t _bits;
    pthread_mutex_t _lock;
    CFIndex _order;            /* immutable */
    CFMutableBagRef _runLoops;
    union {
    CFRunLoopSourceContext version0;    /* immutable, except invalidation */
        CFRunLoopSourceContext1 version1;    /* immutable, except invalidation */
    } _context;
};

CFRunLoopSourceRef 是事件产生的地方。从源码可以看到,它定义了两个版本:Source0 和 Source1:

  • Source0:非基于Port的,只包含了一个回调(函数指针),它并不能主动触发事件,处理的是App内部的事件,App自己负责管理,如点击按钮、点击屏幕、触摸事件、自定义输入源、performSelector:onThread:等。使用时,需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
  • Source1:包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息,Mach Port驱动,如CFMachPort、CFMessagePort。

Source0和Source1的区别,最主要的就是Source0需要手动标记为待处理,并且去唤醒Runloop。Source1不需要。

在这里对触摸事件做一个简单的说明,详细的可看这篇博客

在2.2.1小节的Log中,我们可以看到系统注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用系统注册的一个Source0,对应回调函数 _UIApplicationHandleEventQueue() 进行应用内部的分发。_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。

_UIApplicationHandleEventQueue() 识别了一个手势时,首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。

系统注册了一个 Observer,其回调函数是回调函数是 _UIGestureRecognizerUpdateObserver(),用来监测 BeforeWaiting (Loop即将进入休眠) 事件,其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

下面代码演示了自定义Source的操作:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self addSource];
}

- (void)addSource {
    if (0 == self.index) {  //初始化Source
        CFRunLoopSourceContext context = {
            0,
            NULL,
            NULL,
            NULL,
            NULL,
            NULL,
            NULL,
            schedule,
            cancel,
            perform
        };
        source0 = CFRunLoopSourceCreate(CFAllocatorGetDefault(), 0, &context);
    }
    else if (1 == self.index) {  //添加源
        CFRunLoopAddSource(CFRunLoopGetCurrent(), source0, kCFRunLoopDefaultMode);
    }
    else if (2 == self.index) {  //将source置为待处理并且唤醒Runloop
        CFRunLoopSourceSignal(source0);
        CFRunLoopWakeUp(CFRunLoopGetCurrent());
    }
    else if (3 == self.index) {  //移除源
        CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source0, kCFRunLoopDefaultMode);
        //移除源要做释放操作
        CFRelease(source0);
    }
    self.index++;
}

//添加源到Runloop时回调
//CFRunLoopAddSource(CFRunLoopGetCurrent(), source0, kCFRunLoopDefaultMode);
void schedule(void *info, CFRunLoopRef rl, CFRunLoopMode mode) {
    NSLog(@"LeeGof %s", __func__);
}

//移除源的时候回调
//CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source0, kCFRunLoopDefaultMode);
void cancel(void *info, CFRunLoopRef rl, CFRunLoopMode mode) {
    NSLog(@"LeeGof %s", __func__);
}

//手动将source置为待处理并且唤醒Runloop的时候回调
//CFRunLoopSourceSignal(source0);
//CFRunLoopWakeUp(CFRunLoopGetCurrent());
void perform(void *info){
    NSLog(@"LeeGof %s", __func__);
}

关于Source1里说的port,这里我们先看一下官方文档的介绍:

Communication occurs between NSPort objects, which typically reside in different threads or tasks. The distributed objects system uses NSPort objects to send NSPortMessage objects back and forth. Implement interapplication communication using distributed objects whenever possible and use NSPort objects only when necessary.

To receive incoming messages, NSPort objects must be added to an NSRunLoop object as input sources. NSConnection objects automatically add their receive port when initialized.

When an NSPort object receives a port message, it forwards the message to its delegate in a handleMachMessage: or handlePortMessage: message. The delegate should implement only one of these methods to process the incoming message in whatever form desired. handleMachMessage: provides a message as a raw Mach message beginning with a msg_header_t structure. handlePortMessage: provides a message as an NSPortMessage object, which is an object-oriented wrapper for a Mach message. If a delegate has not been set, the NSPort object handles the message itself.

When you are finished using a port object, you must explicitly invalidate the port object prior to sending it a release message. Similarly, if your application uses garbage collection, you must invalidate the port object before removing any strong references to it. If you do not invalidate the port, the resulting port object may linger and create a memory leak. To invalidate the port object, invoke its invalidate method.

Foundation defines three concrete subclasses of NSPort. NSMachPort and NSMessagePort allow local (on the same machine) communication only. NSSocketPort allows for both local and remote communication, but may be more expensive than the others for the local case. When creating an NSPort object, using allocWithZone: or port, an NSMachPort object is created instead.
View Code

对于上面的官方介绍,这里做一个总结:

  • NSPort是通信通道的抽象类,我们能够使用它,进行线程间的通信;
  • 要接收传入消息,必须将NSPort对象添加到NSRunloop对象中作为输入源;
  • NSPort用完之后,要进行释放。不然产生的对象有可能造成内存泄漏;要使Port对象无效,可以调用它的invalidate方法;
  • Foundation定义了NSPort的三个具体子类,其中NSMachPort和NSMessagePort只允许本地(在同一台机器)通信;NSSocketPort支持本地和远程通信,但是对于本地情况,可能比其他的要昂贵。在使用allocWithZone:或port创建NSPort对象时,将创建一个NSMachport对象。

下面我们看一个基于Port的线程之间的通信示例:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    self.isMainToSub = YES;
    [NSThread currentThread].name = @"主线程";
    
    self.mainThreadPort = [NSPort port];
    self.mainThreadPort.delegate = self;
    [[NSRunLoop currentRunLoop] addPort:self.mainThreadPort forMode:NSDefaultRunLoopMode];
    
    [self addSubThread];
}

- (void)addSubThread {
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        self.subThreadPort = [NSPort port];
        self.subThreadPort.delegate = self;
        
        [[NSRunLoop currentRunLoop] addPort:self.subThreadPort forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];  //子线程的Runloop需要手动开启
    }];
    [thread setName:@"子线程"];
    [thread start];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSMutableArray *components = [NSMutableArray array];
    if (self.isMainToSub) {
        NSData *data = [@"hello" dataUsingEncoding:NSUTF8StringEncoding];
        [components addObject:data];
        //在主线程中通过port,给子线程发送一个消息
        [self.subThreadPort sendBeforeDate:[NSDate date] components:components from:self.mainThreadPort reserved:0];
    }
    else {
        NSData *data = [@"world" dataUsingEncoding:NSUTF8StringEncoding];
        [components addObject:data];
        //在子线程中通过port,给主线程发送一个消息
        [self.mainThreadPort sendBeforeDate:[NSDate date] components:components from:self.subThreadPort reserved:0];
    }
    self.isMainToSub = !self.isMainToSub;
}

#pragma mark - NSPortDelegate

- (void)handlePortMessage:(id)message {
    NSLog(@"LeeGof %@", [NSThread currentThread].name);
    
    NSArray *components = [message valueForKey:@"components"];
    if (components && [components count] > 0) {
        NSData *data = [components objectAtIndex:0];
        NSString *mes = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"LeeGof %@", mes);
    }
}

2.2.4CFRunLoopTimerRef 

源码: 

//CFRunLoop.h
typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;

//CFRunLoop.c
struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFMutableSetRef _rlModes;
    CFAbsoluteTime _nextFireDate;
    CFTimeInterval _interval;        /* immutable */
    CFTimeInterval _tolerance;          /* mutable */
    uint64_t _fireTSR;            /* TSR units */
    CFIndex _order;            /* immutable */
    CFRunLoopTimerCallBack _callout;    /* immutable */
    CFRunLoopTimerContext _context;    /* immutable, except invalidation */
};

CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。常见的就是NSTimer和performSelector:withObject:afterDelay:的使用。

CFRunLoopTimerRef的示例如下:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    CFRunLoopTimerRef timer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault, 0, 1, 0, 0, ^(CFRunLoopTimerRef timer) {
        NSLog(@"%s", __func__);
    });
    CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopDefaultMode);
}

2.2.5CFRunLoopObserverRef

源码:

//CFRunLoop.h
typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;

//CFRunLoop.c
struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFIndex _rlCount;
    CFOptionFlags _activities;        /* immutable */
    CFIndex _order;            /* immutable */
    CFRunLoopObserverCallBack _callout;    /* immutable */
    CFRunLoopObserverContext _context;    /* immutable, except invalidation */
};

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

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),  //即将进入RunLoop (1)
    kCFRunLoopBeforeTimers = (1UL << 1),  //即将处理Timer (2)
    kCFRunLoopBeforeSources = (1UL << 2),  //即将处理Source  (4)
    kCFRunLoopBeforeWaiting = (1UL << 5),  //即将进入休眠  (32)
    kCFRunLoopAfterWaiting = (1UL << 6),  //从休眠中唤醒  (64)
    kCFRunLoopExit = (1UL << 7),  //即将退出RunLoop  (128)
    kCFRunLoopAllActivities = 0x0FFFFFFFU  //监听RunLoop的所有状态
};

可以在main函数中加如下代码来监控RunLoop的状态变化:

        CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
            NSLog(@"CFRunLoopActivity : %lu", activity);
        });
        CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
        CFRelease(observer);

系统在主线程RunLoop注册了两个Observer,回调都是_wrapRunLoopWithAutoreleasePoolHandler

  • 第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
  • 第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠)调用 _objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

另外,系统还注册了一个Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,其回调是_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv(),这个函数函数会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

2.2.6RunLoop callout

 主线程几乎所有函数都从如下六个之一的函数调起:

  • __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
  • __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
  • __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
  • __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
  • __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
  • __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__

2.3RunLoop运行机制

这个官方网站上有介绍,如下图所示:

我们这里采用YY大神的图来做讲解:

关于上图,先做一个说明:Source1在处理的时候会分发一些操作给Source0去处理,Source0中可能存在一些Timer出现,所以会回到第二步重新处理Timer和Source0,处理完后到第五步,直到没有Source1,没有事情可做,进入休眠状态,当外部有事件就会立即唤醒RunLoop。

对应的源码如下:

// 用DefaultMode启动
void CFRunLoopRun(void) {    /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

// 用指定的Mode启动,允许设置RunLoop超时时间
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

// RunLoop的实现
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    __CFRunLoopLock(rl);
    // 首先根据modeName找到对应mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
    Boolean did = false;
    if (currentMode) __CFRunLoopModeUnlock(currentMode);
    __CFRunLoopUnlock(rl);
    return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
    }
    volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
    CFRunLoopModeRef previousMode = rl->_currentMode;
    rl->_currentMode = currentMode;
    int32_t result = kCFRunLoopRunFinished;

    // 1. 通知 Observers: RunLoop 即将进入 loop
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);

    // 10. 通知 Observers: RunLoop 即将退出
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

        __CFRunLoopModeUnlock(currentMode);
        __CFRunLoopPopPerRunData(rl, previousPerRun);
    rl->_currentMode = previousMode;
    __CFRunLoopUnlock(rl);
    return result;
}

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    //省略部分代码...

    // 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
    if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
    // 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
    if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
    // 执行被加入的block
    __CFRunLoopDoBlocks(rl, rlm);

    // 4. RunLoop 触发 Source0 (非port) 回调
    Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
    if (sourceHandledThisLoop) {
        // 执行被加入的block
        __CFRunLoopDoBlocks(rl, rlm);
    }

    Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);

    // 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息
    if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        msg = (mach_msg_header_t *)msg_buffer;
        if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
            goto handle_msg;
        }
#elif DEPLOYMENT_TARGET_WINDOWS
        if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
            goto handle_msg;
        }
#endif
    }

    didDispatchPortLastTime = false;

    // 6. 通知 Observers: RunLoop 的线程即将进入休眠(sleep)
    if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
    __CFRunLoopSetSleeping(rl);
    

    __CFPortSetInsert(dispatchPort, waitSet);
        
    __CFRunLoopModeUnlock(rlm);
    __CFRunLoopUnlock(rl);

    CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();

    // 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
    // • 一个基于 port 的Source 的事件。
    // • 一个 Timer 到时间了
    // • RunLoop 自身的超时时间到了
    // • 被其他什么调用者手动唤醒
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        do {
            if (kCFUseCollectableAllocator) {
                // objc_clear_stack(0);
                // <rdar://problem/16393959>
                memset(msg_buffer, 0, sizeof(msg_buffer));
            }
            msg = (mach_msg_header_t *)msg_buffer;
            
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
            
            if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
                // Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
                while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
                if (rlm->_timerFired) {
                    // Leave livePort as the queue port, and service timers below
                    rlm->_timerFired = false;
                    break;
                } else {
                    if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
                }
            } else {
                // Go ahead and leave the inner loop.
                break;
            }
        } while (1);
#else
        if (kCFUseCollectableAllocator) {
            // objc_clear_stack(0);
            // <rdar://problem/16393959>
            memset(msg_buffer, 0, sizeof(msg_buffer));
        }
        msg = (mach_msg_header_t *)msg_buffer;
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
#endif
        
        
#elif DEPLOYMENT_TARGET_WINDOWS
        // Here, use the app-supplied message queue mask. They will set this if they are interested in having this run loop receive windows messages.
        __CFRunLoopWaitForMultipleObjects(waitSet, NULL, poll ? 0 : TIMEOUT_INFINITY, rlm->_msgQMask, &livePort, &windowsMessageReceived);
#endif
        
        __CFRunLoopLock(rl);
        __CFRunLoopModeLock(rlm);

        rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));

        // Must remove the local-to-this-activation ports in on every loop
        // iteration, as this mode could be run re-entrantly and we don't
        // want these ports to get serviced. Also, we don't want them left
        // in there if this function returns.

        __CFPortSetRemove(dispatchPort, waitSet);
        
        __CFRunLoopSetIgnoreWakeUps(rl);

        // user callouts now OK again
    __CFRunLoopUnsetSleeping(rl);

    // 8. 通知 Observers: RunLoop 的线程刚刚被唤醒
    if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

    // 收到消息,处理消息
        handle_msg:;
        __CFRunLoopSetIgnoreWakeUps(rl);

#if DEPLOYMENT_TARGET_WINDOWS
        if (windowsMessageReceived) {
            // These Win32 APIs cause a callout, so make sure we're unlocked first and relocked after
            __CFRunLoopModeUnlock(rlm);
        __CFRunLoopUnlock(rl);

            if (rlm->_msgPump) {
                rlm->_msgPump();
            } else {
                MSG msg;
                if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE | PM_NOYIELD)) {
                    TranslateMessage(&msg);
                    DispatchMessage(&msg);
                }
            }
            
            __CFRunLoopLock(rl);
        __CFRunLoopModeLock(rlm);
         sourceHandledThisLoop = true;
            
            // To prevent starvation of sources other than the message queue, we check again to see if any other sources need to be serviced
            // Use 0 for the mask so windows messages are ignored this time. Also use 0 for the timeout, because we're just checking to see if the things are signalled right now -- we will wait on them again later.
            // NOTE: Ignore the dispatch source (it's not in the wait set anymore) and also don't run the observers here since we are polling.
            __CFRunLoopSetSleeping(rl);
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            
            __CFRunLoopWaitForMultipleObjects(waitSet, NULL, 0, 0, &livePort, NULL);
            
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);            
            __CFRunLoopUnsetSleeping(rl);
            // If we have a new live port then it will be handled below as normal
        }
        
        
#endif
        if (MACH_PORT_NULL == livePort) {
            CFRUNLOOP_WAKEUP_FOR_NOTHING();
            // handle nothing
        } else if (livePort == rl->_wakeUpPort) {
            CFRUNLOOP_WAKEUP_FOR_WAKEUP();
            // do nothing on Mac OS
#if DEPLOYMENT_TARGET_WINDOWS
            // Always reset the wake up port, or risk spinning forever
            ResetEvent(rl->_wakeUpPort);
#endif
        }
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        // 9.1 如果一个 Timer 到时间了,触发这个Timer的回调
        else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                // Re-arm the next timer, because we apparently fired early
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
#if USE_MK_TIMER_TOO
        // 9.1 如果一个 Timer 到时间了,触发这个Timer的回调
        else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            // On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled.
            // In this case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. adding or removing timers). The fix for the issue is to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                // Re-arm the next timer
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
        // 9.2 如果有dispatch到main_queue的block,执行block
        else if (livePort == dispatchPort) {
            CFRUNLOOP_WAKEUP_FOR_DISPATCH();
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
#if DEPLOYMENT_TARGET_WINDOWS
            void *msg = 0;
#endif
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
            sourceHandledThisLoop = true;
            didDispatchPortLastTime = true;
        } 
        // 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
        else {
            CFRUNLOOP_WAKEUP_FOR_SOURCE();
            
            // If we received a voucher from this mach_msg, then put a copy of the new voucher into TSD. CFMachPortBoost will look in the TSD for the voucher. By using the value in the TSD we tie the CFMachPortBoost to this received mach_msg explicitly without a chance for anything in between the two pieces of code to set the voucher again.
            voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release);

            // Despite the name, this works for windows handles as well
            CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
            if (rls) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        mach_msg_header_t *reply = NULL;
        sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
        if (NULL != reply) {
            (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
            CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
        }
#elif DEPLOYMENT_TARGET_WINDOWS
                sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
#endif
        }
            
            // Restore the previous voucher
            _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release);
            
        } 
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
#endif
    // 执行加入到Loop的block
    __CFRunLoopDoBlocks(rl, rlm);
        
    // 进入loop时参数说处理完事件就返回
    if (sourceHandledThisLoop && stopAfterHandle) {
        retVal = kCFRunLoopRunHandledSource;
        }
    // 超出传入参数标记的超时时间
    else if (timeout_context->termTSR < mach_absolute_time()) {
            retVal = kCFRunLoopRunTimedOut;
    } else if (__CFRunLoopIsStopped(rl)) {  // 被外部调用者强制停止了
            __CFRunLoopUnsetStopped(rl);
        retVal = kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {
        rlm->_stopped = false;
        retVal = kCFRunLoopRunStopped;
    } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {  // source/timer/observer一个都没有了
        retVal = kCFRunLoopRunFinished;
    }
        
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        voucher_mach_msg_revert(voucherState);
        os_release(voucherCopy);
#endif

    } while (0 == retVal);  // 如果没超时,mode里没空,loop也没被停止,那继续loop

    if (timeout_timer) {
        dispatch_source_cancel(timeout_timer);
        dispatch_release(timeout_timer);
    } else {
        free(timeout_context);
    }

    return retVal;
}

可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

当 RunLoop 进行回调时,一般都是通过一个很长的函数调用出去,对于上面的运行机制源码,我们采用2.2.6小节说的几个回调函数来简单描述一下:

{
    // 1. 通知Observers,即将进入RunLoop
    // 此处有Observer会创建AutoreleasePool:
    _objc_autoreleasePoolPush();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);

    do {
        // 2. 通知 Observers: 即将触发 Timer 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
        // 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        // 4. 触发 Source0 (非基于port的) 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        // 6. 通知Observers,即将进入休眠
        /// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
 
        // 7. sleep to wait msg.
        mach_msg() -> mach_msg_trap();
        
         // 8. 通知Observers,线程被唤醒
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
 
        // 9. 如果是被Timer唤醒的,回调Timer
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
 
        // 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
 
        // 9. 如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
 
 
    } while (...);
 
    // 10. 通知Observers,即将退出RunLoop
    // 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}

关于Runloop实现休眠的原理,这里做一个简单的解释:

  • 实现休眠实际上是调用了内核的API(mach_msg),进入内核态,由内核来将线程置于休眠;
  • 有消息需要处理的时候,就唤醒线程,回到用户态,来处理消息。

3.实战篇

3.1Timer使用

一步步来,首先看一下我们创建Timer的常用方式:

[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(startTimer) userInfo:nil repeats:YES];

上面创建一个Timer,在正常模式下可以运行,但当我们滚动TextView的时候,可以看到它并没有执行:

 怎么样可以使定时器在滑动TextView的时候还执行呢?

    //开启定时器并在NSRunLoopCommonModes下运行
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(startTimer) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

上面创建的定时器是在主线程进行执行的。现在思考这么一个应用场景:如果我们的定时器执行的是一个耗时操作,我们能把定时器的执行放在主线程执行吗?会带来什么结果呢?答案是否定的,因为将耗时操作放在主线程,那么会造成主线程的卡顿,用户体验很不好。该怎么做呢?从上篇文章介绍的GCD知道,我们一般是会将耗时操作放在其他线程中执行。下面我们来看一下代码:

1     dispatch_async(dispatch_get_global_queue(0, 0), ^{
2         self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(startTimer) userInfo:nil repeats:YES];
3         [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
4     });

大家思考一下:上面的定时器会不会执行?

通过运行,可以看到上面的定时器没有执行。为什么呢?回到前面的知识点:主线程的RunLoop已经自动创建和启动了,子线程的RunLoop需要手动创建和启动。稍微调整一下上面的代码:

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(startTimer) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
        [[NSRunLoop currentRunLoop] run];
    });

重新运行,可以看到定时器可以执行了。NSThread创建的线程也是同样的:

    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(startTimer) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
        [[NSRunLoop currentRunLoop] run];
    }];
    [thread start];

接下来再思考一个问题:怎么停止这个RunLoop?

    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(startTimer) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
        while (!self.isFinish) {
            [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
        }
    }];
    [thread start];

3.2延迟加载

在上篇分析GCD的文章中,我们了解到:如果有耗时操作,就抛给其他线程执行,以避免造成主线程的阻塞。在实际项目中,这是我们避免出现卡顿现象的法宝。这里,我们思考一个问题:如果耗时操作是UI方面的,我们该怎么办?比如说这种应用场景:有很多分辨率高的图片在表格中展示。类似下面这个产品:

拖动时,可以看到存在明显的卡顿现象。下面我们做一个简化版的Demo,来分析这种现象。首先,我们先看看常规的加载方式:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"ListCell"];
    
    
    UILabel *lblTitle = [[UILabel alloc] initWithFrame:(CGRect){10, 5, kScreenWidth - 20, 20}];
    lblTitle.backgroundColor = [UIColor clearColor];
    lblTitle.text = [NSString stringWithFormat:@"This is the %ld cell", (long)indexPath.row];
    [cell addSubview:lblTitle];
    
    //常规方式
    [GofImageTableViewController createImageViewWithTag:1 cell:cell];
    [GofImageTableViewController createImageViewWithTag:2 cell:cell];
    [GofImageTableViewController createImageViewWithTag:3 cell:cell];
    [GofImageTableViewController createImageViewWithTag:4 cell:cell];
    
    return cell;
}

+ (void)createImageViewWithTag:(NSInteger)tag
                                   cell:(UITableViewCell *)cell {
    float width = (kScreenWidth - 50) / 4;
    UIImageView *imageView = [[UIImageView alloc] initWithFrame:(CGRect){(10 * tag + width * (tag - 1)), 30, width, width}];
    NSString *strImagePath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"%ld", (long)tag] ofType:@"png"];
    UIImage *image = [UIImage imageWithContentsOfFile:strImagePath];
    imageView.contentMode = UIViewContentModeScaleAspectFit;
    imageView.image = image;
    
    [cell addSubview:imageView];
}

这种方式加载的效果和上面的示例一样:

 

通过运行我们可以看到:在iPhone 7 Plus 模拟器上运行,效果是很卡顿的。这是为什么呢?

因为在每一次RunLoop循环中,需要绘制屏幕上的所有点,这里需要渲染很多高清图片(最多一次是24张)。现象我们看到了,现在思考一下怎么解决这个问题呢?

我们知道上面的问题在于,RunLoop监测和处理我们的UI交互,在每一次的RunLoop循环中,需要去绘制屏幕上的所有点,当图片是高清的时候,渲染这些图片需要耗费的时间就多。那么可不可以这样来做:每一次RunLoop循环,我们仅加载一张图片,采用分步加载的思想来处理。下面我们就来分步骤做说明:

第一步:监听RunLoop循环

/**
 添加RunLoop观察者
 */
- (void)addRunLoopObserver {
    
    //1.获取当前RunLoop
    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    
    //2.定义上下文
    CFRunLoopObserverContext context = {
        0,
        (__bridge void *)(self),
        NULL,
        NULL,
        NULL
    };
    
    //3.创建观察者
    observer = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 0, &callBack, &context);
    
    //4.给当前RunLoop添加观察者
    CFRunLoopAddObserver(runLoop, observer, kCFRunLoopDefaultMode);
    
    //5.释放
    CFRelease(observer);
}

/**
 RunLoop回调函数
 */
static void callBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    
}

第二步:将耗时操作保存在一个数组中,暂不执行。这里有一个小技巧,由于页面最多同时显示24张图片,那么我们这个任务数组的最大长度,我们可以定义为24,这样不显示的就不需要去加载了。

@property (nonatomic, strong) NSMutableArray    *arrTasks;  //!<存放耗时任务的数组
@property (nonatomic, assign) NSUInteger        iMaxTask;  //!<数组中最大任务数量(因为这里是加载图片,一屏最多24张图片,因此设置该值为24)

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"ListCell"];
    
    
    UILabel *lblTitle = [[UILabel alloc] initWithFrame:(CGRect){10, 5, kScreenWidth - 20, 20}];
    lblTitle.backgroundColor = [UIColor clearColor];
    lblTitle.text = [NSString stringWithFormat:@"This is the %ld cell", (long)indexPath.row];
    [cell addSubview:lblTitle];
    
    [self addTask:^{
        [GofImageTableViewController createImageViewWithTag:1 cell:cell];
    }];
    [self addTask:^{
        [GofImageTableViewController createImageViewWithTag:2 cell:cell];
    }];
    [self addTask:^{
        [GofImageTableViewController createImageViewWithTag:3 cell:cell];
    }];
    [self addTask:^{
        [GofImageTableViewController createImageViewWithTag:4 cell:cell];
    }];
    
    return cell;
}

- (void)addTask:(RunLoopBlock)task {
    //保存新的任务
    [self.arrTasks addObject:task];
    
    //清除旧的任务
    if (self.arrTasks.count > self.iMaxTask) {
        [self.arrTasks removeObjectAtIndex:0];
    }
}

第三步:每次RunLoop循环的回调函数,从数组中拿出一个任务执行。

/**
 RunLoop回调函数
 */
static void callBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
//    NSLog(@"RunLoop回调");
    //拿到控制器对象
    GofImageTableViewController *vc = (__bridge GofImageTableViewController *)info;
    
    if (vc.arrTasks.count == 0) {
        return;
    }
    
    //从数组中取到任务
    RunLoopBlock task = vc.arrTasks.firstObject;
    //执行任务
    task();
    //干掉已经执行完成的任务
    [vc.arrTasks removeObjectAtIndex:0];
}

做完这三步之后,我们来看看运行效果:

这是我们想要的效果吗?明显不是,存在BUG,因为RunLoop休息之后,我们的图片加载任务就没有执行。那么怎么去唤醒RunLoop呢?我们可以加一个空的定时器来唤醒它:

@property (nonatomic, strong) NSTimer           *timer;  //!<空定时器方法,用于唤醒RunLoop

self.timer = [NSTimer scheduledTimerWithTimeInterval:0.001 target:self selector:@selector(startTimer) userInfo:nil repeats:YES];

/**
 空定时器方法,用于唤醒RunLoop
 */
- (void)startTimer {
    
}

继续看效果:

从效果可以看到,我们完美的解决了卡顿的问题。但还不是很完美,因为在拖拽过程中,图片没有加载。怎么调整一下,让它拖拽过程中也加载图片呢?很简单,只需要修改第一个步骤中的kCFRunLoopDefaultMode为kCFRunLoopCommonModes即可。 

3.3线程常驻

+ (GofThread *)sharedThread {
    static id sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[NSThread alloc] initWithTarget:self selector:@selector(_gofThread:) object:nil];
        [sharedInstance start];
    });
    return sharedInstance;
}

+ (void)_gofThread:(id)object {
    NSLog(@"新线程,新气象");
    
    @autoreleasepool {
        [[NSThread currentThread] setName:@"com.gof.threadtest"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
} 
posted @ 2017-03-13 16:14  LeeGof  阅读(718)  评论(1编辑  收藏  举报