OC内存管理-runloop

一、runloop 简介

RunLoop是通过内部维护的 事件循环(Event Loop) 来对 事件/消息 进行管理的一个对象。

  • 没有消息处理时,休眠已避免资源占用,由用户态切换到内核态。
  • 有消息需要处理时,立刻被唤醒,由内核态切换到用户态。
image.png

runloop的官方文档在thread篇章Run Loops,也就从侧面说明了runloop是与线程息息相关的。

1.1 runloop 输入源与处理机制

官方有如下一张图:


`runloop`与`source`结构

线程的输入源:

  • Port Source:基于端口的输入源。
  • Custom Source:自定义输入源
  • performSelectorCocoa执行Selector的源。
  • Timer Source:定时源。

线程针对输入源的处理机制:

  • handlePort:处理基于端口的输入源。
  • customSrc:处理用户自定义输入源。
  • mySelector:处理Selector的源。
  • timerFired:处理定时源。

有以下案例:

- (void)sourcesTest {
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notificationAction:) name:@"notificationTest" object:nil];
    //__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
    [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"timer action");
    }];
    [self performSelector:@selector(performSelectorAction) withObject:nil afterDelay:1.0];
//
//     __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
//    __FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__
    void (^block)(void) = ^{
        NSLog(@"block action");
    };
    block();

    // __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"main queue");
    });
}

//__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
- (void)performSelectorAction {
    NSLog(@"timer  action");
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    // __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
    NSLog(@"touches action");
    [[NSNotificationCenter defaultCenter] postNotificationName:@"notificationTest" object:nil];
}
- (void)notificationAction:(NSNotification *)noti {
    // __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__
    NSLog(@"notification  action");
}

timerperformSelector对应的回调都是__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__

image.png

后续的调用函数不同。

block对应__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__

image.png

不过后续调用到了__FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__

主线程对应__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__:

image.png

系统触摸事件对应__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__

image.png

通知事件对应__CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__

image.png

小结:

  • 调用timer:__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__NSTimerperformSelector都属于timer
  • 响应source0: __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__block,系统触摸事件(触摸事件先交给source1唤醒runloop然后交给source0处理)
  • 响应source1: __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__。处理系统事件
  • GCD主队列:__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
  • observer源: __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__。通知

二、CFRunloop 的使用

2.1 CFRunLoopMode 验证

- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
    NSLog(@"scrollViewDidScroll: %@",[NSRunLoop currentRunLoop].currentMode);
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
    NSLog(@"scrollViewDidEndDecelerating: %@",[NSRunLoop currentRunLoop].currentMode);
}

- (void)runloopModeTest {
    //获取当前runloop
    CFRunLoopRef runloop = CFRunLoopGetCurrent();
    //获取当前mode
    CFRunLoopMode runloopMode  = CFRunLoopCopyCurrentMode(runloop);
    NSLog(@"runloopMode: %@",runloopMode);
    //获取所有 mode
    CFArrayRef modeArray= CFRunLoopCopyAllModes(runloop);
    NSLog(@"modeArray: %@",modeArray);

    NSTimer *timer = [NSTimer timerWithTimeInterval:3 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"timerWithTimeInterval mode: %@",[[NSRunLoop currentRunLoop] currentMode]);
    }];
    //timer 添加到 runloop 的 commonModes
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

滚动页面输出:

runloopMode: kCFRunLoopDefaultMode
//主线程runloop modes
modeArray: (
    UITrackingRunLoopMode,
    GSEventReceiveRunLoopMode,
    kCFRunLoopDefaultMode,
    kCFRunLoopCommonModes
)
//页面静止
timerWithTimeInterval mode: kCFRunLoopDefaultMode
//页面滚动
scrollViewDidScroll: UITrackingRunLoopMode
scrollViewDidScroll: UITrackingRunLoopMode
timerWithTimeInterval mode: UITrackingRunLoopMode
scrollViewDidScroll: UITrackingRunLoopMode
scrollViewDidEndDecelerating: UITrackingRunLoopMode
//页面静止
timerWithTimeInterval mode: kCFRunLoopDefaultMode

页面滚动过程中处于UITrackingRunLoopMode,静止状态处于kCFRunLoopDefaultMode

2.2 CFRunLoopTimerRef

@interface ViewController () {
    CFRunLoopTimerRef timerRef;
}
@end

- (void)cfTimerTest {
    /** CFRunLoopTimerContext   timer 上下文
     version: 版本
     info: 传递参数
     void *(*retain)(const void *info): retain 操作
     void    (*release)(const void *info): release 操作
     (*copyDescription)(const void *info): copy 描述信息
     */
    CFRunLoopTimerContext context = {
        0,
        ((__bridge void *)self),
        NULL,
        NULL,
        NULL
    };
    //获取当前 runloop
    CFRunLoopRef runloop = CFRunLoopGetCurrent();

    /** CFRunLoopTimerCreate 创建 timer
     allocator: 用于分配对象的内存
     fireDate:在什么是触发 (距离现在)
     interval: 每隔多少时间触发一次
     flags: 未来参数
     order: CFRunLoopObserver的优先级 当在Runloop同一运行阶段中有多个CFRunLoopObserver 正常情况下使用0
     callout: 回调,比如触发事件。
     context:上下文记录信息
     */
    //创建timer
    timerRef = CFRunLoopTimerCreate(kCFAllocatorDefault, 0, 3, 0, 0, _runLoopTimerCallBack, &context);
    CFRunLoopAddTimer(runloop, timerRef, kCFRunLoopCommonModes);
}

// CFRunLoopTimerCallBack typedef void (*CFRunLoopTimerCallBack)(CFRunLoopTimerRef timer, void *info);
void _runLoopTimerCallBack(CFRunLoopTimerRef timer, void *info) {// info 为 context 中的 info
    NSLog(@"_runLoopTimerCallBack:%@,%@,%@",timer,info,[NSRunLoop currentRunLoop].currentMode);
}

