20

RunLoop学习笔记

 5 years ago
source link: http://www.cocoachina.com/ios/20190121/26178.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在一般情况下,一个线程在执行完了一个任务后就会自动退出。我们想要有这样一个机制,让线程随时可以处理事件但是不退出。RunLoop实际就是一个对象,这个对象提供了一个入口函数,线程执行了这个函数后,会一直处在这个函数循环中,接收消息->等待->处理,直到循环结束。

RunLoop概念

RunLoop介绍

[图片上传失败...(image-663bc5-1547089087030)]

从代码上看,RunLoop就是一个对象,它的结构如下,源码在 这里

struct __CFRunLoop {
    CFRuntimeBase _base;    pthread_mutex_t _lock;  /* locked for accessing mode list */
    __CFPort _wakeUpPort;   // used for CFRunLoopWakeUp 内核向该端口发送消息可以唤醒runloop
    Boolean _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;//存储所有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,若干commoMode,若干commonModeItem,还有一个当前运行的mode。

RunLoop和线程

RunLoop就是管理线程的,当线程的RunLoop开启后,线程就会在执行任务后,处于休眠状态,随时等待接收新的任务被唤醒,而不是退出。

只有主线程的RunLoop是默认开启的,所以程序启动后,主线程会一直运行,不会退出。其他线程默认是没有RunLoop的,所以在执行完了任务后线程会自动退出,但是当我们去获取当前线程的RunLoop时,系统会自动创建该线程对应的RunLoop。

RunLoop Mode

Mode可以看做是事件的管家,一个Mode管理着各种事件,它的结构如下:

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;#if USE_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;#endif#if USE_MK_TIMER_TOO
    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 对象有一个name,若干source0,source1,timer,observer和port,可以看出事件都是由mode在管理,而RunLoop管理着Mode。

从源码可以看出,RunLoop总是运行在某个特定的Mode下,每个RunLoop都可以包含若干个Mode,每个Mode又包含source,timer,observer。每次调用RunLoop的主函数 _CFRunLoopRun() 时必须指定一种Mode。这个Mode称为 currentMode ,当切换Mode时必须退出当前Mode。然后重新进入RunLoop以保证不同的Mode的source,timer,observer互不影响。

[图片上传失败...(image-3cd7a0-1547089087030)]

如上图所示,RunLoop实际就是source,timer,observer的集合,不同的mode把不同组的source,timer,observer隔绝开来。RunLoop在某个时刻只能泡在一个Mode下,处理这一个mode中的source,timer,observer。

苹果文档中提到的Mode有五个:

  • NSDefaultRunLoopMode

  • NSConnectionReplyMode

  • NSModalPanelRunLoopMode

  • NSEventTrackingRunLoopMode

  • NSRunLoopCommonModes

    iOS中暴露的Mode只有 NSDefaultRunLoopModeNSRunLoopCommonModes , NSRunLoopCommonModes 是一个Mode的集合,默认包括 NSDefaultRunLoopModeNSEventTrackingRunLoopMode ,当我们把事件源加入到 NSRunLoopCommonModes 这种Mode时,就等于是把事件源同时加入了 NSDefaultRunLoopModeNSEventTrackingRunLoopMode 这两种Mode。我们也可以通过 CFRunLoopAddCommonMode() 这个方法将自定义的Mode加入到 kCFRunLoopCommonModes 组合中。

RunLoop Source

RunLoop Source分为Source,Observer,Timer三种,他们统称为ModeItem。

CFRunLoopSource

CFRunLoopSource 分为source0和source1,它的结构如下:

struct __CFRunLoopSource {    CFRuntimeBase _base;
    uint32_t _bits; //用于标记Signaled状态,source0只有在被标记为Signaled状态,才会被处理
    pthread_mutex_t _lock;    CFIndex _order;         /* immutable */
    CFMutableBagRef _runLoops;    union {        CFRunLoopSourceContext version0;     //version0和version1决定是source0还是source1
        CFRunLoopSourceContext1 version1;    
    } _context;
};

source0是APP内部事件,由APP自己管理的UIEvent都是source0。当一个source0事件准备执行的时候,必须先把他标记为signal状态,以下是source0的结构体:

typedef struct {    CFIndex version;    void *  info;    const void *(*retain)(const void *info);    void    (*release)(const void *info);    CFStringRef (*copyDescription)(const void *info);
    Boolean (*equal)(const void *info1, const void *info2);    CFHashCode  (*hash)(const void *info);    void    (*schedule)(void *info, CFRunLoopRef rl, CFStringRef mode);    void    (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode);    void    (*perform)(void *info);
} CFRunLoopSourceContext;

source0是飞非基于port的,它包含了一个回调,并不能主动触发事件。使用时需要先调用 CFRunLoopSourceSignal(source) ,将这个source标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒RunLoop,让其处理这个事件。

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

typedef struct {    CFIndex version;    void *  info;    const void *(*retain)(const void *info);    void    (*release)(const void *info);    CFStringRef (*copyDescription)(const void *info);
    Boolean (*equal)(const void *info1, const void *info2);    CFHashCode  (*hash)(const void *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, CFIndex size, CFAllocatorRef allocator, void *info);#else
    void *  (*getPort)(void *info);    void    (*perform)(void *info);#endif} CFRunLoopSourceContext1;

CFRunLoopObserver

CFRunLoopObserver 是RunLoop状态的监听者,可以监听RunLoop的各种状态并执行回调:

struct __CFRunLoopObserver {    CFRuntimeBase _base;
    pthread_mutex_t _lock;    CFRunLoopRef _runLoop;      //哪个RunLoop对象
    CFIndex _rlCount;    CFOptionFlags _activities;      //监听的是哪种状态
    CFIndex _order;         /* immutable */
    CFRunLoopObserverCallBack _callout; //回调
    CFRunLoopObserverContext _context;  /* immutable, except invalidation */};

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

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), //即将进入run loop
    kCFRunLoopBeforeTimers = (1UL << 1), //即将处理timer
    kCFRunLoopBeforeSources = (1UL << 2),//即将处理source
    kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),//被唤醒但是还没开始处理事件
    kCFRunLoopExit = (1UL << 7),//run loop已经退出
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

