runloop

Runloop
一、简介

runloop本质是一个循环,保证保证线程不退出,实现了“闲”等待,有任务的时候唤醒线程处理任务,没任务的时候让线程休眠,它的工作模式如下图所示

Runloop

对于开发者来说,runloop是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行循环逻辑,线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。

​ 正是因为这个内部循环iOS应用在main函数执行完之后还可以一直运行,下面是iOS应用的main函数,在UIApplicationMain()函数会开启主线程的runloop,保持应该可以接收消息,处理消息。

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
二、结构

1、RunLoop数据结构
RunLoop其实就是一个对象,它的结构如下,主要包含了一个线程,若干个Mode,若干个commonMode,还有一个当前运行的Mode

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;
};

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

一个runloop对象包含若干个modes,每个mode又包含若干个Source/Timer/Observer以及port,由结构可以看出runloop管理mode,mode管理着事件。runloop结构中_wakeUpPort有消息唤醒对应线程_pthread的runloop后,在currentMode模式下的mode去处理事件,currentMode根据自己结构中的时间类型、timer处理事件,并调用observer

注意:当切换Mode时必须退出当前Mode,然后重新进入Runloop以保证不同Mode的Source/Timer/Observer互不影响。

2、RunLoop 对外的接口

在 CoreFoundation 里面关于 RunLoop 有5个类,如下,接下来我们分别看一下这几个结构

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

(1)CFRunLoopSourceRef
CFRunLoopSourceRef是事件产生的地方,它的结构如下

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

从上面CFRunLoolSource的结构中我们看到,Source有两个版本,分别是Source0 和 Source1,对应着CFRunLoopSourceContext version0和CFRunLoopSourceContext1 version1,它们的结构如下

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

//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;
  • Source0 ,不基于Port,只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
  • Source1 包含了一个 mach_port 和一个回调(函数指针),所以它可以用于通过内核和其他线程相互发送消息,也可以监听系统端口,处理端口消息,这种 Source 能主动唤醒 RunLoop 的线程。

(2)CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-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 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

(3)CFRunLoopObserverRef 是观察者,用于观察RunLoop的各种状态,抛出回调

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 中包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调感受到这个变化。可以观测的时间点有以下几个:

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
};

上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

CFRunLoopRef : CFRunLoopRef 类对CFRunLoopModeRef 类进行了封装,CFRunLoopModeRef 类并没有对外暴露,他们关系如下图。runloop包含若干个modes,每个mode又包含若干个Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入

Runloop Mode
3、runloop的modes

官方文档中介绍了5个,分别是

  • UIInitializationRunLoopMode: 在刚启动 App 时进入的第一个 Mode,启动完成后就不再使用。
  • kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行
  • UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
  • GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
  • kCFRunLoopCommonModes: 这是一个Mode的组合,包含了NSDefaultRunLoopMode UITrackingRunLoopMode。
三、RunLoop相关API的底层实现

可以选择苹果开源代码学习,地址是 https://opensource.apple.com/source/CF
1、令RunLoop运行运行的API

(1) CFRunLoopRun
在默认的mode下运行当前线程的RunLoop。

void 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中调用了CFRunLoopRunSpecific(),参数传入当前RunLoop对象,modeName参数传入kCFRunLoopDefaultMode

(2) CFRunLoopRunInMode
使用该函数可以在指定mode下运行当前线程的RunLoop。函数原型如下

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

具体实现如下

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

在CFRunLoopRunInMode()中也调用了CFRunLoopRunSpecific(),参数传入当前RunLoop对象,modeName参数继续传递CFRunLoopRunInMode传入的modeName.

(3)CFRunLoopSpecific()
CFRunLoopRun()和CFRunLoopInMode()函数内部都调用了CFRunLoopSpecific,所以我们看一下CFRunLoopSpecific的实现

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    //判断runloop是否正在销毁
    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    //锁住CFRunLoopRef,防止其他线程修改
    __CFRunLoopLock(rl);
    //根据modeName,找到对应的mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    //对于model进行判空,异常处理
    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);
    CFRunLoopModeRef previousMode = rl->_currentMode;
    rl->_currentMode = currentMode;
    int32_t result = kCFRunLoopRunFinished;

//告诉外面的监听者obsevers,进入runloop
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);