- (void)dealloc {
    [self invalidCFTimer];
}

// 移除 timer
- (void)invalidCFTimer {
    CFRunLoopRef runloop = CFRunLoopGetCurrent();
    if (timerRef && CFRunLoopContainsTimer(runloop, timerRef, kCFRunLoopCommonModes)) {
        CFRunLoopRemoveTimer(runloop, timerRef, kCFRunLoopCommonModes);
    }
}
  • CFRunLoopTimerContext创建上下文用于传递参数,info就是最终callback回调中的info信息。
  • CFRunLoopTimerCreate主要是回调函数以及时间间隔的设置。
  • dealloc中需要移除timer,不释放有可能crash,不会造成循环引用。需要判断timer是否存在以及是否在要移除的runloop中。

2.3 CFRunLoopObserverRef

CFRunLoopObserverRef observerRef;

- (void)cfObserverTest {
    //context 与 timer 的相同
    CFRunLoopObserverContext context = {
        0,
        ((__bridge void *)self),
        NULL,
        NULL,
        NULL
    };
    CFRunLoopRef runloop = CFRunLoopGetCurrent();

    /**
     allocator: 用于分配对象的内存
     activities: 要关注的事件,与 runloop 循环中处理的状态一致。
         typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
             kCFRunLoopEntry = (1UL << 0),
             kCFRunLoopBeforeTimers = (1UL << 1),
             kCFRunLoopBeforeSources = (1UL << 2),
             kCFRunLoopBeforeWaiting = (1UL << 5),
             kCFRunLoopAfterWaiting = (1UL << 6),
             kCFRunLoopExit = (1UL << 7),
             kCFRunLoopAllActivities = 0x0FFFFFFFU
         };
     repeats: CFRunLoopObserver 是否循环调用,不循环只会有一次回调。NO的情况下 CFRunLoopObserverRef 会自动移除。
     order: CFRunLoopObserver 的优先级 当在Runloop同一运行阶段中有多个 CFRunLoopObserver 正常情况下使用 0
     callout: 回调
     context: 上下文记录信息
     */
    observerRef = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, NO, 0, _runLoopObserverCallBack, &context);
    CFRunLoopAddObserver(runloop, observerRef, kCFRunLoopDefaultMode);
}

//typedef void (*CFRunLoopObserverCallBack)(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info);
void _runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
    NSLog(@"observerCallBack: %@,%lu,%@",observer,activity,info);
}

- (void)removeRunloopObserver {
    CFRunLoopRef runloop = CFRunLoopGetCurrent();
    if (observerRef && CFRunLoopContainsObserver(runloop, observerRef, kCFRunLoopDefaultMode)) {
        CFRunLoopRemoveObserver(runloop, observerRef, kCFRunLoopDefaultMode);
    }
}

- (void)dealloc {
    [self removeRunloopObserver];
}
  • CFRunLoopObserverContextCFRunLoopTimerContext结构相同。
  • CFRunLoopObserverCreate可以只监听某些状态。
    • repeats控制是否循环调用,不循环的情况下回调只会调用一次(所有状态假加起来),并且会自动从runloop中移除。

可以通过observer进行卡顿检测相关逻辑处理。

2.4 CFRunLoopSourceRef

2.4.1 source0

- (void)source0Test {
    /**
     typedef struct {
         CFIndex    version; //版本
         void *    info; //回调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); //hash code
         void    (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode); //准备代发回调
         void    (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode); //取消回调
         void    (*perform)(void *info); // 执行回调
     } CFRunLoopSourceContext;
     */
    CFRunLoopSourceContext context = {
        0,
        NULL,
        NULL,
        NULL,
        NULL,
        NULL,
        NULL,
        schedule,
        cancel,
        perform,
    };

    /**
     allocator: 传递NULL或 kCFAllocatorDefault 以使用当前默认分配器。
     order: 优先级索引,指示处理运行循环源的顺序。这里传0为了的就是自主回调
     context: 为运行循环源保存上下文信息的结构
     */
    //创建source
    CFRunLoopSourceRef source0 = CFRunLoopSourceCreate(CFAllocatorGetDefault(), 0, &context);
    CFRunLoopRef runloop = CFRunLoopGetCurrent();
    //source 指定了 runloop 与 mode 就进去就绪状态了。schedule 调用
    CFRunLoopAddSource(runloop, source0, kCFRunLoopDefaultMode);
    //一个执行信号 perform
    CFRunLoopSourceSignal(source0);
    //这里加延迟为了让 perform 有机会执行。
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        //唤醒 runloop 防止沉睡状态
        CFRunLoopWakeUp(runloop);
        //取消移除 source,执行 cancel 回调
        CFRunLoopRemoveSource(runloop, source0, kCFRunLoopDefaultMode);
    });
}

void schedule(void *info, CFRunLoopRef rl, CFRunLoopMode mode){
    NSLog(@"schedule 准备代发");
}

void perform(void *info){
    NSLog(@"source0 执行");
}

void cancel(void *info, CFRunLoopRef rl, CFRunLoopMode mode){
    NSLog(@"source0 cancel");
}

输出:

schedule 准备代发
source0 执行
source0 cancel
  • CFRunLoopSourceContext相比timerobserver多了判等以及哈希相关计算外。更重要的是schedule 、cancel 、perform 3个回调。
  • CFRunLoopSourceCreate相比就比较简单了,重要参数是context

2.4.2 source1 线程间通信

@interface ViewController ()<NSPortDelegate> 

@property (nonatomic, strong) NSPort* subThreadPort;
@property (nonatomic, strong) NSPort* mainThreadPort;

@end

- (void)source1Test {
    self.mainThreadPort = [NSPort port];
    self.mainThreadPort.delegate = self;
    // port - source1 -- runloop
    [[NSRunLoop currentRunLoop] addPort:self.mainThreadPort forMode:NSDefaultRunLoopMode];
    [self task];
}

