美文网首页
iOS-浅谈OC中的Runloop

iOS-浅谈OC中的Runloop

作者: 晴天ccc | 来源:发表于2019-06-19 08:57 被阅读0次

    目录

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

    RunLoop基础知识

    • 什么是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对象。
    • RunLoop的作用

    • 保持程序的持续运行。
    • 处理App中的各种事件(比如触摸事件、定时器事件等)。
    • 节省CPU资源,提高程序性能:该做事时做事,该休息时休息。
    • ......
    • 什么时候使用Runloop

    • 当需要和该线程进行交互的时候才会使用Runloop
    • iOS中的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对象

    获取当前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对象。
    • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
    • 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它的时候创建。
    • 主线程的RunLoop在main函数的UIApplicationMain方法中已经自动获取(创建)。
    • 子线程默认没有开启RunLoop,除非在子线程主动调用[NSRunLoop currentRunLoop]函数。
    • RunLoop会在线程结束时销毁。

    RunLoop的底层结构

    • CFRunLoopRef
    • CFRunLoopModeRef
    • CFRunLoopSourceRef
    • CFRunLoopTimerRef
    • CFRunLoopObserverRef

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

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

    • CFRunLoopRef

    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不会导致程序退出。
    • CFRunLoopModeRef

    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不会导致程序退出。
    • CFRunLoopSourceRef

    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
    • Source1包含了一个mach_port和一个回调指针,可以监听系统端口和通过内核和其他线程通信,接收、分发系统事件。
    • 它能够主动唤醒RunLoop(由操作系统内核进行管理,例如CFMessagePort消息)
    • 简单来说,Source1其实就是用来接收系统发出的事件(例如手机的触摸、摇晃或者锁屏等等)。

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

    • 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

    • CFRunLoopObserverRef

    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界面。

    • CFRunLoopTimerRef

    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的参与。

    • 补充

    • _currentMode

    • RunLoop 对象内部的 _currentMode 指向了该RunLoop其中一个RunLoopMode,就是当前运行中的 RunLoopMode。
    • RunLoop 当前只会执行_currentMode包含的事件(source0、source1、observer、timer)
    • _commonModes

    • 一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。
    • 每当RunLoop的内容发生变化时,RunLoop 都会自动将_commonModeItems里的 Source/Observer/Timer同步到具有Common”标记的所有Mode里。
    • RunLoop的五种运行模式

    系统注册了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:

    • kCFRunLoopDefaultMode
    • UITrackingRunLoopMode

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

    • kCFRunLoopCommonModes

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

    • 各个Model模式间的监听和切换

    问题思考,我们知道:

    • 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 只能增加不能删除。

    相关文章

      网友评论

          本文标题:iOS-浅谈OC中的Runloop

          本文链接:https://www.haomeiwen.com/subject/adzcqctx.html