//核心代码
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);


//告诉外面的监听者obsevers,退出runloop
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    __CFRunLoopModeUnlock(currentMode);
    __CFRunLoopPopPerRunData(rl, previousPerRun);
    rl->_currentMode = previousMode;
    __CFRunLoopUnlock(rl);
    return result;
}

下面我们看一下核心代码_CFRunLoopRun的具体实现

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    uint64_t startTSR = mach_absolute_time();
    
    if (__CFRunLoopIsStopped(rl)) {
        __CFRunLoopUnsetStopped(rl);
        return kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {
        rlm->_stopped = false;
        return kCFRunLoopRunStopped;
    }
 //以上部分依旧是状态的判断,runloop停止了会直接返回一个停止状态
//-------------------------------------------------------------------


 /*
  *
  * source 分为 source0、source1
  * 这里的端口port的事件和消息是通过source1来处理的
  *
  */
    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)));
    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) {
        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
 //以上部分为端口port的获取,port一般用于线程之间的通信
//--------------------------------------------------------------




 /*
  *
  * runloop的循环时间设置
  * 可以看到使用了GCD(触及内核)的定时器
  * 设置循环时间接近无限
  * 定时器NSTimer是CFRunloopTimerRef的封装
  * 所以GCD的定时器会更加精确
  */
    dispatch_source_t timeout_timer = NULL;
    struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
    if (seconds <= 0.0) { // instant timeout
        seconds = 0.0;
        timeout_context->termTSR = 0ULL;
    } else if (seconds <= TIMER_INTERVAL_LIMIT) {
        dispatch_queue_t queue = pthread_main_np() ? __CFDispatchQueueGetGenericMatchingMain() : __CFDispatchQueueGetGenericBackground();
        timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        dispatch_retain(timeout_timer);
        timeout_context->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;
    }
    
    Boolean didDispatchPortLastTime = true;
    int32_t retVal = 0;
//以上部分为timer的设置
//--------------------------------------------------------------



 /*
  *
  * 开始一个do循环,准备开始处理事件
  * 参杂一些预编译的宏
  * 
  *
  * 
  */
    do {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        voucher_mach_msg_state_t voucherState = VOUCHER_MACH_MSG_STATE_UNCHANGED;
        voucher_t voucherCopy = NULL;
#endif
        uint8_t msg_buffer[3 * 1024];
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        mach_msg_header_t *msg = NULL;
        mach_port_t livePort = MACH_PORT_NULL;
#elif DEPLOYMENT_TARGET_WINDOWS
        HANDLE livePort = NULL;
        Boolean windowsMessageReceived = false;
#endif
        __CFPortSet waitSet = rlm->_portSet;
        
        __CFRunLoopUnsetIgnoreWakeUps(rl);

 /*
  *
  * 核心部分
  * 准备处理timer
  * 准备处理source
  * 
  *
  */   
//准备处理timer
        if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
//准备处理source
        if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
     
        __CFRunLoopDoBlocks(rl, rlm); //执行   
        
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {
            __CFRunLoopDoBlocks(rl, rlm);
        }
        
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
        
        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI

//开始处理事件了,可以发现gcd和runloop是协同工作的关系
            msg = (mach_msg_header_t *)msg_buffer;
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
      
               //如果gdc的端口有消息来了处理消息,没有的话往下执行,就去休息了
                goto handle_msg;
            }
#elif DEPLOYMENT_TARGET_WINDOWS
            if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
                goto handle_msg;
            }
#endif
        }
        

//休息
        didDispatchPortLastTime = false;
        
        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);
        
        CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();
        
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        do {
            if (kCFUseCollectableAllocator) {
                // objc_clear_stack(0);
                // <rdar://problem/16393959>
                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, &voucherState, &voucherCopy);
 

//醒了,跳出循环
//下面的预处理基本也是 休息与唤醒的处理           
            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);
            // <rdar://problem/16393959>
            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, &voucherState, &voucherCopy);
#endif
        
        
#elif DEPLOYMENT_TARGET_WINDOWS
        // Here, use the app-supplied message queue mask. They will set this if they are interested in having this run loop receive windows messages.
        __CFRunLoopWaitForMultipleObjects(waitSet, NULL, poll ? 0 : TIMEOUT_INFINITY, rlm->_msgQMask, &livePort, &windowsMessageReceived);
