【精】Runloop 深入浅出,综合解答

简单的说run loop是事件驱动的一个大循环,如下代码所示:

int main(int argc, char * argv[]) {
     //程序一直运行状态
     while (AppIsRunning) {
          //睡眠状态,等待唤醒事件
          id whoWakesMe = SleepForWakingUp();
          //得到唤醒事件
          id event = GetEvent(whoWakesMe);
          //开始处理事件
          HandleEvent(event);
     }
     return 0;
}

-----------------------------runloop 与Cocoa息息相关---------------------------------------
1、相关知识:
        自动释放池、延迟回调、触摸事件、屏幕刷新、定时器(NSTimer)、GCD,mach kernel,block,pthread,NSObject(NSThreadPerformAddition),CADisplayLink,CATransition,CAAnimation,dispatch_get_main_queue(),NSPort,NSURLConnection,AFNetworking,这么说来跟很多知识都有关联。
2、CFRunLoopRef构造:数据结构;创建与退出;mode切换和item依赖;Runloop启动
3、Runloop内部逻辑:关键在两个判断点(是否睡觉,是否退出)
4、Runloop本质:mach port和mach_msg()
5、如何处理界面刷新
6、如何处理UI事件响应
7、如何处理手势
8、如何处理GCD任务
9、如何处理timer(与CADisplayLink)
10、如何处理performSelector
11、如何处理网络请求
12、Runloop 常见应用

---------------------------------------------------------------------------------------------------------

1、与Runloop相关的模块

先简单介绍runloop的功能,以及涉及到的模块
        【线程】 Runloop的寄生于线程:一个线程只能有唯一对应的runloop;但这个根runloop里可以嵌套子runloops;线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。在任何一个Cocoa程序的线程中,都可以通过:NSRunLoop   *runloop = [NSRunLoop currentRunLoop];
        线程(创建)-->runloop将进入-->最高优先级OB创建释放池-->runloop将睡-->最低优先级OB销毁旧池创建新池-->runloop将退出-->最低优先级OB销毁新池-->线程(销毁)
        主线程 (有 RunLoop 的线程) 几乎所有函数都从以下六个之一的函数调起(可以通过xcode端点调试,看函数栈):

CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION
CFRunloop is calling out to an abserver callback function
用于向外部报告 RunLoop 当前状态的更改,框架中很多机制都由 RunLoopObserver 触发,如 CAAnimation

CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK
CFRunloop is calling out to a block
消息通知、非延迟的perform、dispatch调用、block回调、KVO

CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
CFRunloop is servicing the main desipatch queue

CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION
CFRunloop is calling out to a timer callback function
延迟的perform, 延迟dispatch调用

CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
CFRunloop is calling out to a source 0 perform function
处理App内部事件、App自己负责管理(触发),如UIEvent、CFSocket。普通函数调用,系统调用

CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION
CFRunloop is calling out to a source 1 perform function
由RunLoop和内核管理,Mach port驱动,如CFMachPort、CFMessagePort

【自动释放池】自动释放池寄生于Runloop:程序启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler(),一是,监测Entry(即将进入Loop)状态,其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池,其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前;另外一个,Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
       下面将介绍runloop的结构,然后继续介绍【触摸事件】,【屏幕刷新】,【定时器】,【屏幕刷新】等。

2. CFRunLoopRef构造:

NSRunloop是对CFRunloopRef的面向对象封装,但是不是线程安全,CFRunloopRef数据结构如下:

// runloop数据结构
struct __CFRunLoopMode {
    CFStringRef _name;            // Mode名字,
    CFMutableSetRef _sources0;    // Set<CFRunLoopSourceRef>
    CFMutableSetRef _sources1;    // Set<CFRunLoopSourceRef>
    CFMutableArrayRef _observers; // Array<CFRunLoopObserverRef>
    CFMutableArrayRef _timers;    // Array<CFRunLoopTimerRef>
    ...
};
// mode数据结构
struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set<CFStringRef>
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set<CFRunLoopModeRef>
    ...
};

创建与退出:mode切换和item依赖

a 主线程的runloop自动创建,子线程的runloop默认不创建(在子线程中调用NSRunLoop *runloop = [NSRunLoop currentRunLoop];获取RunLoop对象的时候,就会创建RunLoop);
    b runloop退出的条件:app退出;线程关闭;设置最大时间到期;modeItem为空;
    c 同一时间一个runloop只能在一个mode,切换mode只能退出runloop,再重进指定mode(隔离modeItems使之互不干扰);
    d 一个item可以加到不同mode;一个mode被标记到commonModes里(这样runloop不用切换mode)。
