iOS-浅谈OC中的Runloop

2019-06-19  本文已影响0人  晴天ccc

目录

  • RunLoop基础知识
    ---- 什么是RunLoop
    ---- RunLoop的作用
    ---- 什么时候使用Runloop
    ---- iOS中的RunLoop的参与
    ---- 获取RunLoop对象
    ---- RunLoop和线程的关系
  • RunLoop的底层结构
    ---- CFRunLoopRef
    ---- CFRunLoopModeRef
    ---- CFRunLoopSourceRef
    ------------ source0
    ------------ source1
    ---- CFRunLoopObserverRef
    ------------ observer
    ---- CFRunLoopTimerRef
    ------------ timer
  • 补充
    ---- currentMode
    ---- commonModes
    ---- RunLoop的五种运行模式
    ---- 各个Model模式间的监听和切换

RunLoop基础知识

  • 顾名思义,就是运行循环,它会在程序运行过程中循环做一些事情。
  • 通常情况下,单条线程一次只能执行一个任务,执行完成线程就会退出。如果我们希望有一个机制,让线程可以随时随地处理事件且不退出,这种模型通常称作 Event Loop
  • Event Loop在很多系统框架里面都有实现,比如Node.js的事件处理,Windows程序的消息循环,再比如iOS/OSX的RunLoop
  • 一个RunLoop就是一个事件处理的循环,用来不停的调度工作及处理输入事件。
  • 让线程在有工作的时候保持忙碌,在没有工作的时候进入休眠,这样做的好处就是让线程进入休眠之后避免资源占用。

iOS系统中有2套API来访问和使用RunLoop:

  • CFRunLoopRef:是 CoreFoundation 框架内的,是一套纯 C 的 API。开源的【 Runloop源码】 。
  • NSRunLoop:基于 CFRunloopRef 封装而成。提供了面向对象的 API。
  • NSRunLoopCFRunLoopRef都代表着RunLoop对象。
  • 保持程序的持续运行。
  • 处理App中的各种事件(比如触摸事件、定时器事件等)。
  • 节省CPU资源,提高程序性能:该做事时做事,该休息时休息。
  • ......
  • 当需要和该线程进行交互的时候才会使用Runloop

新建一个iOS项目,我们打开main函数,我们注释掉其它代码,改成如下:

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Hello, World!");
    }
    return 0;
}

结果打印:

Hello, World!
Program ended with exit code: 0
  • 开始运行程序,我们APP启动之后便会闪屏退出。
  • 控制台打印结果可以看出,执行完输出,Program ended with exit code: 0,应用会自动闪屏退出。

如果打开将代码恢复到原样:

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

程序会一直处于运行中,随时接收用户的点击和交互事件,除非我们主动关闭该App应用。

总结:

  • 没有RunLoop的参与,执行完NSLog(@"Hello, World!");代码后,会即将退出程序。
  • 在OC代码中,Runloop是由系统默认开启的。
  • 在main函数中的UIApplicationMain方法中自动创建一个RunLoop并开启,

我们模拟一下Runloop的【伪代码】如下:

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        int retVal = 0;
        do {
            // 睡眠中等待
            int message = sleep_and_wait();
            // 处理消息
            retVal = process_message(message);
        }while (0 == retVal);
        return 0;
    }
}

获取当前runLoop

NSRunLoop * runLoop1 = [NSRunLoop currentRunLoop];

获取主线程runLoop

NSRunLoop * runLoop2 = [NSRunLoop mainRunLoop];

获取当前runLoop

CFRunLoopRef runLoop3 = CFRunLoopGetCurrent();

获取主线程runLoop

CFRunLoopRef runLoop4 = CFRunLoopGetMain();

分别打印NSRunLoopCFRunLoopRef
发现地址并不相同,证明了NSRunLoop并不是等价于CFRunLoopRef,而是对CFRunLoopRef进行了封装。

NSLog(@"%p %p",[NSRunLoop currentRunLoop], CFRunLoopGetCurrent());

查看【 Runloop源码】,我下载的是CF-1153.18.tar.gz。在CFRunLoop.c中,

CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
......
    // 读取
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    if (!loop) {// 没有的话就创建一个
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
            __CFLock(&loopsLock);
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        if (!loop) {
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            loop = newLoop;
        }
    }
