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

每次面试,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有哪些使用场景)