CFRunLoopTimer

CFRunLoopTimerNSTimertoll-free-bridged ,可以在设定的时间点抛出回调:

struct __CFRunLoopTimer {    CFRuntimeBase _base;
    uint16_t _bits;  //标记fire状态
    pthread_mutex_t _lock;    CFRunLoopRef _runLoop;        //添加该timer的runloop
    CFMutableSetRef _rlModes;     //存放所有 包含该timer的 mode的 modeName,意味着一个timer可能会在多个mode中存在
    CFAbsoluteTime _nextFireDate;    CFTimeInterval _interval;     //理想时间间隔  /* immutable */
    CFTimeInterval _tolerance;    //时间偏差      /* mutable */
    uint64_t _fireTSR;          /* TSR units */
    CFIndex _order;         /* immutable */
    CFRunLoopTimerCallBack _callout;    /* immutable */
    CFRunLoopTimerContext _context; /* immutable, except invalidation */};

RunLoop的实现

下面从以下3个方面来介绍RunLoop的实现:

  • 获取RunLoop

  • 添加Mode

  • 添加RunLoop Source

获取RunLoop

Apple不允许我们直接创建RunLoop对象,只能通过以下几个函数来获取:

  • CFRunLoopRef CFRunLoopGetCurrent(void)

  • CFRunLoopRef CFRunLoopGetMain(void)

  • +(NSRunLoop *)currentRunLoop

  • +(NSRunLoop *)mainRunLoop

    前两个是Core Foundation的API,后两个是Foundation中的API。

    那么RunLoop什么时候创建呢?我们进入函数内部来看:

CFRunLoopGetCurrent

CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);    if (rl) return rl;   //传入当前线程作为参数
    return _CFRunLoopGet0(pthread_self());
}

CFRunLoopGetCurrent(void) 函数内部调用了 _CFRunLoopGet0() ,传入的参数是当前线程。

CFRunLoopGetMain

CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();    static CFRunLoopRef __main = NULL; // no retain needed//传入主线程作为参数
    if (!__main) __main = _CFRunLoopGet0(_CFMainPThread); // no CAS needed
    return __main;
}

CFRunLoopGetMain(void) 最终也是调用了 CFRunLoopGet0 ,传入了主线程作为参数,所以这个方法不管是在主线程还是子线程中都能获得主线程的RunLoop。

CFRunLoopGet0

前面两个方法最终都是调用了 CFRunLoopGet0 这个方法,下面我们看一下这个方法的具体实现:

static CFMutableDictionaryRef __CFRunLoops = NULL;static CFSpinLock_t loopsLock = CFSpinLockInit; 
// t==0 is a synonym for "main thread" that always works//根据线程取RunLoopCF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {    if (pthread_equal(t, kNilPthreadT)) {
        t = pthread_main_thread_np();
    }
    __CFSpinLock(&loopsLock);    //如果存储RunLoop的字典不存在
    if (!__CFRunLoops) {
        __CFSpinUnlock(&loopsLock);        //创建一个临时字典dict
        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);        //创建主线程的RunLoop
        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());        //把主线程的RunLoop保存到dict中,key是线程,value是RunLoop
        CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);        //此处NULL和__CFRunLoops指针都指向NULL,匹配,所以将dict写到__CFRunLoops
        if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {            //释放dict
            CFRelease(dict);
        }        //释放mainrunloop
        CFRelease(mainLoop);
        __CFSpinLock(&loopsLock);
    }    //以上说明,第一次进来的时候,不管是getMainRunloop还是get子线程的runloop,主线程的runloop总是会被创建
    //从字典__CFRunLoops中获取传入线程t的runloop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFSpinUnlock(&loopsLock);    //如果没有获取到
    if (!loop) {        //根据线程t创建一个runloop
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFSpinLock(&loopsLock);
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));        if (!loop) {            //把newLoop存入字典__CFRunLoops,key是线程t
            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)) {            //注册一个回调,当线程销毁时,销毁对应的RunLoop
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }    return loop;
}

