-
准备工作
RunLoop源码下载地址 -
RunLoop 概念
-
RunLoop 简介
RunLoop其实就是一种事务处理的循环,是事件接受、分发的机制的实现,用来不停的调度工作以及处理输入事件。其本质就是一个do-while循环,如果有输入则执行对应事件,没有则休息,这里RunLoop的do-while和普通的do-while循环不一样,普通的循环会让cpu处于忙等待的情况,当cpu跑满了也就崩溃了。而runloop的do-while循环是一种闲等待,也就是不会消耗cpu所以runloop具有休眠功能。
下图是runloop运行原理图:
-
-
RunLoop 作用
- 保持程序持续运行。程序启动就会自动开启一个
runloop在主线程 - 处理App中的各种事件
- 节省CPU资源,提高程序性能 有事情则做事,没事情则休眠
- 保持程序持续运行。程序启动就会自动开启一个
-
RunLoop 和线程之间的关系
从上文中的runloop运行原理图可以知道两者之间的关系是一一对应的关系 -
RunLoop 底层源码分析
-
RunLoop本质RunLoop底层其实就是一个结构体,是一个__CFRunLoop对象,具体结构如下:从对象的结构中可以得出当前运行的
mode只会有一个,但是每个runloop里面是存在多个mode的同时也存在着多个item也就是事务(source、timer、observer) -
获取
RunLoop以及进一步验证RunLoop和线程之间的关系- 获取主线程
runloop:CFRunLoopGetMain()发现底层就是调用
_CFRunLoopGet0方法获取runloop,入参是主线程 - 获取当前线程
runloop:CFRunLoopGetCurrent()
同样的是调用了
_CFRunLoopGet0方法获取runloop此时的入参是当前线程 - 获取
runloop:_CFRunLoopGet0大致流程如下:CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) { if (pthread_equal(t, kNilPthreadT)) { t = pthread_main_thread_np(); } __CFSpinLock(&loopsLock); if (!__CFRunLoops) { //如果存储runloop的字典不存在则创建 //只有第一次进来的时候才会走到这 __CFSpinUnlock(&loopsLock); //创建一个临时字典 CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks); //创建主线程 CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np()); // dict : key value //将线程和runloop以key-value的形式存到临时字典 CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop); //OSAtomicCompareAndSwapPtrBarrier 将临时的字典写入到__CFRunLoops if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) { CFRelease(dict); } //释放mainLoop CFRelease(mainLoop); __CFSpinLock(&loopsLock); } //通过线程也就是key 到__CFRunLoops中寻找runloop CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); __CFSpinUnlock(&loopsLock); if (!loop) { //如果没有找到则基于入参的线程创建一个runloop //这里只有可能是获取其他线程的runloop CFRunLoopRef newLoop = __CFRunLoopCreate(t); __CFSpinLock(&loopsLock); loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); if (!loop) { //此时将新创建的runloop存到__CFRunLoops中 CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop); //赋值操作 loop = newLoop; } // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it __CFSpinUnlock(&loopsLock); //释放新创建的runloop 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的字典是否存在也就是__CFRunLoops是否存在,如果不存在说明是第一次获取runloop此时创建主线程runloop然后存储到__CFRunLoops中 - 通过
key也就是入参的线程在__CFRunLoops中查找runloop如果找到了给对应的runloop注册一个回调然后返回,如果没有找到则基于入参的线程创建一个新的runloop然后存到__CFRunLoops,然后一样的注册一个回调然后返回 总结:
从上述的源码发现底层runloop的存储使用的是字典结构,线程是key,对应的runloop是value,从这里进一步的印证了线程和 runloop的关系是一一对应的
- 先判断存储
- 获取主线程
-
RunLoop创建就是开辟空间创建
runloop对象并配置对应参数 -
RunLoop Mode
struct __CFRunLoopMode { CFRuntimeBase _base; pthread_mutex_t _lock; /* must have the run loop locked before locking this */ CFStringRef _name; Boolean _stopped; //mode是否被终止 char _padding[3]; //几种事件 CFMutableSetRef _sources0; //sources0 集合 CFMutableSetRef _sources1; //sources1 集合 CFMutableArrayRef _observers; //observers 集合 CFMutableArrayRef _timers; //timers 集合 CFMutableDictionaryRef _portToV1SourceMap; __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 */ };从
__CFRunLoopMode的结构体当中我们可以得到的消息是每个mode中有若干个source0、source1、timer、observer和若干个端口所以事务的执行是由mode控制的,而上文张分析runloop的结构的时候又知道每个runloop当中都含有多个mode,但是当前的mode只有一个,所以从这里可以总结出两点:runloop管理着mode- 每次
runloop运行的时候都必须指定有且仅有一个mode指定对应的事务,如果需要执行其他mode的事务需要切换mode重新进入runloop - 这里可以总结出
runloopmode事务、三者的关系如下图
Mode的类型
从官方文档中可以知道mode的类型一共五种如下其中mode在苹果文档中提及的有五个,而在
iOS中公开暴露出来的只有NSDefaultRunLoopMode和NSRunLoopCommonModes。NSRunLoopCommonModes实际上是一个Mode的集合,默认包括NSDefaultRunLoopMode和NSEventTrackingRunLoopMode(当以模态方式跟踪事件(例如鼠标拖动循环)时,应将运行循环设置为此模式。)。可通过CFRunLoopAddCommonMode添加到runloop运行模式集合中
-
RunLoop Source
Run Loop Source分为Source、Observer、Timer三种,也就是ModeItemSourcesource又分为source0和source1表示可以唤醒RunLoop的一些事件,例如用户点击了屏幕,就会创建一个RunLoopsource0表示非系统事件,即用户自定义的事件,不能自动唤醒runloop需要先调用CFRunLoopSourceSignal(source)将source置为待处理事件,然后再唤醒runloop让其处理这个事件source1由RunLoop和内核管理,source1带有mach_port_t,可以接收内核消息并触发回调,以下是source1的结构体
Observer主要用于监听RunLoop的状态变化,并作出一定响应,主要有以下一些状态Timer就是是定时器,可以在设定的时间点抛出回调, 综上所述得知
Runloop通过监控Source来决定有没有任务要做,除此之外,我们还可以用Runloop Observer来监控Runloop本身的状态。Runloop Observer可以监控上面的Runloop事件,具体流程如下图。
-
添加
modeCFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef mode)添加一个mode到runloop的 commonmodes中CFStringRef CFRunLoopCopyCurrentMode(CFRunLoopRef rl)返回当前运行的mode的名称从这里也可以证明当前运行的
mode是唯一的CFArrayRef CFRunLoopCopyAllModes(CFRunLoopRef rl)返回runloop中所有的mode
-
添加/移除
Sourcevoid CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode)添加source0或者是source1添加void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { /* DOES CALLOUT */ CHECK_FOR_FORK(); if (__CFRunLoopIsDeallocating(rl)) return; if (!__CFIsValid(rls)) return; Boolean doVer0Callout = false; __CFRunLoopLock(rl); if (modeName == kCFRunLoopCommonModes) { //如果是kCFRunLoopCommonModes //则将对应的source添加到_commonModeItems中 CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL; if (NULL == rl->_commonModeItems) { //如果没有则创建一个 rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks); } // CFSetAddValue(rl->_commonModeItems, rls); if (NULL != set) { //如果set 存在则将source添加到每一个common-modes中去 //这里需要解释一下 应为kCFRunLoopCommonModes 是Mode的集合 所需需要在每个mode中都对应添加这个source //这样才能体现kCFRunLoopCommonModes的灵活性 不管设置那个mode 只要改mode在kCFRunLoopCommonModes这个 //集合中source就能执行 CFTypeRef context[2] = {rl, rls}; /* add new item to all common-modes */ CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context); CFRelease(set); } } else { //通过modeName 获取 mode CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true); if (NULL != rlm && NULL == rlm->_sources0) { //如果mode存在但是_sources0不存在则初始化_sources0、_sources1、_portToV1SourceMap rlm->_sources0 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks); rlm->_sources1 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks); rlm->_portToV1SourceMap = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, NULL); } if (NULL != rlm && !CFSetContainsValue(rlm->_sources0, rls) && !CFSetContainsValue(rlm->_sources1, rls)) { //如果_sources0和_sources1的集合中都不存在source if (0 == rls->_context.version0.version) { //如果version == 0 则添加到_sources0 CFSetAddValue(rlm->_sources0, rls); } else if (1 == rls->_context.version0.version) { //如果version == 1 则添加到_sources1 CFSetAddValue(rlm->_sources1, rls); __CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info); if (CFPORT_NULL != src_port) { //此处只有在加到source1的时候才会把souce和一个mach_port_t对应起来 //可以理解为,source1可以通过内核向其端口发送消息来主动唤醒runloop CFDictionarySetValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port, rls); __CFPortSetInsert(src_port, rlm->_portSet); } } __CFRunLoopSourceLock(rls); //把runloop加入到source的_runLoops中 if (NULL == rls->_runLoops) { rls->_runLoops = CFBagCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeBagCallBacks); // sources retain run loops! } CFBagAddValue(rls->_runLoops, rl); __CFRunLoopSourceUnlock(rls); if (0 == rls->_context.version0.version) { if (NULL != rls->_context.version0.schedule) { doVer0Callout = true; } } } if (NULL != rlm) { __CFRunLoopModeUnlock(rlm); } } __CFRunLoopUnlock(rl); if (doVer0Callout) { // although it looses some protection for the source, we have no choice but // to do this after unlocking the run loop and mode locks, to avoid deadlocks // where the source wants to take a lock which is already held in another // thread which is itself waiting for a run loop/mode lock rls->_context.version0.schedule(rls->_context.version0.info, rl, modeName); /* CALLOUT */ } }source步骤:- 先判断
mode如果是commonMode则将source添加到_commonModeItems中,并且将source添加到每一个common-modes中 - 如果不是
commonMode则先通过mode名称获取对应mode - 判断
mode的source0是否存在,不存在则初始化_sources0、_sources1、_portToV1SourceMap - 判断
source是否已经在source0或者是source1的集合中 - 如果存在则啥都不干,不存在则判断
rls->_context.version0.version是0,则添加到source0集合中 - 是1 则添加到
source1的集合中然后吧souce和一个mach_port_t对应起来
- 先判断
void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode)理解了添加的操作,删除的操作就比较简单了,这里就不赘述了void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { /* DOES CALLOUT */ CHECK_FOR_FORK(); Boolean doVer0Callout = false, doRLSRelease = false; __CFRunLoopLock(rl); //同样的是需要判断mode if (modeName == kCFRunLoopCommonModes) { if (NULL != rl->_commonModeItems && CFSetContainsValue(rl->_commonModeItems, rls)) { CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL; //_commonModeItems中删除对应source CFSetRemoveValue(rl->_commonModeItems, rls); if (NULL != set) { CFTypeRef context[2] = {rl, rls}; /* remove new item from all common-modes */ CFSetApplyFunction(set, (__CFRunLoopRemoveItemFromCommonModes), (void *)context); CFRelease(set); } } else { } } else { CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, false); if (NULL != rlm && ((NULL != rlm->_sources0 && CFSetContainsValue(rlm->_sources0, rls)) || (NULL != rlm->_sources1 && CFSetContainsValue(rlm->_sources1, rls)))) { CFRetain(rls); if (1 == rls->_context.version0.version) { __CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info); if (CFPORT_NULL != src_port) { CFDictionaryRemoveValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port); __CFPortSetRemove(src_port, rlm->_portSet); } } CFSetRemoveValue(rlm->_sources0, rls); CFSetRemoveValue(rlm->_sources1, rls); __CFRunLoopSourceLock(rls); if (NULL != rls->_runLoops) { CFBagRemoveValue(rls->_runLoops, rl); } __CFRunLoopSourceUnlock(rls); if (0 == rls->_context.version0.version) { if (NULL != rls->_context.version0.cancel) { doVer0Callout = true; } } doRLSRelease = true; } if (NULL != rlm) { __CFRunLoopModeUnlock(rlm); } } __CFRunLoopUnlock(rl); if (doVer0Callout) { // although it looses some protection for the source, we have no choice but // to do this after unlocking the run loop and mode locks, to avoid deadlocks // where the source wants to take a lock which is already held in another // thread which is itself waiting for a run loop/mode lock rls->_context.version0.cancel(rls->_context.version0.info, rl, modeName); /* CALLOUT */ } if (doRLSRelease) CFRelease(rls); }void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef mode)添加
Observer和添加source区别就在于Observer如果已经添加到其他 runloop中去了则不能再被添加,从__CFRunLoopObserver和__CFRunLoopSource也可以看出差异点定义的时候
__CFRunLoopObserver就是只能获取一个runloop而__CFRunLoopSource是一个runloop的集合void CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef * mode)
同CFRunLoopRemoveSourcevoid CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode)
同CFRunLoopAddObservervoid CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode)
同CFRunLoopRemoveSource
-
CFRunLoopRun
-
CFRunLoopRunInMode
指定模式下运行
runloop再看
CFRunLoopRunSpecific方法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; int32_t result = kCFRunLoopRunFinished; // 1.通知observer即将进入runloop if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); //通知observer已退出runloop if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); __CFRunLoopModeUnlock(currentMode); __CFRunLoopPopPerRunData(rl, previousPerRun); rl->_currentMode = previousMode; __CFRunLoopUnlock(rl); return result; }大致流程如下:
- 判断当前
runloop中是否含有mode如果没有则退出 - 判断
mode中是否含有itme没有则退出 - 将传入的
mode置为当前运行mode - 通知
Observer进入runloop - 启动
runloop这里调用的是__CFRunLoopRun方法 - 通知
Observer已退出runloop
- 判断当前
-
__CFRunLoopRun
由于该源码太多这里就是用伪代码来代替
//核心函数 /* rl, rlm are locked on entrance and exit */ static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode){ //通过GCD开启一个定时器,然后开始跑圈 dispatch_source_t timeout_timer = NULL; ... dispatch_resume(timeout_timer); int32_t retVal = 0; //处理事务,即处理items do { // 通知 Observers: 即将处理timer事件 __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); // 通知 Observers: 即将处理Source事件 __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources) // 处理Blocks __CFRunLoopDoBlocks(rl, rlm); // 处理sources0 Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle); // 处理sources0返回为YES if (sourceHandledThisLoop) { // 处理Blocks __CFRunLoopDoBlocks(rl, rlm); } // 判断有无端口消息(Source1) if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) { // 处理消息 goto handle_msg; } // 通知 Observers: 即将进入休眠 __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); __CFRunLoopSetSleeping(rl); // 等待被唤醒 __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY); // user callouts now OK again __CFRunLoopUnsetSleeping(rl); // 通知 Observers: 被唤醒,结束休眠 __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); handle_msg: if (被timer唤醒) { // 处理Timers __CFRunLoopDoTimers(rl, rlm, mach_absolute_time()); }else if (被GCD唤醒){ // 处理gcd __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); }else if (被source1唤醒){ // 被Source1唤醒,处理Source1 __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) } // 处理block __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;//结束 } }while (0 == retVal); return retVal; }发现和上文中的
runloop流程图是一致的
这里挑一个处理timers源码作讲解其他大同小异__CFRunLoopDoTimers源码发现底层就是通过调用
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__方法来执行timer的回调这里也可以通过打印堆栈来验证
类似这种函数一共有6种:
- block应用:
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ - 调用timer:
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ - 响应source0:
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ - 响应source1:
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ - GCD主队列:
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ - observer源:
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
- block应用:
-
总结
通过源码探索也可以发现
runloop其实很简单,它是一个对象,并且和线程一一对应,主线程的runloop是自动开启的,子线程的runloop是需要手动开启的,每一个runloop中会有多个mode, 每一个mode中又有多个source,但是runloop每次run只能选择一种mode(这也就造成了timer和页面滑动之后的问题下文有详细讲解)。RunLoop运行的核心是一个do..while..循环,遍历所有需要处理的事件,如果有事件处理就让线程工作,没有事件处理则让线程休眠,同时等待事件到来。
-
-
RunLoop的应用及相关问题
-
NSTimer的应用
NSTimer其实就是上文提到的timer,底层runloop会在每个时间点都注册一个事件,到了时间点回调时间,但是runloop为了节省资源不会每次都很准确的回调事件,timer有个属性是宽容度Tolerance标识了可以有的多大误差,这样的机制就导致了timer不是特别准确。而且如果某个时间点错过了就不会再重复执行
面试题:以+scheduledTimerWithTimeInterval:的方式触发的timer,在滑动页面上的列表时,timer会暂停回调, 为什么?如何解决?
应为scheduledTimerWithTimeInterval添加方式默认的mode是defaultMode,而页面滑动时的mode是UITrackingRunLoopMode,滑动的时候runloop会切换mode,上文也提到过runloop一次只能执行一个mode中的事件,所以对应的timer就会停止。
**解决方法是:**将timer添加到NSRunLoopCommonModes中就好 -
GCD Timer的应用
GCD的线程管理是通过系统来直接管理的。GCD Timer是通过dispatch port给RunLoop发送消息,来使RunLoop执行相应的block,如果所在线程没有RunLoop,那么GCD会临时创建一个线程去执行block,执行完之后再销毁掉,因此GCD的Timer是可以不依赖RunLoop的。
至于这两个Timer的准确性问题,如果不在RunLoop的线程里面执行,那么只能使用GCD Timer,由于GCD Timer是基于MKTimer(mach kernel timer),已经很底层了,因此是很准确的。
如果GCD Timer在RunLoop的线程中执行那么可能出现的问题和timer大同小异
面试题:GCD Timer和 NSTimer那个更精确,为什么
这个问题不能单单直接肯定的说是GCD Timer上文也分析到了如果在有RunLoop的线程中精确度和NSTimer差不多,具体原因可以看上文的NSTimer分析
如果线程中没有runloop首选GCD timer,它相对于NSTimer更加低层并且没有runloop的影响所以更加精确 -
AutoreleasePool和RunLoop的关系
- App 启动后,苹果在主线程
RunLoop里注册了两个Observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()。 - 第一个
Observer监视的事件是Entry(即将进入Loop),其回调内会调用_objc_autoreleasePoolPush()创建自动释放池。其order是 -2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。 - 第二个
Observer监视了两个事件:BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush()释放旧的池并创建新池;Exit(即将退出Loop) 时调用_objc_autoreleasePoolPop()来释放自动释放池。这个Observer的order是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
总结一下自动释放池的创建、销毁时机: kCFRunLoopEntry进入runloop之前,创建一个自动释放池kCFRunLoopBeforeWaiting休眠之前,销毁自动释放池,创建一个新的自动释放池kCFRunLoopExit退出runloop之前,销毁自动释放池
- App 启动后,苹果在主线程
-
事件响应
苹果注册了一个 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的回调。
-
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的应用
由于图片渲染到屏幕需要消耗较多资源,为了提高用户体验,当用户滚动Tableview的时候,只在后台下载图片,但是不显示图片,当用户停下来的时候才显示图片。
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"imgName"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];此方法即时此时正在滑动屏幕也不会出现卡顿的情况,应为滑动的过程中,runloop的mode是NSEventTrackingRunLoopMode所以滑动的时候会切换mode那么NSDefaultRunLoopMode的事件也就停止了, 滑动完成之后才会切到NSDefaultRunLoopMode然后继续事件,这样就不会造成卡顿现象 -
线程常驻
这个就很简单了其实就是开个线程将
runloop跑起来,但是单纯的跑runloop也不行,如果事件执行完了runloop就会消亡对应的线程也就会销毁。所以可以添加一个timer或者是NSPort具体方法如下:[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];[[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];三种方法都能保证runloop运行,线程常驻
-