#endif
        
        __CFRunLoopLock(rl);
        __CFRunLoopModeLock(rlm);
        
        rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));
        
        // 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
        __CFRunLoopUnsetSleeping(rl);


//醒来了,通知观察者observers
        if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
        
    handle_msg:;//跳转到这处理消息
        __CFRunLoopSetIgnoreWakeUps(rl);



//开始处理消息 
#if DEPLOYMENT_TARGET_WINDOWS
        if (windowsMessageReceived) {
            // These Win32 APIs cause a callout, so make sure we're unlocked first and relocked after
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            
            if (rlm->_msgPump) {
                rlm->_msgPump();
            } else {
                MSG msg;


//获取消息、转译、处理,类似window
                if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE | PM_NOYIELD)) {  
                    TranslateMessage(&msg);
                    DispatchMessage(&msg);
                }
            }
            
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
            sourceHandledThisLoop = true;
            
            // To prevent starvation of sources other than the message queue, we check again to see if any other sources need to be serviced
            // Use 0 for the mask so windows messages are ignored this time. Also use 0 for the timeout, because we're just checking to see if the things are signalled right now -- we will wait on them again later.
            // NOTE: Ignore the dispatch source (it's not in the wait set anymore) and also don't run the observers here since we are polling.
            __CFRunLoopSetSleeping(rl);
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            
            __CFRunLoopWaitForMultipleObjects(waitSet, NULL, 0, 0, &livePort, NULL);
            
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
            __CFRunLoopUnsetSleeping(rl);
            // If we have a new live port then it will be handled below as normal
        }
        
        

//识别端口的类型,因为分主动唤醒、被动唤醒、定时器唤醒的等状态,也就是识别处理不同类型的消息
#endif
        if (MACH_PORT_NULL == livePort) {
            CFRUNLOOP_WAKEUP_FOR_NOTHING();
            // handle nothing
        } else if (livePort == rl->_wakeUpPort) {
            CFRUNLOOP_WAKEUP_FOR_WAKEUP();
            // do nothing on Mac OS
#if DEPLOYMENT_TARGET_WINDOWS
            // Always reset the wake up port, or risk spinning forever
            ResetEvent(rl->_wakeUpPort);
#endif
        }
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
            CFRUNLOOP_WAKEUP_FOR_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
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                // Re-arm the next timer
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
        else if (livePort == dispatchPort) {
            CFRUNLOOP_WAKEUP_FOR_DISPATCH();
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
#if DEPLOYMENT_TARGET_WINDOWS
            void *msg = 0;
#endif
            __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();
            
            // If we received a voucher from this mach_msg, then put a copy of the new voucher into TSD. CFMachPortBoost will look in the TSD for the voucher. By using the value in the TSD we tie the CFMachPortBoost to this received mach_msg explicitly without a chance for anything in between the two pieces of code to set the voucher again.
            voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release);
            
            // Despite the name, this works for windows handles as well
            CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
            if (rls) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
                mach_msg_header_t *reply = NULL;


//事件、port 都是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);
                }
#elif DEPLOYMENT_TARGET_WINDOWS
                sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
#endif
            }
            
            // Restore the previous voucher
            _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release);
            
        }
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
#endif
        
        __CFRunLoopDoBlocks(rl, rlm);
        
        
        if (sourceHandledThisLoop && stopAfterHandle) {
            retVal = kCFRunLoopRunHandledSource;
        } else if (timeout_context->termTSR < mach_absolute_time()) {
            retVal = kCFRunLoopRunTimedOut;
        } else if (__CFRunLoopIsStopped(rl)) {
            __CFRunLoopUnsetStopped(rl);
            retVal = kCFRunLoopRunStopped;
        } else if (rlm->_stopped) {
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            retVal = kCFRunLoopRunFinished;
        }
        
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        voucher_mach_msg_revert(voucherState);
        os_release(voucherCopy);
#endif
        
    } while (0 == retVal);
    
    if (timeout_timer) {
        dispatch_source_cancel(timeout_timer);
        dispatch_release(timeout_timer);
    } else {
        free(timeout_context);
    }
    
    return retVal;
}

blob.png

四、获取该对象