启动Runloop:

// 用DefaultMode启动
void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}

// 用指定的Mode启动,允许设置RunLoop最大时间(假无限循环),执行完毕是否退出
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

CFRunLoopModeRef:
数据结构(见上);
创建添加:runloop自动创建对应的mode;mode只能添加不能删除

// 添加mode
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);

类型:
1. kCFRunLoopDefaultMode: 默认 mode,通常主线程在这个 Mode 下运行。
2. UITrackingRunLoopMode: 追踪mode,保证Scrollview滑动顺畅不受其他 mode 影响。
3. UIInitializationRunLoopMode: 启动程序后的过渡mode,启动完成后就不再使用。
4: GSEventReceiveRunLoopMode: Graphic相关事件的mode,通常用不到。
5: kCFRunLoopCommonModes: 占位mode,作为标记DefaultMode和CommonMode用。

modeItems

// 添加移除item的函数(参数:添加/移除哪个item到哪个runloop的哪个mode下)
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);

CFRunLoopSourceRef:事件来源
按照官方文档CFRunLoopSourceRef为3类,但数据结构只有两类(???)
Port-Based Sources:与内核端口相关
Custom Input Sources:与自定义source相关
Cocoa Perform Selector Sources:与PerformSEL方法相关)

数据结构(source0/source1);

// source0 (manual): order(优先级),callout(回调函数)
CFRunLoopSource {order =..., {callout =... }}

// source1 (mach port):order(优先级),port:(端口), callout(回调函数)
CFRunLoopSource {order = ..., {port = ..., callout =...}

source0:event事件,只含有回调,需要标记待处理(signal),然后手动将runloop唤醒(wakeup);
source1 :包含一个 mach_port 和一个回调,被用于通过内核和其他线程发送的消息,能主动唤醒runloop。
CFRunLoopObserverRef:监听runloop状态,接收回调信息(常见于自动释放池创建销毁)
数据结构:

// Observer:order(优先级),ativity(监听状态),callout(回调函数)
CFRunLoopObserver {order = ..., activities = ..., callout = ...}

创建与添加;

// 第一个参数用于分配该observer对象的内存空间
// 第二个参数用以设置该observer监听什么状态
// 第三个参数用于标识该observer是在第一次进入run loop时执行还是每次进入run loop处理时均执行
// 第四个参数用于设置该observer的优先级,一般为0
// 第五个参数用于设置该observer的回调函数
// 第六个参数observer的运行状态
CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
      // 执行代码
}

监听的状态:

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

3、Runloop内部逻辑:关键在两个判断点(是否睡觉,是否退出)

代码实现:

// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {

    // 0.1 根据modeName找到对应mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
    // 0.2 如果mode里没有source/timer/observer, 直接返回。
    if (__CFRunLoopModeIsEmpty(currentMode)) return;

    // 1.1 通知 Observers: RunLoop 即将进入 loop。---(OB会创建释放池)
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);

    // 1.2 内部函数,进入loop
    __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {

        Boolean sourceHandledThisLoop = NO;
        int retVal = 0;
        do {

            // 2.1 通知 Observers: RunLoop 即将触发 Timer 回调。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
            // 2.2 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
            // 执行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);

            // 2.3 RunLoop 触发 Source0 (非port) 回调。
            sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
            // 执行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);

            // 2.4 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
            if (__Source0DidDispatchPortLastTime) {
                Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                if (hasMsg) goto handle_msg;
            }

            // 3.1 如果没有待处理消息,通知 Observers: RunLoop 的线程即将进入休眠(sleep)。--- (OB会销毁释放池并建立新释放池)
            if (!sourceHandledThisLoop) {
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
            }

            // 3.2. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
            // -  一个基于 port 的Source1 的事件。
            // -  一个 Timer 到时间了
            // -  RunLoop 启动时设置的最大超时时间到了
            // -  被手动唤醒
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
                mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
            }

            // 3.3. 被唤醒,通知 Observers: RunLoop 的线程刚刚被唤醒了。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);

            // 4.0 处理消息。
            handle_msg:

            // 4.1 如果消息是Timer类型,触发这个Timer的回调。
            if (msg_is_timer) {
                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
            } 

            // 4.2 如果消息是dispatch到main_queue的block,执行block。
            else if (msg_is_dispatch) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } 

            // 4.3 如果消息是Source1类型,处理这个事件
            else {
                CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                if (sourceHandledThisLoop) {
                    mach_msg(reply, MACH_SEND_MSG, reply);
                }
            }

            // 执行加入到Loop的block
            __CFRunLoopDoBlocks(runloop, currentMode);

            // 5.1 如果处理事件完毕,启动Runloop时设置参数为一次性执行,设置while参数退出Runloop
            if (sourceHandledThisLoop && stopAfterHandle) {
                retVal = kCFRunLoopRunHandledSource;
            // 5.2 如果启动Runloop时设置的最大运转时间到期,设置while参数退出Runloop
            } else if (timeout) {
                retVal = kCFRunLoopRunTimedOut;
            // 5.3 如果启动Runloop被外部调用强制停止,设置while参数退出Runloop
            } else if (__CFRunLoopIsStopped(runloop)) {
                retVal = kCFRunLoopRunStopped;
            // 5.4 如果启动Runloop的modeItems为空,设置while参数退出Runloop
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                retVal = kCFRunLoopRunFinished;
            }

            // 5.5 如果没超时,mode里没空,loop也没被停止,那继续loop,回到第2步循环。
        } while (retVal == 0);
    }

    // 6. 如果第6步判断后loop退出,通知 Observers: RunLoop 退出。--- (OB会销毁新释放池)
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

函数作用栈显示:

{
    // 1.1 通知Observers,即将进入RunLoop
    // 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
    do {

        // 2.1 通知 Observers: 即将触发 Timer 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
        // 2.2 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
         // 执行Block
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

        // 2.3 触发 Source0 (非基于port的) 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
        // 执行Block
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

        // 3.1 通知Observers,即将进入休眠
        // 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);

        // 3.2 sleep to wait msg.
        mach_msg() -> mach_msg_trap();

        // 3.3 通知Observers,线程被唤醒
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);

        // 4.1 如果是被Timer唤醒的,回调Timer
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);

        // 4.2 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);

        // 4.3 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);

        // 5. 退出判断函数调用栈无显示
    } while (...);

    // 6. 通知Observers,即将退出RunLoop
    // 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}

一步一步写具体的实现逻辑过于繁琐不便理解,按Runloop状态大致分为:
        1- Entry:通知OB(创建pool);
        2- 执行阶段:按顺序通知OB并执行timer,source0;若有source1执行source1;
        3- 休眠阶段:利用mach_msg判断进入休眠,通知OB(pool的销毁重建);被消息唤醒通知OB;
        4- 执行阶段:按消息类型处理事件;
        5- 判断退出条件:如果符合退出条件(一次性执行,超时,强制停止,modeItem为空)则退出,否则回到第2阶段;
        6- Exit:通知OB(销毁pool)。

4、Runloop本质:mach port和mach_msg()。

Mach是XNU的内核,进程、线程和虚拟内存等对象通过端口发消息进行通信,Runloop通过mach_msg()函数发送消息,如果没有port 消息,内核会将线程置于等待状态 mach_msg_trap() 。如果有消息,判断消息类型处理事件,并通过modeItem的callback回调(处理事件的具体执行是在DoBlock里还是在回调里目前我还不太明白???)。
        Runloop有两个关键判断点,一个是通过msg决定Runloop是否等待,一个是通过判断退出条件来决定Runloop是否循环。

5、如何处理界面刷新:

当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
        苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
        _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

这个函数内部的调用栈大概是这样的:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
    QuartzCore:CA::Transaction::observer_callback:
        CA::Transaction::commit();
            CA::Context::commit_transaction();
                CA::Layer::layout_and_display_if_needed();
                    CA::Layer::layout_if_needed();
                        [CALayer layoutSublayers];
                            [UIView layoutSubviews];
                    CA::Layer::display_if_needed();
                        [CALayer display];
                            [UIView drawRect];

6、如何处理UI事件响应

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。
_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
        【举例】拿按钮点击来说在Main thread堆栈中所处位置,堆栈最底层大概是,往上依次是,如图:
5 Handle Touch Event
4 RunLoop(包含CFRunLoopRunSpecific,CFRunLoopRun,__CFRunLoopDoSouces0,__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION) 
3 GSEventRunModal(Graphic Services) 
2 UIApplication(main.m)
1 main
0 start(dyld)

注意看runloop的方法:CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION

7、如何处理手势

当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。
当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

8、如何处理GCD任务

当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调里执行这个 block。Runloop只处理主线程的block,dispatch 到其他线程仍然是由 libDispatch 处理的。

注意看runloop的方法:CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE

9、如何处理timer:(与CADisplayLink)

NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

CFRunLoopTimerRef:系统内“定时闹钟”
NSTimer和performSEL方法实际上是对CFRunloopTimerRef的封装;runloop启动时设置的最大超时时间实际上是GCD的dispatch_source_t类型。
数据结构:

// Timer:interval:(闹钟间隔), tolerance:(延期时间容忍度),callout(回调函数)
CFRunLoopTimer {firing =..., interval = ...,tolerance = ...,next fire date = ...,callout = ...}

创建与生效;

//NSTimer:
  // 创建一个定时器(需要手动加到runloop的mode中)
 + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

  // 默认已经添加到主线程的runLoop的DefaultMode中
 + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

// performSEL方法
// 内部会创建一个Timer到当前线程的runloop中(如果当前线程没runloop则方法无效;performSelector:onThread: 方法放到指定线程runloop中)
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay

相关类型(GCD的timer与CADisplayLink)
GCD的timer:
dispatch_source_t 类型,可以精确的参数,不用以来runloop和mode,性能消耗更小。

dispatch_source_set_timer(dispatch_source_t source, // 定时器对象
                              dispatch_time_t start, // 定时器开始执行的时间
                              uint64_t interval, // 定时器的间隔时间
                              uint64_t leeway // 定时器的精度
                              );

CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop。
        CADisplayLink :
Timer的tolerance表示最大延期时间,如果因为阻塞错过了这个时间精度,这个时间点的回调也会跳过去,不会延后执行。
        CADisplayLink 是一个和屏幕刷新率一致的定时器,如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似,只是没有tolerance容忍时间),造成界面卡顿的感觉。

10、如何处理performSelector

除了基于端口的源,Cocoa定义了自定义输入源,允许你在任何线程执行selector方法。和基于端口的源一样,执行selector请求会在目标线程上序列化,减缓许多在线程上允许多个方法容易引起的同步问题。不像基于端口的源,一个selector执行完后会自动从run loop里面移除。
当在其他线程上面执行selector时,目标线程须有一个活动的run loop。对于你创建的线程,这意味着线程在你显式的启动run loop之前是不会执行selector方法的,而是一直处于休眠状态。
        NSObject类提供了类似如下的selector方法:

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)argwaitUntilDone:(BOOL)wait modes:(NSArray *)array; 

注意看runloop的方法:cfrunloop_is_calling_out_to_a_timer_callback_function

11、如何处理网络请求

关于网络请求的接口:最底层是CFSocket层,然后是CFNetwork将其封装,然后是NSURLConnection对CFNetwork进行面向对象的封装,NSURLSession 是 iOS7 中新增的接口,也用到NSURLConnection的loader线程。所以还是以NSURLConnection为例。
        当开始网络传输时,NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。

12、Runloop 常见应用:

根据前面介绍过的跟runloop先关的模块,那么每个模块都有应用。这里列举常见应用:

滑动与图片刷新

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

- (void)viewDidLoad {
  [super viewDidLoad];
  // 只在NSDefaultRunLoopMode下执行(刷新图片)
  [self.myImageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@""] afterDelay:ti inModes:@[NSDefaultRunLoopMode]];
}

常驻子线程,保持子线程一直处理事件
        为了保证线程长期运转,可以在子线程中加入RunLoop,并且给Runloop设置item,防止Runloop自动退出。

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}
- (void)start {
    [self.lock lock];
    if ([self isCancelled]) {
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } else if ([self isReady]) {
        self.state = AFOperationExecutingState;
        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    [self.lock unlock];
}

接到程序崩溃时的信号进行自主处理例如弹出提示等

CFRunLoopRef runLoop = CFRunLoopGetCurrent();
NSArray *allModes = CFBridgingRelease(CFRunLoopCopyAllModes(runLoop));
while (1) {
     for (NSString *mode in allModes) {
          CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
     }
}

异步测试

- (BOOL)runUntilBlock:(BOOL(^)())block timeout:(NSTimeInterval)timeout
{
     __block Boolean fulfilled = NO;
     void (^beforeWaiting) (CFRunLoopObserverRef observer, CFRunLoopActivity activity) =
     ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
          fulfilled = block();
          if (fulfilled) {
               CFRunLoopStop(CFRunLoopGetCurrent());
          }
     };

     CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, 0, beforeWaiting);
     CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

     // Run!
     CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, false);

     CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
     CFRelease(observer);

     return fulfilled;
}

