美文网首页
什么是RunLoop,RunLoop有哪些使用场景

什么是RunLoop,RunLoop有哪些使用场景

作者: 码农老张 | 来源:发表于2018-07-26 16:39 被阅读0次

    每次面试,Runloop这个概念几乎是必问的。所以,还是写点东西出来做个记录,同时也加深一下自己的记忆。

    一.什么是RunLoop

            RunLoop 既运行循环机制,在应用级别考虑,应用程序中所有的任务处理(用户交互事件、网络请求回调数据接收等)都是在线程中执行,一般来讲一个线程一次只能执行一个任务,执行完线程销毁(OC 中子线程异步销毁,主线程除外),但是问题来了如何让线程保活呢?并且如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒呢?RunLoop 就是解决这些问题的,它所做的一切都是基于线程,可以说是为线程而生

    OSX/iOS 系统中,提供的 两个 RunLoop 对象:CFRunLoopRef是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,且其是开源的,开源下载地址(CoreFoundation 源码)。NSRunLoop是基于 CFRunLoopRef 的封装,提供了面向对象的 API,并不开源。分析 CoreFoundation 库内的 RunLoop 源码分析。CoreFoundation 源码中 CFRunLoop 关于 RunLoop 的有五个类:

    CFRunLoopRef

    CFRunLoopSourceRef

    CFRunLoopTimerRef

    CFRunLoopObserverRef

    CFRunLoopModeRef

    这五大类是如何通过C语言封装成的呢 ?

    typedefstruct__CFRunLoop*CFRunLoopRef;

    typedefstruct__CFRunLoopSource*CFRunLoopSourceRef;

    typedefstruct__CFRunLoopObserver*CFRunLoopObserverRef;

    typedefstruct__CFRunLoopTimer*CFRunLoopTimerRef;      

    由 CFRunLoop.h文件查看到如上源码

    typedefstruct__CFRunLoopMode*CFRunLoopModeRef;

    由 CFRunLoopModeRef 类于CFRunLoop.c 第 521 行可找到,下面具体分析其各种的源码结构

    RunLoop

    CFRunLoop.c文件中 第 636 行:

    struct__CFRunLoop{

            CFRuntimeBase _base;

            pthread_mutex_t_lock;/* locked for accessing mode list */

            __CFPort _wakeUpPort;// used for CFRunLoopWakeUp  内核向该端口发送消息可以唤醒

            runloopBoolean _unused;

            volatile_per_run_data *_perRunData;// reset for runs of the run loop

            pthread_t_pthread;//RunLoop对应的线程

            uint32_t_winthread;   

            CFMutableSetRef _commonModes;//存储的是字符串,记录所有标记为common的mode

            CFMutableSetRef _commonModeItems;//等同NSMutableSet 存储所有commonMode的item(source、timer、observer)

            CFRunLoopModeRef _currentMode;//当前运行的mode

            CFMutableSetRef _modes;//存储的是CFRunLoopModeRef

            struct_block_item*_blocks_head;//doblocks的时候用到

            struct_block_item*_blocks_tail;

            CFTypeRef _counterpart;

    };

    由源码可知,一个RunLoop对象,主要包含了对应的一个线程,若干个 Mode,若干个 commonMode,还有一个当前运行的    Mode(_currentMode)。

    我们并不能去创建这个 CFRunLoopRef(为什么呢?),而是通过如下方法去获取当前线程的 RunLoop:

    // CFRunLoop.h 中73行源码

    CF_EXPORT  CFRunLoopRef  CFRunLoopGetCurrent(void);// 获取当前线程runloop

    CF_EXPORT  CFRunLoopRef  CFRunLoopGetMain(void);// 获取主线程 runloop

    具体使用姿势:

    // 获取当前Runloop

    CFRunLoopRef  runloop =CFRunLoopGetCurrent();

    RunLoopMode - 运行模式

    CFRunLoop.c文件中 第 523 行:

    struct__CFRunLoopMode{

            CFRuntimeBase    _base;

            pthread_mutex_t   _lock;/* must have the run loop locked before locking this */

            CFStringRef   _name;//mode名称

            Boolean   _stopped;//mode是否被终止

            char  _padding[3];

            //几种事件

            CFMutableSetRef   _sources0;//sources0

            CFMutableSetRef   _sources1;//sources1

            CFMutableArrayRef   _observers;//通知

            CFMutableArrayRef   _timers;//定时器

            CFMutableDictionaryRef   _portToV1SourceMap;//字典  key是mach_port_t,value是CFRunLoopSourceRef

            __CFPortSet   _portSet;//保存所有需要监听的port,比如_wakeUpPort,_timerPort都保存在这个数组中

            CFIndex   _observerMask;

    #ifUSE_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;

    #end

    if#ifUSE_MK_TIMER_TOOmach_port_t   _timerPort;   

            Boolean   _mkTimerArmed;

    #endif

    #ifDEPLOYMENT_TARGET_WINDOWS

            DWORD    _msgQMask;

            void   (*_msgPump)(void);

    #endif

            uint64_t   _timerSoftDeadline;/* TSR */

            uint64_t   _timerHardDeadline;/* TSR */

    };

    一个 CFRunLoopMode 对象有一个 name ,若干 source0、source1、timer、observer和若干port,所有事件都是由 Mode 在管理,而 一个线程下的 RunLoop 管理着若干个 Mode (ModeItems)。在一个线程中 runloop 保活线程并且在 指定 Mode下使其接受处理事件,又在每次 runloop 循环中(之前讲的死循环概念)进行 Mode间的切换。

    Cocoa框架和Core Foundation框架中定义了五种 Mode (Guides and Sample Code )

    Default 默认Mode,APP运行起来之后,主线程的RunLoop默认在该Mode下运行

    NSDefaultRunLoopModeCocoa 框架

    kCFRunLoopDefaultModeCore Foundation 框架

    Event tracking 追踪触摸的手势,所有 UI 交互事件都运行在这个Mode下

    UITrackingRunLoopMode(Cocoa)

    Common modes 共有型 Model 含有上面两种Mode模式的意义

    NSRunLoopCommonModes(Cocoa)

    kCFRunLoopCommonModes(Core Foundation)

    Connection 系统内核模式,系统调用事件发生会切换到相应模式下,开发者无法操作

    GSEventReceiveRunLoopMode(Cocoa)

    Modal 项目初始化模式,只会走一次

    UIInitializationRunLoopMode(Cocoa)

    RunLoop Source - 事件源

    CFRunLoop.c文件中 第 943 行:

    struct    __CFRunLoopSource {

            CFRuntimeBase    _base;   

            uint32_t     _bits;//用于标记Signaled状态,source0只有在被标记为Signaled状态,才会被处理

            pthread_mutex_t     _lock;

            CFIndex_order;/* immutable */

            CFMutableBagRef    _runLoops;

            union{

                    CFRunLoopSourceContextversion0;/* immutable, except invalidation */

                    CFRunLoopSourceContext1version1;/* immutable, except invalidation */

                    } _context;

    };

    事件源顾名思义事件的产生地,由上源码会发现  Source 分为两种:

    Source0 非基于Port的

    只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理 signal 状态,然后手动调 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。

    其作用范围是应用程序中事件,由App自己管理的UIEvent、CFSocket都是source0。以下是source0的结构体:

    typedef    struct{

        CFIndex    version;

        void*  info;

        const    void*(*retain)(constvoid*info);

        void(*release)(constvoid*info);

        CFStringRef(*copyDescription)(constvoid*info);   

        Boolean (*equal)(constvoid*info1,constvoid*info2);

        CFHashCode(*hash)(constvoid*info);

        void(*schedule)(void*info,CFRunLoopRefrl,CFStringRefmode);

        void(*cancel)(void*info,CFRunLoopRefrl,CFStringRefmode);

        void(*perform)(void*info);

    }CFRunLoopSourceContext;

    Source1

    包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。可以接收内核消息并触发回调,这种 Source 能主动唤醒 RunLoop 的线程。

    其作用范围是由RunLoop和内核管理,source1带有mach_port_t,可以接收内核消息并触发回调,以下是source1的结构体:

    typedef    struct{

        CFIndex    version;

        void*  info;

        constvoid*(*retain)(constvoid*info);

        void(*release)(constvoid*info);

        CFStringRef(*copyDescription)(constvoid*info);   

        Boolean (*equal)(constvoid*info1,constvoid*info2);

        CFHashCode(*hash)(constvoid*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,CFIndexsize,CFAllocatorRefallocator,void*info);

    #else

        void*  (*getPort)(void*info);

        void(*perform)(void*info);

    #endif

    }CFRunLoopSourceContext1;

    CFRunLoopObserver - 观察者

    CFRunLoop.c文件中 第 981 行:

    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 */

    };

    Observer 是 mode 下数组保存 ,其都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。回调函数CFRunLoopObserverCallBack

    CFRunLoopObserver 可以观察的状态有如下6种:

    /* Run Loop Observer Activities */

    typedefCF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {

        kCFRunLoopEntry = (1UL <<0),

        kCFRunLoopBeforeTimers = (1UL <<1),

        kCFRunLoopBeforeSources = (1UL <<2),

        kCFRunLoopBeforeWaiting = (1UL <<5),

        kCFRunLoopAfterWaiting = (1UL <<6),

        kCFRunLoopExit = (1UL <<7),

        kCFRunLoopAllActivities =0x0FFFFFFFU

    };

    CFRunLoopTimer

    CFRunLoop.c文件中 第 1049 行:

    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 */

    };

    是基于时间的触发器,它和 NSTimer 是toll-free bridged的,可以相互转换的。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,可以在设定的时间点 RunLoop 会被唤醒并执行回调。

    RunLoop 与线程的关系

    苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 这两个函数内部的逻辑大概是下面这样:

    CFRunLoopRef CFRunLoopGetMain(void) {

        CHECK_FOR_FORK();

        staticCFRunLoopRef __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)returnrl;

        return_CFRunLoopGet0(pthread_self());

    }

    获取RunLoop函数

    staticCFMutableDictionaryRef __CFRunLoops =NULL;

    staticCFSpinLock_t loopsLock = CFSpinLockInit;

    // should only be called by Foundation

    // t==0 is a synonym for "main thread" that always works

    CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {

        if(pthread_equal(t, kNilPthreadT)) {

    t = pthread_main_thread_np();

        }

        __CFSpinLock(&loopsLock);

        if(!__CFRunLoops) {

            __CFSpinUnlock(&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);

            __CFSpinLock(&loopsLock);

        }

        CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));

        __CFSpinUnlock(&loopsLock);

        if(!loop) {

    CFRunLoopRef newLoop = __CFRunLoopCreate(t);

            __CFSpinLock(&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

            __CFSpinUnlock(&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);

            }

        }

        returnloop;

    从上面的代码可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。

    AutoreleasePool和RunLoop

    App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

    第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

    第二个 Observer 监视了两个事件:

    BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;

    Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

    在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

    事件响应

    苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。

    当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考这里。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用mach port 转发给需要的App进程

    随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发,此过程是Source0 完成的。

    _UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的

    手势识别

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

    苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。

    当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

    界面刷新

    当UI改变( Frame变化、 UIView/CALayer 的继承结构变化等)时,或手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理。

    苹果注册了一个用来监听BeforeWaiting和Exit的Observer,在它的回调函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面

    定时器

    当使用NSTimer的scheduledTimerWithTimeInterval方法时。事实上此时Timer会被加入到当前线程的Run Loop中,且模式是默认的NSDefaultRunLoopMode。而如果当前线程就是主线程,也就是UI线程时,某些UI事件,比如UIScrollView的拖动操作,会将Run Loop切换成NSEventTrackingRunLoopMode模式,在这个过程中,默认的NSDefaultRunLoopMode模式中注册的事件是不会被执行的。也就是说,此时使用scheduledTimerWithTimeInterval添加到Run Loop中的Timer就不会执行。

    所以为了设置一个不被UI干扰的Timer,我们需要手动创建一个Timer,然后使用NSRunLoop的addTimer:forMode:方法来把Timer按照指定模式加入到Run Loop中。这里使用的模式是:NSRunLoopCommonModes,这个模式等效于NSDefaultRunLoopMode和NSEventTrackingRunLoopMode的结合。

    无论是单次执行的NSTimer还是重复执行的NSTimer都不是准时的,这与当前NSTimer所处的线程有很大的关系,如果NSTimer当前所处的线程正在进行大数据处理(假设为一个大循环),NSTimer本次执行会等到这个大数据处理完毕之后才会继续执行

    这期间有可能会错过很多次NSTimer的循环周期,但是NSTimer并不会将前面错过的执行次数在后面都执行一遍,而是继续执行后面的循环,也就是在一个循环周期内只会执行一次循环。

    无论循环延迟的多离谱,循环间隔都不会发生变化,在进行完大数据处理之后,有可能会立即执行一次NSTimer循环,但是后面的循环间隔始终和第一次添加循环时的间隔相同。这个事件是怎么执行的?并且为什么有的时候会延迟?为什么子线程中创建的Timer并不执行?

    首先,在进入循环开始以后,就要处理source0事件,处理后检测一下source1端口是否有消息,如果一个Timer的时间间隔刚好到了则此处有可能会得到一个消息,则runLoop直接跳转至端口激活处从而去处理Timer事件。

    第二,为什么会延迟?我们知道,两次端口事件是在两个runLoop循环中分别执行的。比如Timer的时间间隔为1秒,在第一次Timer回调结束后,在很短时间内立即进入runLoop的下一次循环,这次并不是Timer回调并且是一个计算量非常大的任务,计算时间超过了1秒,那么runLoop的第二个循环就要执行很久,无法进入下一个循环等待有可能即将到来的Timer第二次回调的信号,所以Timer第二次回调就会推迟了。

    第三,为什么在子线程中创建的Timer并且提交到当前runLoop中并不会运行?这还是要从runLoop的获取函数中看,当调用currentRunLoop的时候会取当前线程对应的runLoop,而首次是取不到的,则会创建一个新的runLoop。但是!这个runLoop并没有run。就是没有开启

    - (void)applicationDidBecomeActive:(UIApplication*)application{

    // NSThread 创建一个子线程   

     [NSThreaddetachNewThreadSelector:@selector(testTimerSheduleToRunloop1) toTarget:selfwithObject:nil];

    }

    // 测试把timer加到不运行的runloop上的情况

    - (void)testTimerSheduleToRunloop1

    {

            NSLog(@"Test timer shedult to a non-running runloop");

             SvTestObject *testObject4 = [[SvTestObject alloc] init];

            NSTimer*timer = [[NSTimeralloc] initWithFireDate:[NSDatedateWithTimeIntervalSinceNow:1] interval:1target:testObject4 selector:@selector(timerAction:) userInfo:nilrepeats:NO];

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

            // 打开下面一行输出runloop的内容就可以看出,timer却是已经被添加进去//

            NSLog(@"the thread's runloop: %@", [NSRunLoop currentRunLoop]);

            // 下面一行, 该线程的runloop就会运行起来,timer才会起作用

            [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];

            NSLog(@"invoke release to testObject4");

    }

    - (void)applicationWillResignActive:(UIApplication*)application

    {

        NSLog(@"SvTimerSample Will resign Avtive!");

    }

    PerformSelecter

    当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

    当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

    Runloop 开发中运用

    滑动与图片刷新:

    当tableview的cell上有需要从网络获取的图片的时候,滚动tableView,异步线程会去加载图片,加载完成后主线程就会设置cell的图片,但是会造成卡顿。可以让设置图片的任务在CFRunLoopDefaultMode下进行,当滚动tableView的时候,RunLoop是在 UITrackingRunLoopMode 下进行,不去设置图片,而是当停止的时候,再去设置图片。

    GCD

    实际上 RunLoop 底层也会用到 GCD 的东西,当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

    友情链接:

    源码级 RunLoop 剖析 - 简书

    iOS - RunLoop 底层源码详解及具体运用 - 简书

    相关文章

      网友评论

          本文标题:什么是RunLoop,RunLoop有哪些使用场景

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