这段代码可以得出下列结论:

  • RunLoop和线程一一对应,对应的方式是以key-value的方式保存在一个全局字典中。

  • 主线程的RunLoop会在初始化全局字典时创建。

  • 子线程的RunLoop会在第一次获取的时候创建,如果不获取的话就一直不会被创建。

  • RunLoop会在线程销毁的时候销毁。

RunLoop运行

ueARFrj.jpg!web

以上是微博一位大佬画的RunLoop循环的图。

在Core Foundation中我们可以通过以下两个方法让RunLoop运行:

在默认的mode下运行当前线程的RunLoop:

  • void CFRunLoopRun(void)

    在指定的mode下运行当前线程的RunLoop

  • CFRunLoopRunResult CFRunLoopRunInMode(CFStringRef mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled)

CFRunLoopRun

//默认运行runloop的kCFRunLoopDefaultModevoid CFRunLoopRun(void) {   /* DOES CALLOUT */
    int32_t result;    do {        //默认在kCFRunLoopDefaultMode下运行runloop
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

CFRunLoopRun(void) 方法中调用了 CFRunLoopRunSpecific 这个方法,这个方法的mode参数传入了 kCFRunLoopDefaultMode ,即在默认mode下运行RunLoop。

CFRunLoopRunInMode

SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

这个方法最终也是调用了 CFRunLoopRunSpecific ,只不过传入其中的mode是指定的mode而不是默认mode。

这里也可以看出虽然RunLoop可以有多个mode,但是RunLoop在run的时候只能指定一个mode,运行起来之后,被指定的mode即为currentMode。

接下里我们来看一下 CFRunLoopRunSpecific 的具体实现。

CFRunLoopRunSpecific

/*
 * 指定mode运行runloop
 * @param rl 当前运行的runloop
 * @param modeName 需要运行的mode的name
 * @param seconds  runloop的超时时间
 * @param returnAfterSourceHandled 是否处理完事件就返回
 */SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    __CFRunLoopLock(rl);    //根据modeName找到本次运行的mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);    //如果没找到 || mode中没有注册任何事件,则就此停止,不进入循环
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
        Boolean did = false;        if (currentMode) __CFRunLoopModeUnlock(currentMode);
        __CFRunLoopUnlock(rl);        return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
    }
    volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);    //取上一次运行的mode
    CFRunLoopModeRef previousMode = rl->_currentMode;    //如果本次mode和上次的mode一致
    rl->_currentMode = currentMode;    //初始化一个result为kCFRunLoopRunFinished
    int32_t result = kCFRunLoopRunFinished;    
    // 1.通知observer即将进入runloop
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);    //10.通知observer已退出runloop
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    __CFRunLoopModeUnlock(currentMode);
    __CFRunLoopPopPerRunData(rl, previousPerRun);
    rl->_currentMode = previousMode;
    __CFRunLoopUnlock(rl);    return result;
}

通过 CFRunLoopRunSpecific 的内部逻辑,可以看出:

  • 如果指定了一个不存在的mode来运行RunLoop。那么会失败,mode不会被创建,所以这里传入的mode必须是存在的。

  • 如果指定了一共额mode,但是这个mode中不报班任何modeItem,那么RunLoop也不会运行,所以必须要传入至少包含一个modeItem的mode。

  • 在进入RunLoop之前通知observer,状态为kCFRunLoopEntry。

  • 在退出RunLoop之后通知observer,状态为kCFRunLoopExit。

    RunLoop运行的核心函数是 _CFRunLoopRun ,接下来我们分析 _CFRunLoopRun 的源码:

_CFRunLoopRun