Run loop的优点

一个run loop就是一个事件处理循环,用来不停的监听和处理输入事件并将其分配到对应的目标上进行处理。如果仅仅是想实现这个功能,你可能会想一个简单的while循环不就可以实现了吗,用得着费老大劲来做个那么复杂的机制?显然,苹果的架构设计师不是吃干饭的,你想到的他们早就想过了。
首先,NSRunLoop是一种更加高明的消息处理模式,他就高明在对消息处理过程进行了更好的抽象和封装,这样才能是的你不用处理一些很琐碎很低层次的具体消息的处理,在NSRunLoop中每一个消息就被打包在input source或者是timer source(见后文)中了。
        其次,也是很重要的一点,使用run loop可以使你的线程在有工作的时候工作,没有工作的时候休眠,这可以大大节省系统资源。

什么时候使用run loop

仅当在为你的程序创建辅助线程的时候,你才需要显式运行一个run loop。Run loop是程序主线程基础设施的关键部分。所以,Cocoa和Carbon程序提供了代码运行主程序的循环并自动启动run loop。IOS程序中UIApplication的run方法(或Mac OS X中的NSApplication)作为程序启动步骤的一部分,它在程序正常启动的时候就会启动程序的主循环。类似的,RunApplicationEventLoop函数为Carbon程序启动主循环。如果你使用xcode提供的模板创建你的程序,那你永远不需要自己去显式的调用这些例程。

参考http://www.starming.com/index.php?v=index&view=74 
http://www.jianshu.com/p/37ab0397fec7
http://blog.csdn.net/ztp800201/article/details/9240913
http://www.cnblogs.com/zy1987/p/4582466.html

时间: 2024-11-13 06:36:29

【精】Runloop 深入浅出,综合解答的相关文章

埃博拉病毒Ebola_1_中国疾病控制中心

2014年以来,西非各国暴发了严重的埃博拉出血热疫情,世界卫生组织宣布这起疫情已构成“国际公共卫生事件”.埃博拉病毒是否会传入中国?它的传染性有多强?如何预防?我们能否控制疫情?目前是否已经找到有效的治疗方法?本书将告诉你一系列问题的答案. 我们希望通过宣传普及埃博拉出血热的预防控制知识,增强大家对防范疫情的信心,不恐慌,不传谣,从而切实保障国家安全和大家的身体健康. 本书的读者对象主要为中小学生,我们采用生动有趣的漫画形式,深入浅出地解答了什么是埃博拉病毒及其主要传播途径,以及为什么埃博拉病毒

Github上安卓榜排名第2的程序员教你如何学习【转载,侵删】

来自:峰瑞资本(微信号:freesvc)文章作者:代码家(微信 ID:daimajia_share) 软件早已吞噬整个世界,程序员是关键角色.过去 40 年中,许多伟大的公司都由程序员缔造,比如比尔·盖茨.拉里·埃里森.马克·扎克伯格.伊隆·马斯克. 一本哥哥不太关心的 Visual Basic 课外书,代码家如获至宝,他接触了简单的代码,这让他进入到一个全新的世界.从那之后,代码家一直享受着用代码解决问题过程中产生的精神愉悦,并成长为一名专家级程序员.用行话说,黑客.现在,代码家是 Githu

程序的学习过程和学习经验分享

每天,都会有人在微博上私信我,问我关于学习和成长的问题.这种问题我一般都不会回复某个j,毕竟每个人的情况不一样,每个人对待事物的性格也不一样,我不能夸下海口的说,你看某本书几个月就能如何如何,我能做的多数时候只是鼓励鼓励,其次我也从不认为我已经成长到一个多高的境界上,让我去给别人做指导,我还是当之有愧的. 但是,有时候看到大家私信我的时候,确实是处于一种很痛苦或是需要帮助的状态上. 最近晚上有空,就想写写自己从第一次接触程序,到一路颠簸学到今天的一些故事和经验,希望对大家有所帮助. 学习的三个阶