- (void)task {
    __weak typeof(self) weakSelf = self;
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"initWithBlock:%@", [NSThread currentThread]); // 5
        weakSelf.subThreadPort = [NSPort port];
        weakSelf.subThreadPort.delegate = weakSelf;
        [[NSRunLoop currentRunLoop] addPort:weakSelf.subThreadPort forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
    }];
    [thread start];
}

#pragma mark --- NSPortDelegate ---
- (void)handlePortMessage:(id)message {//NSPortMessage 是macos中的类。
    NSLog(@"handlePortMessage: %@, thread:%@",message,[NSThread currentThread]);
    NSArray *componentsArray = [message valueForKey:@"components"];
    for (NSInteger i = 0; i < componentsArray.count; i++) {
        NSData *data = componentsArray[i];
        NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"dataStr: %@",dataStr);
    }
    
    //runtime 获取ivar。
//    unsigned int count = 0;
//    Ivar *ivars = class_copyIvarList([message class], &count);
//    for (int i = 0; i < count; i++) {
//        NSString *name = [NSString stringWithUTF8String:ivar_getName(ivars[i])];
//        NSLog(@"data: %@",name);
//    }
    
    sleep(1);
    if (![[NSThread currentThread] isMainThread]) {
        NSMutableArray *components = [NSMutableArray array];
        //必须转成data,否则会被忽略。
        NSData *data = [@"hotpot" dataUsingEncoding:NSUTF8StringEncoding];
        [components addObject:data];
        //从 subport 给 mainport 发送数据。
        NSLog(@"mainport send data to subport");
        [self.mainThreadPort sendBeforeDate:[NSDate date] components:components from:self.subThreadPort reserved:0];
    }
}


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSMutableArray* components = [NSMutableArray array];
    NSData* data = [@"cat" dataUsingEncoding:NSUTF8StringEncoding];
    [components addObject:data];
    //从 mainport 给 subThreadPort 发送数据。
    NSLog(@"subport send data to mainport");
    [self.subThreadPort sendBeforeDate:[NSDate date] components:components from:self.mainThreadPort reserved:0];
}

输出:


image.png
  • mainThreadPort是在主线程创建添加进runloop的,subThreadPort是在子线程创建添加进runloop的。
  • handlePortMessage :NSPortDelegate的代理方法。参数NSPortMessage是定义在macOS中的。iOS没有该类的定义。
  • 通过sendBeforeDate:components:from: reserved :进行线程间数据传递。从from port给调用方port线程发送数据。
  • 数据需要包装为NSData才有效。

NSPortMessage定义在macOS中:

image.png

三、runloop 结构

既然runloop是一个事件循环,那么它与普通的循环有什么区别呢?
普通循环:

image.png

runloop循环:

image.png

那么可以得到以下结论:

  • runloop能保持程序的持续运行。
  • 处理APP中的各种事件(触摸、定时器、performSelector)。
  • 节省cpu资源、提高程序的性能。有休眠和唤醒状态。

3.1 runloop 源码定位

那么runloop是怎么做到的呢?
通常我们会通过NSRunLoop去获取当前的runloop

[NSRunLoop currentRunLoop];

定义如下:

@property (class, readonly, strong) NSRunLoop *currentRunLoop;

currentRunLoop下符号断点:

image.png

可以看到NSRunLoop是对CFRunLoop的封装。

image.png

通过之前的分析已经定位到了runloop是在CoreFoundation中的 CoreFoundation源码。正好CoreFoundation开源了CFRunLoop

image.png

3.2 CFRunLoopRun

//CFRunLoopRun 是对 do...while 的封装
void CFRunLoopRun(void) {    /* DOES CALLOUT */
    int32_t result;
    do {
        //run
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
        //不是完成或者结束
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
  • runloop底层是对do...while的封装。
  • 当状态为完成或者结束后退出循环。

那么核心逻辑就在CFRunLoopRunSpecific中。还有一个疑问是runloop可以休眠,那么它是如何实现的呢?

3.3 runloop 数据结构

要了解runloop的实现原理,首先要清楚它的数据结构。

3.3.1 线程与runloop的关系

CFRunLoopRunSpecific的第一个参数是CFRunLoopGetCurrent()

CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    //通过 key-value 形式获取 CFRunLoopRef
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    //没有缓存通过线程获取
    return _CFRunLoopGet0(pthread_self());
}
  • 去缓存中获取CFRunLoopRef
  • 缓存中不存在通过线程去获取。

_CFRunLoopGet0

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    //没有传 pthread_t,则默认为主线程
    if (pthread_equal(t, kNilPthreadT)) {
        t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
    //创建可变字典
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        
    //通过 主线程 创建 mainLoop
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
        
    // 进行绑定,存储 thread(key) - runloop(value): dict[@"pthread_main_thread_np"] = mainLoop
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
        
        
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
        CFRelease(dict);
    }
    CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    //通过 thread 获取 runloop(非main runloop)
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    if (!loop) {//runloop不存在
        //创建runloop
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    if (!loop) {
        //存储 runloop
        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);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}
  • runloop与线程是一一对应的,每个runloop对应一个线程。线程并不一定有runloop,在有的情况下是一一对应的。
  • runloop底层是存储在可变字典中的,key为线程,valuerunloop
  • runloopCFRunLoopRef类型,通过__CFRunLoopCreate创建。
image.png

3.3.2 CFRunLoopRef

image.png

可以看到在创建CFRunLoopRef的时候有对应的modesitems_pthread。并且创建完成后调用__CFRunLoopFindMode去找mode

CFRunLoopRef的定义如下:

typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;

实际上底层它是__CFRunLoop类型:

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;            /* locked for accessing mode list */
    __CFPort _wakeUpPort;            // used for CFRunLoopWakeUp
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    pthread_t _pthread;//线程
    uint32_t _winthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;//集合类型 Items
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;//集合类型 Modes
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};
  • 一个runloop对应多个modes以及items