/**
 *  运行run loop
 *
 *  @param rl              运行的RunLoop对象
 *  @param rlm             运行的mode
 *  @param seconds         run loop超时时间
 *  @param stopAfterHandle true:run loop处理完事件就退出  false:一直运行直到超时或者被手动终止
 *  @param previousMode    上一次运行的mode
 *
 *  @return 返回4种状态
 */static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {    //获取系统启动后的CPU运行时间,用于控制超时时间
    uint64_t startTSR = mach_absolute_time();    
    //如果RunLoop或者mode是stop状态,则直接return,不进入循环
    if (__CFRunLoopIsStopped(rl)) {
        __CFRunLoopUnsetStopped(rl);        return kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {
        rlm->_stopped = false;        return kCFRunLoopRunStopped;
    }    
    //mach端口,在内核中,消息在端口之间传递。 初始为0
    mach_port_name_t dispatchPort = MACH_PORT_NULL;    //判断是否为主线程
    Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));    //如果在主线程 && runloop是主线程的runloop && 该mode是commonMode,则给mach端口赋值为主线程收发消息的端口
    if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();    
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    mach_port_name_t modeQueuePort = MACH_PORT_NULL;    if (rlm->_queue) {        //mode赋值为dispatch端口_dispatch_runloop_root_queue_perform_4CF
        modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);        if (!modeQueuePort) {
            CRASH("Unable to get port for run loop mode queue (%d)", -1);
        }
    }#endif
    
    //GCD管理的定时器,用于实现runloop超时机制
    dispatch_source_t timeout_timer = NULL;    struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
    
    //立即超时
    if (seconds termTSR = 0ULL;
    }    //seconds为超时时间,超时时执行__CFRunLoopTimeout函数
    else if (seconds ds = timeout_timer;
        timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
        timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
        dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
        dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
        dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);        uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
        dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
        dispatch_resume(timeout_timer);
    }    //永不超时
    else { // infinite timeout
        seconds = 9999999999.0;
        timeout_context->termTSR = UINT64_MAX;
    }    
    //标志位默认为true
    Boolean didDispatchPortLastTime = true;    //记录最后runloop状态,用于return
    int32_t retVal = 0;    do {        //初始化一个存放内核消息的缓冲池
        uint8_t msg_buffer[3 * 1024];        mach_msg_header_t *msg = NULL;        mach_port_t livePort = MACH_PORT_NULL;        //取所有需要监听的port
        __CFPortSet waitSet = rlm->_portSet;        
        //设置RunLoop为可以被唤醒状态
        __CFRunLoopUnsetIgnoreWakeUps(rl);        
        //2.通知observer,即将触发timer回调,处理timer事件
        if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);        //3.通知observer,即将触发Source0回调
        if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);        
        //执行加入当前runloop的block
        __CFRunLoopDoBlocks(rl, rlm);        
        //4.处理source0事件
        //有事件处理返回true,没有事件返回false
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);        if (sourceHandledThisLoop) {            //执行加入当前runloop的block
            __CFRunLoopDoBlocks(rl, rlm);
        }        
        //如果没有Sources0事件处理 并且 没有超时,poll为false
        //如果有Sources0事件处理 或者 超时,poll都为true
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);        
        //第一次do..whil循环不会走该分支,因为didDispatchPortLastTime初始化是true
        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {            //从缓冲区读取消息
            msg = (mach_msg_header_t *)msg_buffer;            //5.接收dispatchPort端口的消息,(接收source1事件)
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0)) {                //如果接收到了消息的话,前往第9步开始处理msg
                goto handle_msg;
            }

        }
        
        didDispatchPortLastTime = false;        
        //6.通知观察者RunLoop即将进入休眠
        if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);        //设置RunLoop为休眠状态
        __CFRunLoopSetSleeping(rl);        // do not do any user callouts after this point (after notifying of sleeping)
        
        // Must push the local-to-this-activation ports in on every loop
        // iteration, as this mode could be run re-entrantly and we don't
        // want these ports to get serviced.
        
        __CFPortSetInsert(dispatchPort, waitSet);
        
        __CFRunLoopModeUnlock(rlm);
        __CFRunLoopUnlock(rl);        

#if USE_DISPATCH_SOURCE_FOR_TIMERS
        //这里有个内循环,用于接收等待端口的消息
        //进入此循环后,线程进入休眠,直到收到新消息才跳出该循环,继续执行run loop
        do {            if (kCFUseCollectableAllocator) {
                objc_clear_stack(0);                memset(msg_buffer, 0, sizeof(msg_buffer));
            }
            msg = (mach_msg_header_t *)msg_buffer;            //7.接收waitSet端口的消息
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);            //收到消息之后,livePort的值为msg->msgh_local_port,
            if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {                // Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
                while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));                if (rlm->_timerFired) {                    // Leave livePort as the queue port, and service timers below
                    rlm->_timerFired = false;                    break;
                } else {                    if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
                }
            } else {                // Go ahead and leave the inner loop.
                break;
            }
        } while (1);#else
        if (kCFUseCollectableAllocator) {
            objc_clear_stack(0);            memset(msg_buffer, 0, sizeof(msg_buffer));
        }
        msg = (mach_msg_header_t *)msg_buffer;
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);#endif
               
        __CFRunLoopLock(rl);
        __CFRunLoopModeLock(rlm);        
        // Must remove the local-to-this-activation ports in on every loop
        // iteration, as this mode could be run re-entrantly and we don't
        // want these ports to get serviced. Also, we don't want them left
        // in there if this function returns.
        
        __CFPortSetRemove(dispatchPort, waitSet);
        
 
        __CFRunLoopSetIgnoreWakeUps(rl);        
        // user callouts now OK again
        //取消runloop的休眠状态
        __CFRunLoopUnsetSleeping(rl);        //8.通知观察者runloop被唤醒
        if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);      
        //9.处理收到的消息
    handle_msg:;
        __CFRunLoopSetIgnoreWakeUps(rl);        

        if (MACH_PORT_NULL == livePort) {
            CFRUNLOOP_WAKEUP_FOR_NOTHING();            // handle nothing
            //通过CFRunloopWake唤醒
        } else if (livePort == rl->_wakeUpPort) {
            CFRUNLOOP_WAKEUP_FOR_WAKEUP();            //什么都不干,跳回2重新循环
            // do nothing on Mac OS
        }#if USE_DISPATCH_SOURCE_FOR_TIMERS
        //如果是定时器事件
        else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();            //9.1 处理timer事件
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {                // Re-arm the next timer, because we apparently fired early
                __CFArmNextTimerInMode(rlm, rl);
            }
        }#endif#if USE_MK_TIMER_TOO
        //如果是定时器事件
        else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();            // On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled.
            // In this case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. adding or removing timers). The fix for the issue is to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754
           //9.1处理timer事件
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {                // Re-arm the next timer
                __CFArmNextTimerInMode(rlm, rl);
            }
        }#endif
        //如果是dispatch到main queue的block
        else if (livePort == dispatchPort) {
            CFRUNLOOP_WAKEUP_FOR_DISPATCH();
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);            //9.2执行block
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
            sourceHandledThisLoop = true;
            didDispatchPortLastTime = true;
        } else {
            CFRUNLOOP_WAKEUP_FOR_SOURCE();            // Despite the name, this works for windows handles as well
            CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);            // 有source1事件待处理
            if (rls) {                mach_msg_header_t *reply = NULL;                //9.2 处理source1事件
                sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;                if (NULL != reply) {
                    (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
                    CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
                }

            }
        }        if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
        
        __CFRunLoopDoBlocks(rl, rlm);        
        if (sourceHandledThisLoop && stopAfterHandle) {            //进入run loop时传入的参数,处理完事件就返回
            retVal = kCFRunLoopRunHandledSource;
        }else if (timeout_context->termTSR < mach_absolute_time()) {            //run loop超时
            retVal = kCFRunLoopRunTimedOut;
        }else if (__CFRunLoopIsStopped(rl)) {            //run loop被手动终止
            __CFRunLoopUnsetStopped(rl);
            retVal = kCFRunLoopRunStopped;
        }else if (rlm->_stopped) {            //mode被终止
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        }else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {            //mode中没有要处理的事件
            retVal = kCFRunLoopRunFinished;
        }        //除了上面这几种情况,都继续循环
    } while (0 == retVal);    
    if (timeout_timer) {
        dispatch_source_cancel(timeout_timer);
        dispatch_release(timeout_timer);
    } else {        free(timeout_context);
    }    
    return retVal;
}