分享一些自己的学习过程和学习方法(来自daimajia)

每天,都会有人在微博上私信我,问我关于学习和成长的问题.这种问题我一般都不会回复某个j,毕竟每个人的情况不一样,每个人对待事物的性格也不一样,我不能夸下海口的说,你看某本书几个月就能如何如何,我能做的多数时候只是鼓励鼓励,其次我也从不认为我已经成长到一个多高的境界上,让我去给别人做指导,我还是当之有愧的. 但是,有时候看到大家私信我的时候,确实是处于一种很痛苦或是需要帮助的状态上. 最近晚上有空,就想写写自己从第一次接触程序,到一路颠簸学到今天的一些故事和经验,希望对大家有所帮助. 学习的三个阶

畅谈HTML开发

如今,打开浏览器,各种各样的页面能够让人眼花缭乱,对于行外人看到的是美观效果是用户体验,对行内人更多的是关注技术和创造力.对于开发人员都知道DIV是一对html常用标签,DIV+CSS是一对很诱人的组合,很多界面设计师通过软件培训班等学习这门手艺,当然自然很多是冲着会用了DIV+CSS制作网页,常常会被人赞以大拇指的.现在提到DIV+CSS,还有很多人热情高涨.接下来帮助大家纠正下DIV+CSS的一些误解. 1.DIV只是HTML最常用的标签之一 显然HTML并不只是一个DIV标签有用,每个标签

分享《自然语言处理理论与实战》PDF及代码+唐聃+《深入浅出Python机器学习》PDF及代码+段小手+《深度学习实践:计算机视觉》PDF+缪鹏+《最优化理论与算法第2版》高清PDF+习题解答PDF+《推荐系统与深度学习》PDF及代码学习

<自然语言处理理论与实战>高清PDF,362页,带书签目录,文字可以复制:配套源代码.唐聃等著. <大数据智能互联网时代的机器学习和自然语言处理技术>PDF,293页,带书签目录,文字可以复制,彩色配图.刘知远等著.  下载: https://pan.baidu.com/s/1waP6C086-32_Lv0Du3BbNw 提取码: 1ctr <自然语言处理理论与实战>讲述自然语言处理相关学科知识和理论基础,并介绍使用这些知识的应用和工具,以及如何在实际环境中使用它们.由

[家里蹲大学数学杂志]第317期厦门大学2010年综合基础I考研试题参考解答

数学分析部分 ($110'$) 1. 选择题 ($5\times 6'=30'$) (1)  设函数 $f(x)$ 二阶可导, 并且满足方程 $$\bex f''(x)+3[f'(x)]^2+2e^x f(x)=0, \eex$$ 设 $x_0$ 为 $f(x)$ 的一个驻点并且满足 $f(x_0)<0$, 则 $f(x_0)$ 在点 $x_0$ (B). A.   取极大值; B.   取极小值; C.   不取极值; D.   不能确定. 解答: $$\bex f''(x_0)=-2e^{x

传智播客 刘意_2015年Java基础视频-深入浅出精华版 笔记(day11~)(2016年2月3日16:01:00)

day11 1.Eclipse的基本使用 编译: 自动编译,在保存的那一刻(ctrl+s)帮你做好了(class文件出现在bin目录下) 2.Hierarchy 显示Java继承层次结构,选中类后F4 3.eclipse行号的显示与隐藏 4.D:字体大小及颜色 a:Java代码区域的字体大小和颜色: window -- Preferences -- General -- Appearance -- Colors And Fonts -- Java修改 -- Java Edit Text Font

asp编程实例:ASP设计常见问题及解答精要

作为微软推出的网页与数据库解决方案,ASP由于有微软得天独厚的操作系统等技术后盾支持,因此得到了迅速的发展,并且正受到越来越多的欢迎,在目前在电子商务网站的建设中有很多都使用ASP来编写程序. 近来有很多的读者向我们咨询学习ASP的方法和在学习中遇到的一些问题,其中有一些是初学者经常遇到和面临的问题.为了让读者在学习ASP的过程中加深对ASP的了解,并能够有效地解决遇到的问题,我们特别收集了比较有代表性的一些问题,在此列举出来并一一给予解答,希望能在这有限的篇幅里为你学习ASP助一臂之力. 1.