对于timer而言:

[[NSRunLoop currentRunLoop] addTimer:<#(nonnull NSTimer *)#> forMode:<#(nonnull NSRunLoopMode)#>];

显然它是要依赖mode的。

CFRunLoopMode

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;//set source0
    CFMutableSetRef _sources1;//set source1
    CFMutableArrayRef _observers;//array observe
    CFMutableArrayRef _timers;//array times
    CFMutableDictionaryRef _portToV1SourceMap;//dic port
    __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 */
};

而一个mode下又对应多个items(source0、source1、timers、observers),所以就有如下关系:

image.png

  • 1runloop对应1个线程。
  • 1runloop对应多个mode
  • 1mode对应多个sourcetimerobserver

3.3.3 RunLoop Modes

既然有多种mode,那么都有哪些呢?
源码中有如下定义:

CF_EXPORT const CFRunLoopMode kCFRunLoopDefaultMode;
CF_EXPORT const CFRunLoopMode kCFRunLoopCommonModes;

它们对应Foundation中的:

FOUNDATION_EXPORT NSRunLoopMode const NSDefaultRunLoopMode;
FOUNDATION_EXPORT NSRunLoopMode const NSRunLoopCommonModes ;

我们都清楚在页面滚动的时候有一个UITrackingRunLoopMode

UIKIT_EXTERN NSRunLoopMode const UITrackingRunLoopMode;

除了以上3mode还有两个私有mode

UIInitializationRunLoopMode
GSEventReceiveRunLoopMode

RunLoop运行在Mode1上时,是无法接受处理Mode2Mode3上的Source、Timer、Observer事件的。

  • kCFRunLoopDefaultMode/NSDefaultRunLoopMode:默认模式,主线程是在这个运行模式下运行。
  • kCFRunLoopCommonModes/NSRunLoopCommonModes:伪模式,不是一种真正的运行模式,是同步Source/Timer/Observer到多个Mode中。
  • UITrackingRunLoopMode:跟踪用户交互事件(用于 ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响)。对于macOS对应NSEventTrackingRunLoopMode
  • UIInitializationRunLoopMode:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用。
  • GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到。

四、runloop 事务处理

4.1 timer 事务处理

timer为例,将timer加入到runloop中:

NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"timerWithTimeInterval block -- %@",[[NSRunLoop currentRunLoop] currentMode]);
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

底层调用了CFRunLoopAddTimer

image.png

4.1.1 CFRunLoopAddTimer

//runloop  timer  mode
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName) {
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return;
    if (!__CFIsValid(rlt) || (NULL != rlt->_runLoop && rlt->_runLoop != rl)) return;
    __CFRunLoopLock(rl);
    
    //kCFRunLoopCommonModes 集合mode
    if (modeName == kCFRunLoopCommonModes) {
        CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
        if (NULL == rl->_commonModeItems) {
            rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
        }
        //将 timer 加入 modeItems 中。
        CFSetAddValue(rl->_commonModeItems, rlt);
        if (NULL != set) {
            CFTypeRef context[2] = {rl, rlt};
            /* add new item to all common-modes */
            //加入所有的command modes 中。
            CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
            CFRelease(set);
        }
    } else {//非common mode
        //从 runloop 中找到对应的 mode
        CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
        if (NULL != rlm) {//mode存在
            if (NULL == rlm->_timers) {//不存在timer
                CFArrayCallBacks cb = kCFTypeArrayCallBacks;
                cb.equal = NULL;
                //创建timer
                rlm->_timers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &cb);
            }
        }
        if (NULL != rlm && !CFSetContainsValue(rlt->_rlModes, rlm->_name)) {
            __CFRunLoopTimerLock(rlt);
            //timer没有runloop
            if (NULL == rlt->_runLoop) {
                //设置runloop
                rlt->_runLoop = rl;
            } else if (rl != rlt->_runLoop) {//timer所有的runloop与当前runloop不同直接返回。
                __CFRunLoopTimerUnlock(rlt);
                __CFRunLoopModeUnlock(rlm);
                __CFRunLoopUnlock(rl);
                return;
            }
            //将timer 加入 mode中。
            CFSetAddValue(rlt->_rlModes, rlm->_name);
            __CFRunLoopTimerUnlock(rlt);
            __CFRunLoopTimerFireTSRLock();
            __CFRepositionTimerInMode(rlm, rlt, false);
            __CFRunLoopTimerFireTSRUnlock();
            if (!_CFExecutableLinkedOnOrAfter(CFSystemVersionLion)) {
                // Normally we don't do this on behalf of clients, but for
                // backwards compatibility due to the change in timer handling...
                //唤醒runloop
                if (rl != CFRunLoopGetCurrent()) CFRunLoopWakeUp(rl);
            }
        }
        if (NULL != rlm) {
            __CFRunLoopModeUnlock(rlm);
        }
    }
   
    __CFRunLoopUnlock(rl);
}

根据要加入的mode区分是common mode和非common modetimer加入mode中。这个时候只是将timer加入了mode中,要执行肯定要调用CFRunLoopRun,最终要调用CFRunLoopRunSpecific

4.1.2 CFRunLoopRunSpecific

CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);

//对应声明
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled)
  • CFRunLoopGetCurrent()创建runloop
  • mode默认给的是kCFRunLoopDefaultMode
  • 1.0e10(1 * 1010)表示超时时间。
  • returnAfterSourceHandled表示source处理后是否返回。
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);
    //如果没找到 || mode中没有注册任何事件,则就此停止,不进入循环
    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);
    //取上一次运行的 mode
    CFRunLoopModeRef previousMode = rl->_currentMode;
    //如果本次mode和上次的mode一致
    rl->_currentMode = currentMode;
    //初始化一个result为kCFRunLoopRunFinished
    int32_t result = kCFRunLoopRunFinished;
    
    if (currentMode->_observerMask & kCFRunLoopEntry )
        // 1. 通知 Observers: RunLoop 即将进入 loop。
        __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
        //run
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    if (currentMode->_observerMask & kCFRunLoopExit )
        // 10. 通知 Observers: RunLoop 即将退出。
        __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    __CFRunLoopModeUnlock(currentMode);
    __CFRunLoopPopPerRunData(rl, previousPerRun);
    rl->_currentMode = previousMode;
    __CFRunLoopUnlock(rl);
    return result;
}
  • 根据modeName找到本次运行的mode
  • 1.通知 Observers RunLoop即将进入 loop
  • 调用__CFRunLoopRun运行循环。
  • 10.通知 Observers RunLoop 即将退出。