下面我们再来一部分一部分看:

Boolean poll = sourceHandledThisLoop || (0LL == timeout_context->termTSR);        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {

            msg = (mach_msg_header_t *)msg_buffer;            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), 0)) {                goto handle_msg;
            }

        }

上面代码是 main queue 可能被本次 RunLoop 执行的一个机会,可以看到 if 语句里还加入了 didDispatchPortLastTime 这个变量,该变量作用很像是获取上次 RunLoop 有没有执行过 main queue 的标志,假如 handle_msg 执行了 main queue, didDispatchPortLastTime 会被设为 true,这样在下次 RunLoop ,!didDispatchPortLastTime 为 false,不会直接跳转执行 handle_msg。

但假如 handle_msg 执行了其他的分支(比如 timer),那么本次 RunLoop 将不再执行 main queue 了(即便有),来到下次 RunLoop 时,由于!didDispatchPortLastTime 为 true,如果有 main queue 的代码要执行,就会直接跳转到 handle_msg 处理 main queue,略过 RunLoop 休眠等代码。

可以看出系统之所以这么做是为了确保加进来的 main queue 能获得快速执行和跳过界面更新和休眠提升效率。

    if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
    __CFRunLoopSetSleeping(rl);    // do not do any user callouts after this point (after notifying of sleeping)

        // Must push the local-to-this-activation ports in on every loop
        // iteration, as this mode could be run re-entrantly and we don't
        // want these ports to get serviced.

        __CFPortSetInsert(dispatchPort, waitSet);

    __CFRunLoopModeUnlock(rlm);
    __CFRunLoopUnlock(rl);

__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting) 这一步实际上还会进行界面的刷新。

kCFRunLoopBeforeSources 为起点到 kCFRunLoopBeforeWaiting 休眠前,这其中处理了大量的工作-执行block,处理source0,更新界面等等。做完这些之后RunLoop就休眠了,直到RunLoop被timer,source,libdispatch唤醒,唤醒后会发送休眠结束的 kCFRunLoopAfterWaiting 通知。 我们知道屏幕的刷新率是60FPS,即1/60s约等于16ms,假如RunLoop超过了这个时间,就可能掉帧,UI更新就有可能出现卡顿,BeforeSources到AfterWaiting可以粗略认为是一次RunLoop的起止。所以这段时间可以被我们拿来作为卡顿检测。

_CFRunLoopServiceMachPort

第7步调用了__CFRunLoopServiceMachPort函数,这个函数在run loop中起到了至关重要的作用:

/**
 *  接收指定内核端口的消息
 *
 *  @param port        接收消息的端口
 *  @param buffer      消息缓冲区
 *  @param buffer_size 消息缓冲区大小
 *  @param livePort    暂且理解为活动的端口,接收消息成功时候值为msg->msgh_local_port,超时时为MACH_PORT_NULL
 *  @param timeout     超时时间,单位是ms,如果超时,则RunLoop进入休眠状态
 *
 *  @return 接收消息成功时返回true 其他情况返回false
 */static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout) {
    Boolean originalBuffer = true;
    kern_return_t ret = KERN_SUCCESS;    for (;;) {      /* In that sleep of death what nightmares may come ... */
        mach_msg_header_t *msg = (mach_msg_header_t *)*buffer;
        msg->msgh_bits = 0;  //消息头的标志位
        msg->msgh_local_port = port;  //源(发出的消息)或者目标(接收的消息)
        msg->msgh_remote_port = MACH_PORT_NULL; //目标(发出的消息)或者源(接收的消息)
        msg->msgh_size = buffer_size;  //消息缓冲区大小,单位是字节
        msg->msgh_id = 0;  //唯一id
       
        if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); }        
        //通过mach_msg发送或者接收的消息都是指针,
        //如果直接发送或者接收消息体,会频繁进行内存复制,损耗性能
        //所以XNU使用了单一内核的方式来解决该问题,所有内核组件都共享同一个地址空间,因此传递消息时候只需要传递消息的指针
        ret = mach_msg(msg,
                       MACH_RCV_MSG|MACH_RCV_LARGE|((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0)|MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)|MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV),                       0,
                       msg->msgh_size,
                       port,
                       timeout,
                       MACH_PORT_NULL);
        CFRUNLOOP_WAKEUP(ret);        
        //接收/发送消息成功,给livePort赋值为msgh_local_port
        if (MACH_MSG_SUCCESS == ret) {
            *livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;            return true;
        }        
        //MACH_RCV_TIMEOUT
        //超出timeout时间没有收到消息,返回MACH_RCV_TIMED_OUT
        //此时释放缓冲区,把livePort赋值为MACH_PORT_NULL
        if (MACH_RCV_TIMED_OUT == ret) {            if (!originalBuffer) free(msg);
            *buffer = NULL;
            *livePort = MACH_PORT_NULL;            return false;
        }        
        //MACH_RCV_LARGE
        //如果接收缓冲区太小,则将过大的消息放在队列中,并且出错返回MACH_RCV_TOO_LARGE,
        //这种情况下,只返回消息头,调用者可以分配更多的内存
        if (MACH_RCV_TOO_LARGE != ret) break;        //此处给buffer分配更大内存
        buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);        if (originalBuffer) *buffer = NULL;
        originalBuffer = false;
        *buffer = realloc(*buffer, buffer_size);
    }
    HALT;    return false;
}

小结

RunLoop实际就是一个对象,它和线程是一对一的,每个线程都有一个对应的RunLoop对象,主线程的RunLoop会在程序启动时自动创建,子线程需要手动来获取创建。

RunLoop运行的核心是一个do..while循环,遍历所有需要处理的事件,如果有事件处理就让线程工作,没有事件处理则让线程休眠,同时等待事件到来。

RunLoop的应用

在开发过程中几乎所有的操作都是通过Call out进行回调的(无论是Observer的状态通知还是Timer、Source的处理),而系统在回调时通常使用如下几个函数进行回调(换句话说你的代码其实最终都是通过下面几个函数来负责调用的,即使你自己监听Observer也会先调用下面的函数然后间接通知你,所以在调用堆栈中经常看到这些函数):

    static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();    static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();

实际代码如下:

{    /// 1. 通知Observers,即将进入RunLoop
    /// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);    do { 
        /// 2. 通知 Observers: 即将触发 Timer 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);        /// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block); 
        /// 4. 触发 Source0 (非基于port的) 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block); 
        /// 6. 通知Observers,即将进入休眠
        /// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting); 
        /// 7. sleep to wait msg.
        mach_msg() -> mach_msg_trap();        
 
        /// 8. 通知Observers,线程被唤醒
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting); 
        /// 9. 如果是被Timer唤醒的,回调Timer
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer); 
        /// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block); 
        /// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
 
 
    } while (...); 
    /// 10. 通知Observers,即将退出RunLoop
    /// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}

为了验证上面的说法,我们在源码中找到 CFRunLoopDoObservers() 这个方法,看看其实现:

static void __CFRunLoopDoObservers(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopActivity activity) { /* DOES CALLOUT */
    CHECK_FOR_FORK();    CFIndex cnt = rlm->_observers ? CFArrayGetCount(rlm->_observers) : 0;    if (cnt < 1) return;    /* Fire the observers */
    STACK_BUFFER_DECL(CFRunLoopObserverRef, buffer, (cnt <= 1024) ? cnt : 1);    CFRunLoopObserverRef *collectedObservers = (cnt <= 1024) ? buffer : (CFRunLoopObserverRef *)malloc(cnt * sizeof(CFRunLoopObserverRef));    CFIndex obs_cnt = 0;    for (CFIndex idx = 0; idx < cnt; idx++) {        CFRunLoopObserverRef rlo = (CFRunLoopObserverRef)CFArrayGetValueAtIndex(rlm->_observers, idx);        if (0 != (rlo->_activities & activity) && __CFIsValid(rlo) && !__CFRunLoopObserverIsFiring(rlo)) {
            collectedObservers[obs_cnt++] = (CFRunLoopObserverRef)CFRetain(rlo);
        }
    }
    __CFRunLoopModeUnlock(rlm);
    __CFRunLoopUnlock(rl);    for (CFIndex idx = 0; idx < obs_cnt; idx++) {        CFRunLoopObserverRef rlo = collectedObservers[idx];
        __CFRunLoopObserverLock(rlo);        if (__CFIsValid(rlo)) {
            Boolean doInvalidate = !__CFRunLoopObserverRepeats(rlo);
            __CFRunLoopObserverSetFiring(rlo);
            __CFRunLoopObserverUnlock(rlo);            
//执行回调__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(rlo->_callout, rlo, activity, rlo->_context.info);            if (doInvalidate) {                CFRunLoopObserverInvalidate(rlo);
            }
            __CFRunLoopObserverUnsetFiring(rlo);
        } else {
            __CFRunLoopObserverUnlock(rlo);
        }        CFRelease(rlo);
    }
    __CFRunLoopLock(rl);
    __CFRunLoopModeLock(rlm);    if (collectedObservers != buffer) free(collectedObservers);
}