iOS提供了两个对象,分别是NSRunloop和CFRunloopRef,CFRunloopRef是纯C的函数,API是线程安全的,而NSRunloop仅仅是CFRunloopRef的OC封装,API不是线程安全的。从苹果允许我们直接创建RunLoop对象,只能通过以下几个函数来获取RunLoop:

  • CFRunLoopRef CFRunLoopGetCurrent(void)
  • CFRunLoopRef CFRunLoopGetMain(void)
  • +(NSRunLoop *)currentRunLoop
  • +(NSRunLoop *)mainRunLoop

前两个是Core Foundation中的API,后两个是Foundation中的API。接下来我们通过看看CFRunLoopGetMain()和CFRunLoopGetCurrent()的实现(mainRunLoop和currentRunLoop是对上面两个函数的封装),探究RunLoop创建过程。

//取当前所在线程的RunLoop--CFRunLoopGetCurrent
CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    //传入当前线程
    return _CFRunLoopGet0(pthread_self());
}

在CFRunLoopGetCurrent内部调用了_CFRunLoopGet0(),传入的参数是当前线程pthread_self(),即CFRunLoopGetCurrent函数必须要在线程内部调用,才能获取当前线程的RunLoop,也就是说子线程的RunLoop必须要在子线程内部获取

//取主线程的RunLoop--CFRunLoopGetMain
CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    static CFRunLoopRef __main = NULL; // no retain needed
    //传入主线程
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main;
}

CFRunLoopGetMain内部也调用了_CFRunLoopGet0(),传入的参数是主线程pthread_main_thread_np(),即CFRunLoopGetMain()不管在主线程还是子线程中调用,都可以获取到主线程的RunLoop。

//CFRunLoopGet0
static CFMutableDictionaryRef __CFRunLoops = NULL;
static CFSpinLock_t loopsLock = CFSpinLockInit;
//根据线程取RunLoop
CF_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;
}

通过看CFRunLoopGet0的实现可以得出以下结论:

  • RunLoop和线程的一一对应的,对应的方式是以key-value的方式保存在一个全局字典中
  • 主线程的RunLoop会在初始化全局字典时创建 ,主线程的runloop默认开启
  • 子线程的runloop默认不开启,如果不获取的话就一直不会被创建,如果需要开启,使用currentRunloop获取,系统会开启对应的runloop
  • RunLoop会在线程销毁时销毁
五、苹果用 RunLoop 实现的功能

当 RunLoop 进行回调时,一般都是通过一个很长的函数调用出去 (call out), 当你在你的代码中下断点调试时,通常能在调用栈上看到这些函数。下面是这几个函数的整理版本,如果你在调用栈中看到这些长函数名,在这里查找一下就能定位到具体的调用地点了:

{
    /// 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);
}

1)AutoreleasePool

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 了。

2)事件响应

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

3)手势识别

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

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

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

4)界面更新

当在操作 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];

5)定时器

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

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

CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop,这个稍后我会再单独写一页博客来分析。

6)PerformSelecter

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

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

7)关于GCD

实际上 RunLoop 底层也会用到 GCD 的东西,比如 RunLoop 是用 dispatch_source_t 实现的 Timer(评论中有人提醒,NSTimer 是用了 XNU 内核的 mk_timer,我也仔细调试了一下,发现 NSTimer 确实是由 mk_timer 驱动,而非 GCD 驱动的)。但同时 GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。

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

8)关于网络请求

iOS 中,关于网络请求的接口自下至上有如下几层:

CFSocket
CFNetwork       ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession    ->AFNetworking2, Alamofire

• CFSocket 是最底层的接口,只负责 socket 通信。
• CFNetwork 是基于 CFSocket 等接口的上层封装,ASIHttpRequest 工作于这一层。
• NSURLConnection 是基于 CFNetwork 的更高层的封装,提供面向对象的接口,AFNetworking 工作于这一层。
• NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底层仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 线程),AFNetworking2 和 Alamofire 工作于这一层。

下面主要介绍下 NSURLConnection 的工作过程。

通常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0 (即需要手动触发的Source)。CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 的。

当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。

RunLoop_network

NSURLConnectionLoader 中的 RunLoop 通过一些基于 mach port 的 Source 接收来自底层 CFSocket 的通知。当收到通知后,其会在合适的时机向 CFMultiplexerSource 等 Source0 发送通知,同时唤醒 Delegate 线程的 RunLoop 来让其处理这些通知。CFMultiplexerSource 会在 Delegate 线程的 RunLoop 对 Delegate 执行实际的回调。

六、常见问题
1)runloop和线程的关系

苹果不允许我们自己创建 Runloop 对象,但是可以获取到系统提供的 Runloop 对象,主线程的 Runloop 会在应用启动的时候完成启动,其他线程的 Runloop 默认并不会启动,需要我们手动启动。我们分析一下他们的内部实现,看看线程和runloop的关系。

CFRunLoopGetMain()
CFRunLoopGetCurrent()

上面两个函数的实现如下

CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    static CFRunLoopRef __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) return rl;
    return _CFRunLoopGet0(pthread_self());
}

//可以看出CFRunLoopGetMain()和CFRunLoopGetCurrent()都调用了_CFRunLoopGet0()
//我们看看_CFRunLoopGet0()的内部实现
static CFMutableDictionaryRef __CFRunLoops = NULL;
static CFLock_t loopsLock = CFLockInit;
static pthread_t kNilPthreadT = (pthread_t)0;

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
   //如果t是0,表明是主线程,把主线程赋值给t
    if (pthread_equal(t, kNilPthreadT)) 
    {
	    t = pthread_main_thread_np();
    }
    //线程和runloop的对应是存放在全局字典中,所以访问全局字典使用的锁
    __CFLock(&loopsLock);
    //第一次获取时,没有全局字典,初始化全局字典,并且为主线程创建一个runloop,将对应关系保存到全局字典__CFRunLoops中
    if (!__CFRunLoops) 
{
        __CFUnlock(&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);
        __CFLock(&loopsLock);
    }
    //从_CFRunLoops字典中获取对应线程的runloop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    
    if (!loop) 
    {
         //字典中没有找到线程的runloop,直接创建
	      CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
        //在检查一次,如果字典中没有将新创建的runloop和线程保存到全局字典中
	      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
        __CFUnlock(&loopsLock);
	      CFRelease(newLoop);
    }
    if (pthread_equal(t, pthread_self())) 
    {
       // 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) 
        {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

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

2)runloop的时间源

从简介中runloop的工作模式种,可以看到runloop的事件源,分为Input Source 和 Timer Sourc

  • Input Source 又可以分为三类

  • Port-Based Sources,系统底层的 Port 事件,例如 CFSocketRef ,在应用层基本用不到

  • Custom Input Sources,用户手动创建的 Source

  • Cocoa Perform Selector Sources, Cocoa 提供的 performSelector 系列方法,也是一种事件源

  • Timer Source 指的是指定时器事件。

2)为什么要在主线程中更新UI,或者说为什么不能在子线程中更新UI
  • UIKit并不是一个线程安全的类,UI操作涉及到渲染访问各种View对象的属性,如果异步操作下会存在读写问题,而为其加锁则会耗费大量资源(内存、时间片轮转、cpu处理速度…)并拖慢运行速度。另一方面因为整个程序的起点UIApplication是在主线程进行初始化,所有的用户事件都是在主线程上进行传递(如点击、拖动),果UI异步线程的话 还会存在 同步处理事件的问题,所以多点触摸手势等一些事件要保持和UI在同一个线程相对是最优解。

  • 屏幕的渲染是 60帧(60Hz/秒), 也就是1秒钟回调60次的频率,runloop 理想状态下也会按照时钟周期回调60次, 这么高频率的调用是为了屏幕图像显示能够垂直同步不卡顿。如果在子线程中如果要对UI 进行更新,必须等到该子线程运行结束才能把UI的更新提交给渲染服务,很难保证这个处理过程的同步更新。即便能保证 相对主线程而言 系统资源开销 线程调度等等将会占据大部分资源和在同一个线程只专门干一件事有点得不偿失.

3)为什么只有主线程的runloop是开启的?

app启动时会调用main函数,具体如下:

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

mian函数中调用UIApplicationMain,这个函数会创建一个主线程,为了让程序可以一直运行,用于UI处理,所以在主线程中开启一个runloop,让主线程常驻

4)线程保活

线程保活,也就是让线程不销毁,这也就是runloop的存在的意义,所以我们可以使用runloop实现线程保活

runloop的启动分为三种

  • run:无条件进入runloop,会使线程进入死循环,本质是无限调用runMode:beforeDate
  • runUnitDate:设置超时时间,runloop处理完成后或者超时后,可以重新启动runloop,本质也是调用runMode:beforeDate,超时后不在调用。
  • runMode:beforeDate:指定runloop运行的模式

runloop结束分为两种

  • 通过设置超时时间结束runloop,利用runUnitDate或runMode:beforeDate
  • 手动结束,利用CFRunLoopStop()结束,该方法只会结束当前的 runMode:beforeDate: 调用,而不会结束后续的调用

苹果给的方案是如下

//开启
BOOL shouldKeepRunning = YES; // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);

//结束
CFRunLoopStop()

//验证runloop是否开启
利用performSelector:withObject:afterDelay方法,因为该方法依赖于线程的runloop,它本质上是由一个定时器负责定期加入到 runloop 中执行。所以如果这个方法可以成功执行,说明当前线程的 runloop 已经开启,否则则说明没有启动。
七、RunLoop 的实际应用举例

1、AFNetworking

AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 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;
}

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

- (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];
}

当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:…] 将这个任务扔到了后台线程的 RunLoop 中。

2、AsyncDisplayKit

AsyncDisplayKit 是 Facebook 推出的用于保持界面流畅性的框架,其原理大致如下:

UI 线程中一旦出现繁重的任务就会导致界面卡顿,这类任务通常分为3类:排版,绘制,UI对象操作。

排版通常包括计算视图大小、计算文本高度、重新计算子式图的排版等操作。
绘制一般有文本绘制 (例如 CoreText)、图片绘制 (例如预先解压)、元素绘制 (Quartz)等操作。
UI对象操作通常包括 UIView/CALayer 等 UI 对象的创建、设置属性和销毁。

其中前两类操作可以通过各种方法扔到后台线程执行,而最后一类操作只能在主线程完成,并且有时后面的操作需要依赖前面操作的结果 (例如TextView创建时可能需要提前计算出文本的大小)。ASDK 所做的,就是尽量将能放入后台的任务放入后台,不能的则尽量推迟 (例如视图的创建、属性的调整)。

为此,ASDK 创建了一个名为 ASDisplayNode 的对象,并在内部封装了 UIView/CALayer,它具有和 UIView/CALayer 相似的属性,例如 frame、backgroundColor等。所有这些属性都可以在后台线程更改,开发者可以只通过 Node 来操作其内部的 UIView/CALayer,这样就可以将排版和绘制放入了后台线程。但是无论怎么操作,这些属性总需要在某个时刻同步到主线程的 UIView/CALayer 去。

ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。
具体的代码可以看这里:_ASAsyncTransactionGroup

文章中6部分参考下面文章,https://blog.ibireme.com/2015/05/18/runloop/

Runloop的工作:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。 Osx/ios提供了两种runloop对象 : CFRunLoopRef是在CoreFoundation框架呢,是纯C函数的API,是线程安全的 NSRunLoop是基于CFRunLoopRef的封装,不是线程安全 CFRunLoopRef的线程安全性:CFRunLoopRef不是在线程刚创建的时候创建的,而是在线程获取的时候创建的,如果不主动获取线程,一直不会有。CFRunLoopRef在创建时通过static CFSpinLock_t loopsLock锁住CFRunLoopRef对象,所以CFRunLoopRef是安全的。 一个runloop包含若干个mode。每个mode又包含若干个Source/Timer/Observer CFRunLoopSourceRef是事件产生的地方。Source有两个版本: Source0和Source1。 Source0只包含一个回调,它不能主动出发事件,需要调用CFRunLoopSOurceSignal(source)将source标记,然后手动调用CFRunloopWakeUp()来唤醒RunLoop Source1包含了一个mach_port和一个回调,被用于通过内核和其他线程来发送消息,这种source能主动唤醒runloopCFRunLoopTimerRef是基于时间的触发器,它和NSTimer是toll-free bridged的,可以混用,包含一个时间长度和一个回调。当其加入到RunLoop时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒执行回调。 CFRunLoopObserverRef是观察者,每个Observer都包含了一个回调,当RunLoop的状态发生变化时,观察者就能通过回调接受到这个变化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员的修养

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值