4.1.3 __CFRunLoopRun

__CFRunLoopRun中调用了__CFRunLoopDoTimers

// rl and rlm are locked on entry and exit
static Boolean __CFRunLoopDoTimers(CFRunLoopRef rl, CFRunLoopModeRef rlm, uint64_t limitTSR) {    /* DOES CALLOUT */
    Boolean timerHandled = false;
    CFMutableArrayRef timers = NULL;
    //从mode中获取timers进行处理
    for (CFIndex idx = 0, cnt = rlm->_timers ? CFArrayGetCount(rlm->_timers) : 0; idx < cnt; idx++) {
        CFRunLoopTimerRef rlt = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(rlm->_timers, idx);
        
        if (__CFIsValid(rlt) && !__CFRunLoopTimerIsFiring(rlt)) {
            if (rlt->_fireTSR <= limitTSR) {
                if (!timers) timers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeArrayCallBacks);
                CFArrayAppendValue(timers, rlt);
            }
        }
    }
    
    for (CFIndex idx = 0, cnt = timers ? CFArrayGetCount(timers) : 0; idx < cnt; idx++) {
        CFRunLoopTimerRef rlt = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(timers, idx);
        //执行timer
        Boolean did = __CFRunLoopDoTimer(rl, rlm, rlt);
        timerHandled = timerHandled || did;
    }
    if (timers) CFRelease(timers);
    return timerHandled;
}

找到mode中的所有timer然后调用__CFRunLoopDoTimer

4.1.4 __CFRunLoopDoTimer

image.png

__CFRunLoopDoTimer中进行了时间的判断以及timer回调的调用并且重新计算了_fireTSR。这样整个调用流程就与回调堆栈吻合了。

CFRunLoopAddTimer -> CFRunLoopRunSpecific -> __CFRunLoopRun -> __CFRunLoopDoTimers -> __CFRunLoopDoTimer -> __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__

4.2 source 事务处理

timer相同source会调用CFRunLoopAddSource

image.png

source加入mode中。同样调用是在__CFRunLoopRun中。

4.2.1 __CFRunLoopDoSources0

image.png

__CFRunLoopDoSources0中调用了__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__

4.2.2 __CFRunLoopDoSources1

image.png

__CFRunLoopDoSources1最终调用了__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__

CFRunLoopAddSource -> CFRunLoopRunSpecific -> __CFRunLoopRun -> __CFRunLoopDoSources0/__CFRunLoopDoSources1 -> __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ /__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__

4.3 observer 事务处理

同理observer会调用CFRunLoopAddObserver

4.3.1 CFRunLoopAddObserver

image.png

observer加入_observers或者_commonModeItems中。同样调用是在__CFRunLoopRun中。

4.3.2 __CFRunLoopDoObservers

image.png

__CFRunLoopDoObservers最终调用__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__传递了状态参数。

CFRunLoopAddObserver -> CFRunLoopRunSpecific -> __CFRunLoopRun -> __CFRunLoopDoObservers(状态参数) -> __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(状态参数)

4.4 回调函数

4.4.1 CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION

static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(CFRunLoopTimerCallBack func, CFRunLoopTimerRef timer, void *info) {
    if (func) {
        func(timer, info);
    }
    asm __volatile__(""); // thwart tail-call optimization
}

timer直接调用回调函数,传递timer参数。

4.4.2 CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION

static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(void (*perform)(void *), void *info) {
    if (perform) {
        perform(info);
    }
    asm __volatile__(""); // thwart tail-call optimization
}

source0直接调用perform

4.4.3 CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION

static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        void *(*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info),
        mach_msg_header_t *msg, CFIndex size, mach_msg_header_t **reply,
#else
        void (*perform)(void *),
#endif
        void *info) {
    if (perform) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        *reply = perform(msg, size, kCFAllocatorSystemDefault, info);
#else
        perform(info);
#endif
    }
    asm __volatile__(""); // thwart tail-call optimization
}

source1不同架构处理不同,iOS上与source0处理相同。

4.4.4 CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(CFRunLoopObserverCallBack func, CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    if (func) {
        func(observer, activity, info);
    }
    asm __volatile__(""); // thwart tail-call optimization
}

observer调用回调函数其中传递了activity状态参数。

4.4.5 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE

static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() __attribute__((noinline));
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(void *msg) {
    _dispatch_main_queue_callback_4CF(msg);
    asm __volatile__(""); // thwart tail-call optimization
}

直接调用_dispatch_main_queue_callback_4CF

4.5.6 source1 与 source0

点击触摸事件会先调用source1

image.png

然后交给source0处理:

image.png

之后才会进入触摸回调。

系统先通过source1唤醒runloop然后交给source0处理事件。

五、runloop 原理

CFRunLoopRun的过程中do...while的条件是根据返回的状态判断的:

enum {
    kCFRunLoopRunFinished = 1,//完成
    kCFRunLoopRunStopped = 2,//结束
    kCFRunLoopRunTimedOut = 3,//超时
    kCFRunLoopRunHandledSource = 4//处理完source
};

CFRunLoopRunSpecific的过程中也有状态的切换:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),//启动
    kCFRunLoopBeforeTimers = (1UL << 1),//将要处理 timer 事件
    kCFRunLoopBeforeSources = (1UL << 2),//将要处理 Source 事件
    kCFRunLoopBeforeWaiting = (1UL << 5),//将要进入休眠状态,即将由用户态切换到内核态
    kCFRunLoopAfterWaiting = (1UL << 6),//被唤醒,即从内核态切换到用户态后
    kCFRunLoopExit = (1UL << 7),//退出
    kCFRunLoopAllActivities = 0x0FFFFFFFU //监听所有状态
};

可以通过CFRunLoopActivity监听整个runloop的生命周期。

runloop的整个核心逻辑就在__CFRunLoopRun中:

image.png

整个流程伪代码如下:

//获取 mode 处理
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {  
    /// 首先根据modeName找到对应mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    
    /// 1.通知 Observers: RunLoop 即将进入 loop。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    
    /// 内部函数,进入loop
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    
    /// 10.通知 Observers: RunLoop 即将退出。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    return result;
}

//核心函数
/**
 *  运行run loop
 *
 *  @param rl              运行的RunLoop对象
 *  @param rlm             运行的mode
 *  @param seconds         run loop超时时间
 *  @param stopAfterHandle true:run loop处理完事件就退出  false:一直运行直到超时或者被手动终止
 *  @param previousMode    上一次运行的mode
 *
 *  @return 返回4种状态
 */

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    
    //一些超时逻辑相关的处理

    int32_t retVal = 0;
    
    do {  //itmes do
        
        /// 2. 通知 Observers: 即将处理timer事件
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        
        /// 3.通知 Observers: 即将处理Source0(非port)事件
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources)
        
        /// 执行被加入的 block
        __CFRunLoopDoBlocks(rl, rlm);
        
        /// 4.处理sources0 (非port)事件
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        
        /// 处理sources0返回为YES
        if (sourceHandledThisLoop) {
            /// 执行被加入的block
            __CFRunLoopDoBlocks(rl, rlm);
        }
        
        
        /// 5.如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息(9)。
        if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
            /// 处理消息
            goto handle_msg;
        }
        
        /// 6.通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        // 设置RunLoop为休眠状态。
        __CFRunLoopSetSleeping(rl);
        
        // 内循环,用于接收等待端口的消息
        // 进入此循环后,线程进入休眠,直到收到新消息才跳出该循环,继续执行run loop
        do {
            /// 7.等待被唤醒, 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
                /// 7.1 一个基于 port 的 Source1 的事件。
                /// 7.2 一个 Timer 到时间了
                /// 7.3 RunLoop 自身的超时时间到了
                /// 7.4 被其他什么调用者手动唤醒

            // mach 事务 - 指令
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);

        } while(1)
          
        
        // 取消runloop的休眠状态,也就是唤醒。
        __CFRunLoopUnsetSleeping(rl);
        
        /// 8.通知 Observers: RunLoop 的线程刚刚被唤醒了。
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
        
        /// 9.处理唤醒时收到的消息,之后跳转 步骤2
    handle_msg:
            //9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
        if (被Timer唤醒) {
            //处理Timers
            __CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
        } else if (被GCD唤醒) {
            //9.2 如果有dispatch到main_queue的block,执行block
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } else if (被Source1唤醒) {
            //9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件。(被source1唤醒)
            __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
        }
        
        /// 执行加入到Loop的block
        __CFRunLoopDoBlocks(rl, rlm);
        
        
        if (sourceHandledThisLoop && stopAfterHandle) {
            //进入loop时参数说处理完事件就返回。
            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;
        }
        
    } while (0 == retVal);
    
    return retVal;
}

根据源码有以下流程图:


runloop循环流程

同时官方文档中也给出了流程总结:


image.png

5.1 runloop 休眠与唤醒

在第7步前后分别调用了__CFRunLoopSetSleeping__CFRunLoopUnsetSleeping进行休眠与唤醒操作:

image.png

这里是通过Darwin中的Mach来进行内核态和用户态的切换:

image.png

通过mach_msg()函数接收、发送消息,本质上是调用mach_msg_trap()。在用户态调用 mach_msg_trap()时会切换到内核态,内核态中内核实现的mach_msg()函数会完成实际的工作。

比如触摸屏幕摸到硬件(屏幕)将事件先包装成Event告诉source1(port)source1唤醒RunLoop然后将事件Event分发给source0source0来处理。

5.2 mode 切换

默认情况下runloop运行后是在kCFRunLoopDefaultMode模式下的,那么runloop是如何切换mode的呢?
既然要切换mode那么肯定要改变CFRunLoopRunSpecific的参数,搜索CFRunLoopRunSpecific后有在CFRunLoopRunInMode中有调用:

SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

CFRunLoopRunInMode并没有找到调用的时机,给CFRunLoopRunInMode下符号断点:

image.png

image.png

可以看到是在-[NSRunLoop(NSRunLoop) runMode:beforeDate:]中以及GSEventRunModal中进行切换的。

六、runloop 应用场景

6.1 runloop 与 mode

6.1.1 mode 与 timer

常用的一个场景是在TableView中的定时器在滚动的时候回调是不执行的。因为这个时候runloopmodeNSDefaultRunLoopMode切换到了UITrackingRunLoopMode。而timer默认情况下是加在NSDefaultRunLoopMode下的。
这个时候就需要将timer同时加到NSDefaultRunLoopModeUITrackingRunLoopMode下:

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

加入NSRunLoopCommonModes下就可以了。当然也可以使用GCDtimer实现计时器。

6.1.2 mode 与 页面刷新

怎样保证子线程数据回来更新UI的时候不打断用户的滑动操作?
不打断用户操作那么当runloopNSDefaultRunLoopMode模式的时候页面就不在滑动状态。那么就当主线程RunLoopUITrackingRunLoopMode切换到NSDefaultRunLoopMode时再去更新UI

[self performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];

6.2 runloop 与 线程

RunLoop与线程时是一一对应的,数据以key(线程)-value(runloop)存储在全局的字典中。默认情况下线程时不开启runloop的(主线程除外)。

6.2.1 线程 与 timer

有如下案例(在主线程调用):

- (void)testRunloop {
    NSLog(@"1");
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       NSLog(@"2");
       [self performSelector:@selector(performSelectorAction) withObject:nil afterDelay:10];
       NSLog(@"3");
    });
    NSLog(@"4");
}

- (void)performSelectorAction {
   NSLog(@"5");
}

输出:

1
4
2
3

由于performSelector带了延迟函数(即使延迟时间为0),内部创建了timer,而子线程没有开启runloop添加timer会失败,该方法也就失效了。

当然如果是主队列则没有问题,同步函数则结果依赖调用同步函数的线程。

修改代码如下:

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"2");
    [[NSRunLoop currentRunLoop] run];
    [self performSelector:@selector(performSelectorAction) withObject:nil afterDelay:10];
    NSLog(@"3");
});

这个时候performSelectorAction仍然不执行,runrunloop中没有事务导致runloop退出了。退出后再添加timer显然不会执行。
那么将run的逻辑在添加timer后就可以了:

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"2");
    [self performSelector:@selector(performSelectorAction) withObject:nil afterDelay:10];
    [[NSRunLoop currentRunLoop] run];
    NSLog(@"3");
});

这个时候输出:

1
4
2
5
3

这个时候即使delay0也没问题,进一步说明了NStimer是不准的。

那么有个疑问,既然performSelector: withObject : afterDelay:底层是对timer的封装,那么肯定会调用CFRunLoopAddTimer。而runloop不存在的情况下这个函数会直接返回:

image.png

按照理解应该先创建runloop然后添加timer再启动:

[NSRunLoop currentRunLoop];
[self performSelector:@selector(performSelectorAction) withObject:nil afterDelay:0];
[[NSRunLoop currentRunLoop] run];

为什么先添加timer也没问题呢?

image.png

  • performSelector: withObject : afterDelay:内部会先创建runloop然后添加timer,我们只需要在添加timer后启动runloop
  • 自己启动RunLoop,一定要在添加item后。

6.2.2 线程常驻

线程保活在实际开发中经常会遇到一些耗时且需要频繁处理的工作,这些工作和UI无关,比如大文件的下载、后台进行数据的上报等。线程常驻的好处是不用频繁的开辟销毁线程节省资源。

6.2.2.1 线程释放验证

创建一个HPThread继承自NSThread,只重写dealloc方便验证线程是否销毁:

- (void)dealloc {
    NSLog(@"%s",__func__);
}

调用如下:

HPThread *thread = [[HPThread alloc] initWithTarget:self selector:@selector(threadAction) object:nil];
[thread start];

- (void)threadAction {
    @autoreleasepool {
        for (int i = 0; i < 100; i++) {
            NSLog(@"子线程任务 􏱉􏲠􏲡􏲬􏲭􏱉􏲠􏲡􏲬􏲭􏱉􏲠􏲡􏲬􏲭%d - %@",i,[NSThread currentThread]);
        }
        NSLog(@"􏱉􏲠􏲡􏲬􏲭􏲖􏲮子线程任务结束- %@",[NSThread currentThread]);
    }
}

这个时候在threadAction中任务执行完毕后HPThread就释放了:

image.png

这个时候如果HPThread改为属性被持有:

@property (nonatomic, strong) HPThread *thread;

HPThreaddealloc就不会执行了,那么这个时候线程释放了么?
创建一个新任务在self. thread中执行:

UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction:)];
[self.view addGestureRecognizer:tap];

- (void)tapAction:(UITapGestureRecognizer *)tap {
    //waitUntilDone:YES 􏳋􏳌􏱉􏲠􏲡􏲬􏲭􏲆􏲇􏲷􏳍􏲆􏲇􏰷􏰸执行完 otherChildThreadAction 才执行后续的逻辑,为 NO 就一起直接执行了。
    [self performSelector:@selector(otherChildThreadAction) onThread:self.thread withObject:nil waitUntilDone:NO];
    NSLog(@"单点事件执行完毕");
}

- (void)otherChildThreadAction {
    @autoreleasepool {
        for (int i = 0; i < 10; i++) {
            NSLog(@"otherChildThreadAction 子线程任务 􏱉􏲠􏲡􏲬􏲭􏱉􏲠􏲡􏲬􏲭􏱉􏲠􏲡􏲬􏲭%d - %@",i,[NSThread currentThread]);
        }
        NSLog(@"otherChildThreadAction 􏱉􏲠􏲡􏲬􏲭􏲖􏲮子线程任务结束- %@",[NSThread currentThread]);
    }
}

这个时候otherChildThreadAction并没有执行:

image.png

说明HPThread创建的线程已经释放了。那么说明仅仅持有HPThread并不能保证线程存活。

6.2.2.2 线程保活

image.png

显然pthread_create创建的线程在任务执行完毕后就被释放了,要确保线程执行完后不被释放,那么就要持有它。那么就有两种方式:

  • 1.使用pthread代替HPThread实现线程创建逻辑。
  • 2.使用runloop持有thread。为了让thread不释放,runloop要一直有事务。

使用 pthread 比较麻烦更好的方案是使用runloop的方案。
修改threadAction如下:

- (void)threadAction {
    @autoreleasepool {
        for (int i = 0; i < 100; i++) {
            NSLog(@"子线程任务 􏱉􏲠􏲡􏲬􏲭􏱉􏲠􏲡􏲬􏲭􏱉􏲠􏲡􏲬􏲭%d - %@",i,[NSThread currentThread]);
        }
        // 􏱋􏲯􏲰􏲱􏰷􏲲􏲳􏲠􏲡􏲬􏲭􏲴􏲵􏲶􏲬􏲭􏲆􏲇􏲷􏲸􏲴􏲵􏲠􏲡􏰠􏱖􏲹􏲺􏲻􏲵􏰽􏰾􏲼􏲽􏲠􏲡􏰘􏲾􏲿􏱃􏳀􏲵􏳁􏰙􏰎􏲠􏲡􏳂􏱝􏱋􏳃RunLoop 任务执行完毕后线程就销毁了,线程保活需要加入 runloop
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        //为了 runloop 不退出
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
        //end 不会执行,因为跑do...while 循环了。
        NSLog(@"􏱉􏲠􏲡􏲬􏲭􏲖􏲮子线程任务结束- %@",[NSThread currentThread]);
    }
}

这样就保证了线程不被释放:


image.png

但是这个时候又存在一个问题了。self -> thread -> self造成了循环引用。需要在结束任务的时候退出线程打破循环引用:

@property (nonatomic, assign) BOOL stopped;

UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTapAction:)];
doubleTap.numberOfTapsRequired = 2;
[self.view addGestureRecognizer:doubleTap];

- (void)doubleTapAction:(UITapGestureRecognizer *)tap {
    [self performSelector:@selector(exitThread) onThread:self.thread withObject:nil waitUntilDone:NO];
    NSLog(@"双击事件执行完毕");
}

- (void)exitThread {
    self.stopped = YES;
    //􏳢􏳣停止RunLoop,这样只会停止当次的。
    CFRunLoopStop(CFRunLoopGetCurrent());
    [self.thread cancel];
    //􏳒􏳓􏳔􏳕􏳖􏲩􏰚􏰑打破循环引用
    self.thread = nil;
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

thread也可以使用- (instancetype)initWithBlock:(void (^)(void))block来创建。虽然没有循环引用了,vc也能释放,但是runloop持有了thread导致线程和runloop不能释放。

这个时候双击然后返回页面仍然不能释放VC,由于runloop是通过run开启的(runUntilDate)也一样,run一旦成功会不停的调用runMode:beforeDate:来运行runloop,而于CFRunLoopStop只停止了一次runlooprunloop仍然持有了线程)。修改threadAction如下:

- (void)threadAction {
    @autoreleasepool {
        for (int i = 0; i < 100; i++) {
            NSLog(@"子线程任务 􏱉􏲠􏲡􏲬􏲭􏱉􏲠􏲡􏲬􏲭􏱉􏲠􏲡􏲬􏲭%d - %@",i,[NSThread currentThread]);
        }
        // 􏱋􏲯􏲰􏲱􏰷􏲲􏲳􏲠􏲡􏲬􏲭􏲴􏲵􏲶􏲬􏲭􏲆􏲇􏲷􏲸􏲴􏲵􏲠􏲡􏰠􏱖􏲹􏲺􏲻􏲵􏰽􏰾􏲼􏲽􏲠􏲡􏰘􏲾􏲿􏱃􏳀􏲵􏳁􏰙􏰎􏲠􏲡􏳂􏱝􏱋􏳃RunLoop 任务执行完毕后线程就销毁了,线程保活需要加入 runloop
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        //为了 runloop 不退出
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        while (!self.stopped) {
            // 这个方法在没有任务时就睡眠  任务完成了就会退出loop
            [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        //不会执行,因为跑do...while 循环了。 self.stopped
        NSLog(@"􏱉􏲠􏲡􏲬􏲭􏲖􏲮子线程任务结束- %@",[NSThread currentThread]);
    }
}

这个时候通过self.stopped变量控制是否继续run就解决问题了。

runMode: beforeDate:只控制执行一次:

image.png

但是当我们将runMode: beforeDate:mode修改为NSRunLoopCommonModes后:

image.png

thread添加任务也不执行,并且cup占满。

等价于UITrackingRunLoopMode

image.png

所以在while循环中不能一直runUITrackingRunLoopMode模式。

当然也可以使用CFRunLoop相关函数实现:

// 创建上下文
CFRunLoopSourceContext context = {0};
// 创建source
CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
// Runloop中添加source
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
// 销毁source
CFRelease(source);
// 启动
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);

那么CFRunLoop尝试使用common modes

image.png

直接报错CFRunLoopRunSpecific模式只能传递特定模式。这也就是runMode: beforeDate:启动后给thread添加任务也不执行的原因。

  • CFRunLoopRunInModerunMode: beforeDate:只能运行在特定的模式下。
  • CFRunLoopStop只能退出单次的runloop

总结:

  • runloop是一个事件循环,分为内核态和用户态。底层是对do...while的封装。与do...while的区别是它有休眠和唤醒逻辑,从而节省cpu资源、提高程序的性能。
  • NSRunloop是对CFRunloop的封装。
  • runloop与线程一一对应,开启runloop依赖于线程,线程不一定开启runloop(主线程默认开启,子线程需要手动开启)。底层是存储在可变字典中key为线程,valuerunloop
  • runloopmode是一对多的关系。
    • kCFRunLoopDefaultMode/NSDefaultRunLoopMode:默认模式。
    • UITrackingRunLoopMode:跟踪用户交互事件。
    • kCFRunLoopCommonModes/NSRunLoopCommonModes:伪模式,不是一种真正的运行模式,本质上是同步Source/Timer/Observer到多个Mode中。
  • modesourcetimerobserver也是一对多的关系。
    • CFRunLoopSource分为source0source1
      • source0:基于非port也就是用户触发的事件,需要手动唤醒RunLoop,将当前线程从内核态切换到用户态。
      • source1:基于port,包含一个 mach_port 和一个回调。可监听系统端口和通过内核和其他线程发送的消息,能主动唤醒RunLoop,接收分发系统事件。具备唤醒线程的能力。
    • CFRunLoopTimer基于时间的触发器,在预设的时间点唤醒RunLoop执行回调。基于runloop所以不是实时的(RunLoop只负责分发消息,如果线程当前正在处理繁重的任务,有可能导致Timer本次延时或者少执行一次)。
    • CFRunLoopObserver可以监听runloop的状态。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容