我们看到,其最终是执行了 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(rlo->_callout, rlo, activity, rlo->_context.info); 这个回调方法。

NSTimer与GCD Timer,CADisplayLink

NSTimer

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

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

NSTimer不是特别准确的原因:

  • NSTimer添加到RunLoop中默认是添加到 NSDefaultRunLoopMode 这种mode中。,但是当我们拖动UISCrollview时,RunLoop的mode是切换到了 NSEventTrackingRunLoopMode 这种mode下,这样NSTimer就不会执行了。

  • NSTimer并不会在非常准确的时间点去唤醒RunLoop,这也是为了节省资源。

  • 当前线程中有很多耗时操作要处理时,就可能不会准时的去执行timer的回调。

GCD Timer

GCD Timer是通过Dispatch port给RunLoop发送消息,来使RunLoop执行响应的block,如果所在线程没有RunLoop,那么GCD会临时创建一个线程去执行block,执行完了再销毁,因此GCD的timer是不依赖RunLoop的。

至于两个Timer的准确性问题,如果不再RunLoop的线程里面执行,那么只能使用GCD Timer,由于GCD Timer是基于MTimer,已经很底层了,因此是很底层的。

如果在RunLoop的线程里面执行,由于GCD Timer和NSTimer都是通过port发送消息的机制来触发RunLoop的,因此准确性差别应该不是很大,如果线程RunLoop阻塞了,它们都会有延迟问题。

CADisplayLink

CADisplayLink是一个执行频率(fps)和屏幕刷新相同(可以修改preferredFramesPerSecond改变刷新频率)的定时器,它也需要加入到RunLoop才能执行。与NSTimer类似,CADisplayLink同样是基于CFRunloopTimerRef实现,底层使用mk_timer(可以比较加入到RunLoop前后RunLoop中timer的变化)。和NSTimer相比它精度更高(尽管NSTimer也可以修改精度),不过和NStimer类似的是如果遇到大任务它仍然存在丢帧现象。通常情况下CADisaplayLink用于构建帧动画,看起来相对更加流畅,而NSTimer则有更广泛的用处。

AutoreleasePool

AutoreleasePool是另一个与RunLoop相关讨论较多的话题。其实从RunLoop源码分析,autoreleasepool与RunLoop并没有直接的关系,之所以将两个话题放到一起讨论最主要的原因是因为在iOS应用启动后会注册两个observer管理和维护AutoreleasePool。在应用程序刚刚启动时打印currentRunLoop可以看到系统默认注册了很多observer,其中有两个observer的callout都是 _ wrapRunLoopWithAutoreleasePoolHandler ,这两个是和自动释放池相关的监听。

{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = {type = mutable-small, count = 0, values = ()}}{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = {type = mutable-small, count = 0, values = ()}}

第一个observer会监听RunLoop的进入,它会调用 objc_autoreleasePoolPush() 向当前的 AutoreleasePoolPage 增加一个哨兵对象标志创建自动释放池。这个observer的order是-2147483647,优先级最高,确保发生在所有回到操作发生之前。第二个observer会监听RunLoop进入休眠和即将退出RunLoop两种状态,在即将进入休眠时会调用 objc_autoreleasePoolPop()objc_autoreleasePoolPush() 根据情况从最先加入的对象往前清理直到遇到哨兵对象,然后创建一个新的自动释放池。而在即将退出时会调用 objc_autoreleasePoolPop() 释放自动释放池内的对象。这个observer的order是2147483647,优先级最低,确保发生在所有回调操作之后。

自动释放池的创建和释放,销毁的时机如下所示:

  • kCFRunLoopEntry;        //进入RunLoop之前,创建一个自动释放池

  • kCFRunLoopBeforeWaiting; //休眠之前,销毁自动释放池,创建一个新的自动释放池

  • kCFRunLopExit;           //退出RunLoop之前,销毁自动释放池

事件响应

苹果注册了一个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 事件都是在这个回调中完成的。

手势识别

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

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

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

UI更新

App在启动之后的主线程RunLoop中注册了监听主线程即将进入休眠和退出状态的observer,这个observer的回调是 _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv ,它专门负责UI变化后的更新,比如修改了frame,调整了UI层级,或者手动设置了setNeedslayout/setneedsdisplay之后会将这些操作提交到全局容器。这个observer一旦监听到主线程RunLoop的即将进入休眠和退出状态,就会遍历所有的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];