......
    return loop;
}
  • 通过查看源码可知当,通过CFRunLoopGetCurrent读取当前RunLoop时,如果没有就创建一个新的RunLoop,并保存在__CFRunLoops全局字典中。
  • __CFRunLoops字典是以线程作为keyRunLoop作为value
  • 每条线程都有唯一的一个与之对应的RunLoop对象。
  • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
  • 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它的时候创建。
  • 主线程的RunLoop在main函数的UIApplicationMain方法中已经自动获取(创建)。
  • 子线程默认没有开启RunLoop,除非在子线程主动调用[NSRunLoop currentRunLoop]函数。
  • RunLoop会在线程结束时销毁。

RunLoop的底层结构

这5个类之间的关系可以用下图来表示:

查看【 Runloop源码】,我下载的是CF-1153.18.tar.gz。在CFRunLoop.c中,RunLoop底层结构如下:

typedef struct __CFRunLoop * CFRunLoopRef;

struct __CFRunLoop {
    // CoreFoundation 中的 runtime 基础信息
    CFRuntimeBase _base;
    // 针对获取 mode 列表操作的锁
    pthread_mutex_t _lock;            /* locked for accessing mode list */
    // 唤醒端口
    __CFPort _wakeUpPort;            // used for CFRunLoopWakeUp
    // 是否使用过
    Boolean _unused;
    // runloop 运行会重置的一个数据结构
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    // runloop 所对应线程
    pthread_t _pthread;
    uint32_t _winthread;
    // 存放 common mode 的集合
    CFMutableSetRef _commonModes;
    // 存放 common mode item 的集合
    CFMutableSetRef _commonModeItems;
    // runloop 当前所在 mode
    CFRunLoopModeRef _currentMode;
    // 存放 mode 的集合
    CFMutableSetRef _modes;
    
    // runloop 内部 block 链表表头指针
    struct _block_item *_blocks_head;
    // runloop 内部 block 链表表尾指针
    struct _block_item *_blocks_tail;
    // 运行时间点
    CFAbsoluteTime _runTime;
    // 休眠时间点
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

通过上述,我们可以知道:

  • RunLoop也是一个结构体对象
  • _modes:RunLoop可以有多个mode对象。
  • _currentMode:Runloop在同一时间只能且必须在某一种特定的Mode下面Run。
  • 更换Mode时,必须要停止当前的Loop,然后重启新的Loop。
  • 重启的意思是退出当前的while循环,然后重新设置一个新的while,两者互不影响。
  • 切换Mode不会导致程序退出。

Runloop 中可以包含若干个 Mode,每个 Mode 又包含若干的 Source/Timer/Observer

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode {
    // CoreFoundation 中的 runtime 基础信息
    CFRuntimeBase _base;
    // 互斥锁,加锁前需要 runloop 先加锁
    pthread_mutex_t _lock;    /* must have the run loop locked before locking this */
    // mode 的名称
    CFStringRef _name;
    // mode 是否停止
    Boolean _stopped;
    char _padding[3];
    // source0
    CFMutableSetRef _sources0;
    // source1
    CFMutableSetRef _sources1;
    // observers
    CFMutableArrayRef _observers;
    // timers
    CFMutableArrayRef _timers;
    CFMutableDictionaryRef _portToV1SourceMap;
    // port 的集合
    __CFPortSet _portSet;
    // observer 的 mask
    CFIndex _observerMask;
    // 如果定义了 GCD 定时器
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    // GCD 定时器
    dispatch_source_t _timerSource;
    // 队列
    dispatch_queue_t _queue;
    // 当 GCD 定时器触发时设置为 true
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
    // 如果使用 MK_TIMER
#if USE_MK_TIMER_TOO
    // MK_TIMER 的 port
    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代表RunLoop的运行模式。
  • CFRunLoopModeRef可以视为事件的管家,一个Mode管理着各种事件。
  • CFRunLoopModeRef核心内容是4个数组容器,分别用来装Source0、Source1、Observer、Timer
  • 一个RunLoop包含若干个Mode,每个Mode又包含了若干个Source0、Source1、Observer、Timer
  • _sources0_sources1里保存着CFRunLoopSourceRef对象。
  • _observers保存着CFRunLoopObserverRef对象。
  • _timers保存着CFRunLoopTimerRef对象。
  • RunLoop启动时只能选择一个Mode,作为currentMode
  • 如果一个Mode中一个 Source0、Source1、Observer、Timer 都没有,那么 Runloop会直接退出,不进入事件循环。
  • 如果切换Mode,只能退出当前Loop,再重新选择一个Mode进入。这样可以使不同组的Source0、Source1、Timer、Observer能分隔开来,互不影响。
  • 切换Mode不会导致程序退出。
typedef struct __CFRunLoopSource * CFRunLoopSourceRef;

struct __CFRunLoopSource {
    // CoreFoundation 中的 runtime 基础信息
    CFRuntimeBase _base;
    uint32_t _bits;
    // 互斥锁
    pthread_mutex_t _lock;
    // source 的优先级,值为小,优先级越高
    CFIndex _order;            /* immutable */
    // runloop 集合
    CFMutableBagRef _runLoops;
    // 一个联合体,说明 source 要么为 source0,要么为 source1
    union {
        CFRunLoopSourceContext version0;    /* immutable, except invalidation */
        CFRunLoopSourceContext1 version1;    /* immutable, except invalidation */
    } _context;
};

CFRunLoopSourceRef是事件源(输入源)。它是事件产生的地方,输入源是将事件异步传递到线程中。事件的源则取决于输入源的类型。

输入源一般分为两类:

  • source0:自定义输入源,负责触摸事件处理。
  • source1:基于Port(端口)的输入源。,负责系统事件捕捉和Port(端口)的线程间通信。
  • Source1包含了一个mach_port和一个回调指针,可以监听系统端口和通过内核和其他线程通信,接收、分发系统事件。
  • 它能够主动唤醒RunLoop(由操作系统内核进行管理,例如CFMessagePort消息)
  • 简单来说,Source1其实就是用来接收系统发出的事件(例如手机的触摸、摇晃或者锁屏等等)。

当App在前台静止时,如果我们点击App的页面,此时我们首先接触的是手机屏幕,点击屏幕会产生一个系统事件,通过source1捕捉后由Springboard 程序包装成source0 分发给应用处理,因此我们在App内部接收到的触摸事件都是source0

在iOS项目的ViewController中,增加touchesBegan事件。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"%s",__func__);
}

我们在NSLog出增加断点,来查看函数调用栈:

我们发现函数从1直接到16,我们在控制台使用【 LLDB指令】进行查看。

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x0000000102a35f28 iOSStudy`-[ViewController touchesBegan:withEvent:](self=0x0000000144909b30, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x000000028154c000) at ViewController.m:22:5
    frame #1: 0x0000000192753924 UIKitCore`forwardTouchMethod + 344
    frame #2: 0x00000001927537a8 UIKitCore`-[UIResponder touchesBegan:withEvent:] + 64
    frame #3: 0x00000001927623f0 UIKitCore`-[UIWindow _sendTouchesForEvent:] + 496
    frame #4: 0x0000000192763f44 UIKitCore`-[UIWindow sendEvent:] + 3976
    frame #5: 0x000000019273d2cc UIKitCore`-[UIApplication sendEvent:] + 712
    frame #6: 0x00000001927c71ec UIKitCore`__dispatchPreprocessedEventFromEventQueue + 7360
    frame #7: 0x00000001927ca1a4 UIKitCore`__processEventQueue + 6460
    frame #8: 0x00000001927c1650 UIKitCore`__eventFetcherSourceCallback + 160
    frame #9: 0x000000018fce576c CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 28
    frame #10: 0x000000018fce5668 CoreFoundation`__CFRunLoopDoSource0 + 208
    frame #11: 0x000000018fce4960 CoreFoundation`__CFRunLoopDoSources0 + 268
    frame #12: 0x000000018fcdea8c CoreFoundation`__CFRunLoopRun + 824
    frame #13: 0x000000018fcde21c CoreFoundation`CFRunLoopRunSpecific + 600
    frame #14: 0x00000001a77e2784 GraphicsServices`GSEventRunModal + 164
    frame #15: 0x000000019271cfe0 UIKitCore`-[UIApplication _run] + 1072
    frame #16: 0x0000000192722854 UIKitCore`UIApplicationMain + 168
    frame #17: 0x0000000102a361b0 iOSStudy`main(argc=1, argv=0x000000016d3cf8a0) at main.m:17:12
    frame #18: 0x000000018f99e6b0 libdyld.dylib`start + 4
(lldb) 

可以看到 Runloop 正在处理的触摸事件是一个source0

FRunLoopObserverRef是观察者/监听器,用于监听Runloop的状态。如果Runloop 状态变更会通知监听者进行函数回调。每个Observer都包含了一个回调(函数指针)。比如:UI刷新就是通过监听器来实现的。

typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;

struct __CFRunLoopObserver {
    // CoreFoundation 中的 runtime 基础信息
    CFRuntimeBase _base;
    // 互斥锁
    pthread_mutex_t _lock;
    // observer 对应的 runloop
    CFRunLoopRef _runLoop;
    // observer 观察了多少个 runloop
    CFIndex _rlCount;
    CFOptionFlags _activities;        /* immutable */
    // observer 优先级
    CFIndex _order;            /* immutable */
    // observer 回调函数
    CFRunLoopObserverCallBack _callout;    /* immutable */
    // observer 上下文
    CFRunLoopObserverContext _context;    /* immutable, except invalidation */
};

当RunLoop的状态发生变化时,观察者就能通过回调接受到这个变化。主要是用来向外界报告Runloop当前的状态的更改。

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop       1
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer     2
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source    4
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠       32
    kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒     64
    kCFRunLoopExit          = (1UL << 7), // 即将退出Loop      128
};

在代码中,我们可以使用以下方式来监听RunLoop的状态变化:

// 1.先创建一个 observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    switch (activity) {
        case kCFRunLoopEntry:{
            NSLog(@"即将进入loop");
            break;
        }
        case kCFRunLoopBeforeTimers:{
            NSLog(@"即将处理Timer");
            break;
        }
        case kCFRunLoopBeforeSources:{
            NSLog(@"即将处理Source");
            break;
        }
        case kCFRunLoopBeforeWaiting:{
            NSLog(@"即将进入休眠");
            break;
        }
        case kCFRunLoopAfterWaiting:{
            NSLog(@"从休眠中唤醒");
            break;
        }
        case kCFRunLoopExit:{
            NSLog(@"即将退出loop");
            break;
        }
        default:
            break;
    } 
});
// 2 .给当前RunLoop添加观察者:监听RunLoop的状态
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);
// 3.释放Observer
CFRelease(observer);

例如监听到Runloop状态为BeforeWaiting就会刷新UI界面。

struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFMutableSetRef _rlModes;
    CFAbsoluteTime _nextFireDate;
    CFTimeInterval _interval;
    CFTimeInterval _tolerance;
    uint64_t _fireTSR;
    CFIndex _order;
    CFRunLoopTimerCallBack _callout;
    CFRunLoopTimerContext _context;
};
  • CFRunLoopTimerRef是基于时间的触发器,它和NSTimertoll-free bridged的,可以混用。其包含一个时间长度和一个回调(函数指针)。
  • 当其加入到RunLoop时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
  • NSTimer 的执行会受 Mode 影响。GCD 的定时器受 Runloop 的 Mode影响。
  • 方法验证performSelector:withObject:afterDelay:

Timer里面的是 CFRunLoopTimerRef,包括了定时器事件、[performSelector: withObject: afterDelay:]

- (void)viewDidLoad {
    [super viewDidLoad];
    [self performSelector:@selector(run) withObject:nil afterDelay:1.0];
}
- (void)run{
    NSLog(@"-----run-----");
}

增加断点和bt命令,打印如下:

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x0000000102b45f5c iOSStudy`-[ViewController run](self=0x0000000103208dc0, _cmd="run") at ViewController.m:23:5
    frame #1: 0x00000001910e4454 Foundation`__NSFireDelayedPerform + 416
    frame #2: 0x000000018fce5fa0 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 32
    frame #3: 0x000000018fce5ba0 CoreFoundation`__CFRunLoopDoTimer + 1064
    frame #4: 0x000000018fce4ffc CoreFoundation`__CFRunLoopDoTimers + 328
    frame #5: 0x000000018fcdeee4 CoreFoundation`__CFRunLoopRun + 1936
    frame #6: 0x000000018fcde21c CoreFoundation`CFRunLoopRunSpecific + 600
    frame #7: 0x00000001a77e2784 GraphicsServices`GSEventRunModal + 164
    frame #8: 0x000000019271cfe0 UIKitCore`-[UIApplication _run] + 1072
    frame #9: 0x0000000192722854 UIKitCore`UIApplicationMain + 168
    frame #10: 0x0000000102b461bc iOSStudy`main(argc=1, argv=0x000000016d2bf8a0) at main.m:17:12
    frame #11: 0x000000018f99e6b0 libdyld.dylib`start + 4
(lldb) 

通过打印可以看出有CFRunLoopTimerRef的参与。

  • RunLoop 对象内部的 _currentMode 指向了该RunLoop其中一个RunLoopMode,就是当前运行中的 RunLoopMode。
  • RunLoop 当前只会执行_currentMode包含的事件(source0、source1、observer、timer)
  • 一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。
  • 每当RunLoop的内容发生变化时,RunLoop 都会自动将_commonModeItems里的 Source/Observer/Timer同步到具有Common”标记的所有Mode里。

系统注册了5个Mode分别如下:

1. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行。
2. UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode。
4. GSEventReceiveRunLoopMode: 接收系统事件的内部 Mode,通常用不到。
5. kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode。

在iOS中常见的2中Model:

这两个 Mode 都已经被标记为Common属性。
DefaultMode 是 App 平时所处的状态。
TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。
我们可以用这两个 Mode Name 来操作其对应的 Mode。

kCFRunLoopCommonModes 模式等效于 kCFRunLoopDefaultModeUITrackingRunLoopMode的结合。
它不是一个具体的模式,它可以理解成一个标签。
打上标签的RunLoopMode 会被放入到 RunLoop 内部的 _commonModes。
而放到_commonModes的作用可以参考上面。

问题思考,我们知道:

  • RunLoop启动时只能选择一个Mode,作为currentMode
  • 如果切换Mode只能退出当前Loop,再重新选择一个Mode进入。这样可以使不同组的Source0、Source1、Timer、Observer能分隔开来,互不影响。

下面我们来模拟一个实际案例:

操作一:启动一个定时器,每秒执行 `run`方法输出,这时系统会把该事件添加到` DefaultMode` 中,`Timer` 会得到重复调用。
操作二: 滑动一个`TableView`,`RunLoop `会将 `mode `切换为 `TrackingRunLoopMode`。

代码层面,我们XIB中拖入一个UITableVIew,开启一个定时器,然后运行程序:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 开启一个定时器,每隔一秒执行run方法
    [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(run) userInfo:nil repeats:YES];

    // 创建Observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry: {
                CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
                NSLog(@"kCFRunLoopEntry - %@", mode);
                CFRelease(mode);
                break;
            }
                
            case kCFRunLoopExit: {
                CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
                NSLog(@"kCFRunLoopExit - %@", mode);
                CFRelease(mode);
                break;
            }
                
            default:
                break;
        }
    });
    // 添加Observer到RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    // 释放
    CFRelease(observer);
}

- (void)run {
    NSLog(@"-----run-----");
}

开始拖动时:

 -----run-----
 -----run-----
 -----run-----
kCFRunLoopExit - kCFRunLoopDefaultMode
kCFRunLoopEntry - UITrackingRunLoopMode

结束拖动时:

kCFRunLoopExit - UITrackingRunLoopMode
kCFRunLoopEntry - kCFRunLoopDefaultMode
 -----run-----
 -----run-----

模拟结果:

视图不滑动时输出正常,但是当我们滑动视图时输出停止了。 

如果需要滑动的过程中同时处理定时器事件,在两个 Mode 中都能得到回调:
方法一:将 NSTimer 也加入到 UITrackingRunLoopMode(但这样timer被添加了两次,不是同一个timer)。
方法二:将 Timer 加入到顶层的 RunLoop 的 “commonModeItems” 中。”commonModeItems” 被 RunLoop 自动更新到所有具有”Common”属性的 Mode 里去。即:把 timer添加到 NSRunLoopCommonModes

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

此时 timer 会被放入 RunLoop 的 _commonModeItems 里。只要运行到 _commonModes 所包含的某个 RunLoopMode ,就会去处理 _commonModeItems里面的事件,RunLoopMode 本身的事件也会处理。

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

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

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

CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
  • 我们可以通过 mode name 来操作内部的 mode。
  • 当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode 时,RunLoop会自动帮你创建对应的 CFRunLoopModeRef。
  • 其内部的 mode 只能增加不能删除。
上一篇 下一篇

猜你喜欢

热点阅读