通常情况下这种方式是完美的,因为除了系统的更新,还可以利用setNeedsDisplay等方法手动触发下一次RunLoop运行的更新。但是如果当前正在执行大量的逻辑运算可能UI的更新就会比较卡,因此facebook推出了 AsyncDisplayKit 来解决这个问题。AsyncDisplayKit其实是将UI排版和绘制运算尽可能放到后台,将UI的最终更新操作放到主线程(这一步也必须在主线程完成),同时提供一套类UIView或CALayer的相关属性,尽可能保证开发者的开发习惯。这个过程中AsyncDisplayKit在主线程RunLoop中增加了一个Observer监听即将进入休眠和退出RunLoop两种状态,收到回调时遍历队列中的待处理任务一一执行。

GCD和RunLoop的关系

在RunLoop的源代码中可以看到用到了相关GCD的内容,但是RunLoop本身和GCD并没有直接的滚西。当调用了 dispatch_async(dispatch_get_main_queue(), ^(void)block) 时libDispatch会向主线程RunLoop发送消息唤醒RunLoop,RunLoop从消息中获取block,并且在 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ 回调里执行这个block。不过这个操作仅限于主线程,其他线程dispatch操作全部由libDispatch驱动。

更多RunLoop实践

滚动UIScrollview导致定时器失败

在界面上有一个UIScrollview控件,如果此时还有一个定时器在执行一个事件,你会发现当你滚动UIScrollview的时候,定时器会失效。

因为当你滚动UIScrollview控件的时候,RunLoop会切换到 UITrackingRunLoopMode 模式,而定时器运行在defaultMode下面,系统一次只能处理一种模式下的RunLoop,导致定时器失效。

解决办法:

  • 把timer注册到 NSRunLoopCommonModes ,它包含了defaultMode和trackingMode两种模式:

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
  • 使用GCD创建定时器,GCD创建的定时器不受RunLoop的影响:

    // 获得队列
    dispatch_queue_t queue = dispatch_get_main_queue();    
    // 创建一个定时器(dispatch_source_t本质还是个OC对象)
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);    
    // 设置定时器的各种属性(几时开始任务,每隔多长时间执行一次)
    // GCD的时间参数,一般是纳秒(1秒 == 10的9次方纳秒)
    // 比当前时间晚1秒开始执行
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));    
    //每隔一秒执行一次
    uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
    dispatch_source_set_timer(self.timer, start, interval, 0);    
    // 设置回调
    dispatch_source_set_event_handler(self.timer, ^{        NSLog(@"------------%@", [NSThread currentThread]);

    });    
    // 启动定时器
    dispatch_resume(self.timer);

图片下载

由于图片渲染到屏幕需要较多的资源,为了提高用户体验,当用户滚动tableview的时候,只在后台下载图片,但是不显示图片,当用户停下里的时候才显示图片。

[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"imgName"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];

上面的代码可以达到:当用户点击屏幕之后,如果此时用户又开始滚动tableview,那么就算过了三秒,图片也不会显示出来,用户停止了滚动,才显示图片。

常驻线程

需要创建一个在后台一直存在的线程,来做一下需要频繁处理的任务。比如检测网络状态等。

默认情况下一个线程创建出来,运行完要做的事情,线程就会消亡。、而程序启动时创建的主线程已经加入到RunLoop,所以主线程不会消亡。

这个时候我们就需要把自己创建的线程加到RunLoop中来,就可以实现线程后台常驻。

- (void)viewDidLoad {
    [super viewDidLoad];    
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [self.thread start];
}

- (void)run
{    NSLog(@"----------run----%@", [NSThread currentThread]);    @autoreleasepool{    /*如果不加这句,会发现runloop创建出来就挂了,因为runloop如果没有CFRunLoopSourceRef事件源输入或者定时器,就会立马消亡。
      下面的方法给runloop添加一个NSport,就是添加一个事件源,也可以添加一个定时器,或者observer,让runloop不会挂掉*/
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];    
    // 方法1 ,2,3实现的效果相同,让runloop无限期运行下去
    [[NSRunLoop currentRunLoop] run];
   }    
    // 方法2
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];    
    // 方法3
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];    
    NSLog(@"---------");
}

- (void)test
{    NSLog(@"----------test----%@", [NSThread currentThread]);
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

如果没有实现添加NSPort或者NSTimer,会发现执行完run方法,线程就会消亡,后续再执行touchbegan方法无效。

我们必须保证线程不消亡,才可以在后台接受时间处理

RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 RunLoop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。

可以发现执行完了run方法,这个时候再点击屏幕,可以不断执行test方法,因为线程self.thread一直常驻后台,等待事件加入其中,然后执行。

观察事件状态,优化性能

假设我们想实现cell的高度缓存计算,因为“计算cell的预缓存高度”的任务需要在最无感的时候进行,所以应该同时满足:

  • RunLoop处于“空闲”状态

  • 当这一次RunLoop迭代处理完成了所有事件,马上要休眠时

CFRunLoopRef runLoop = CFRunLoopGetCurrent();CFStringRef runLoopMode = kCFRunLoopDefaultMode;CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {    // TODO here});CFRunLoopAddObserver(runLoop, observer, runLoopMode);
在其中的 TODO 位置,就可以开始任务的收集和分发了,当然,不能忘记适时的移除这个 observer

注:由于RunLoop写的人很多,我个人水平有限不可能写的像大牛那么好,因此这篇博客主要是摘抄的大牛的博客,以加深印象。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK