1. 简介
RunLoop可以理解为一个do-while循环:
function loop(){
initialize();
do{
event = getEvent();
HandleEvent(event);
}while(isWeakUp);
}
当然肯定还有一些其他的处理逻辑在内部,才能保证其一一对应的线程能够在有任务时被唤醒,没有任务时休眠。
OSX/iOS系统提供了两个RunLoop对象:NSRunLoop和CFRunLoopRef;
CFRunLoopRef是苹果提供的CoreFoundation框架内一套纯c的函数API,而且API的线程都是安全的;
NSRunLoop则是基于CFRunLoopRef的封装,提供了面向对象的API,但是线程是不安全的。
首先看一下RunLoop结构体
struct __CFRunLoop {
CFRuntimeBase _base;
_CFRecursiveMutex _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
_CFThreadRef _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;//set
CFMutableSetRef _commonModeItems;//set
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes; //set
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
_Atomic(uint8_t) _fromTSD;
CFLock_t _timerTSRLock;
};
苹果不允许直接创建RunLoop,只提供了两个自动获取的函数,CFRunLoopGetMain()和CFRunLoopGetCurrent()此两个函数的逻辑是:
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
获取一个pthread对应的RunLoop
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(_CFThreadRef t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {//第一次进入时,初始化你一个全局Dic,并为主线程创建一个RunLoop:key:pthread_t value:CFRunLoopRef
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);
}
直接从dict内取Loop
CFRunLoopRef newLoop = NULL;
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {//取不到就创建一个
newLoop = __CFRunLoopCreate(t);
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
__CFUnlock(&loopsLock);
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
if (newLoop) { CFRelease(newLoop); }
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
#if _POSIX_THREADS //注册一个回调,线程销毁时,顺带销毁对应的runloop
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
#else
_CFSetTSD(__CFTSDKeyRunLoopCntr, 0, &__CFFinalizeRunLoop);
#endif
}
}
return loop;
}
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());
}
从代码可以看出,线程和runloop之间是一一对应的,其关系是保存在一个全局的Dictionary里。线程刚创建的时候并没有RunLoop,如果你不主动获取,它一直不会创建。RunLoop的创建是发生在第一次获取时,RunLoop的销毁是发生在线程结束时,只能在一个线程的内部获取其RunLoop(主线程除外)。
主要是_wakeUpPort、_commonModes、_commonModeItems、_currentMode
_wakeUpPort:是指当前唤醒runloop的端口
_commonModes:是一个集合,存放的是当前runloop所有Mode,而每个Mode包含若干个 Source/Timer/Observer
_commonModeItems:这个集合主要存放的是唤醒runloop的事件源:timer、source(分为source0,source1)、observer;
_currentMode:每次调用RunLoop的主函数,只能指定其中的一个Mode,故当前被指定的Mode就是_currentMode,RunLoop如需切换Mode只能退出当前loop重新指定Mode再进入
一个RunLoop包含若干个Mode,每个Mode又包含若干个Source/Timer?Observer。每次调用RunLoop的主函数时,只能指定其中一个Mode,这个Mode被称作CurrentMode。如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入。这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响。
CFRunLoopSourceRef是事件产生的的地方。Source有两个版本,Source0和Source1。
source0和source1的区别
source0不具备唤醒线程的能力,只包含一个回调,不能主动触发事件,使用时需先调用CFRunLoopSourceSignal(source),将这个source标记为待处理,然后手动调用 CFRunLoopWeakUp(runnloop)来唤醒RunLoop,让其处理这个事件。
source1:则能够主动唤醒线程,包含了一个mach_port和一个回调(函数指针),被用于通过内核和其他线程相互发送消息,这种Source能主动唤起RunLoop的线程。内核端口mach_port可以直接唤醒RunLoop,如触摸事件。
struct __CFRunLoopMode {
CFRuntimeBase _base;
_CFRecursiveMutex _lock; /* must have the run loop locked before locking this */
CFStringRef _name;// Mode Name, 例如 @"kCFRunLoopDefaultMode"
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0; //set
CFMutableSetRef _sources1;//set
CFMutableArrayRef _observers;//array
CFMutableArrayRef _timers;//array
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet;
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
__CFPort _timerPort;
Boolean _mkTimerArmed;
#endif
#if TARGET_OS_WIN32
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
uint64_t _timerSoftDeadline; /* TSR */
uint64_t _timerHardDeadline; /* TSR */
};
系统默认注册了5个mode
1. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行的。
2. UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响
3. UIInitializationRunLoopMode:在刚启动App时进入的第一个Mode,启动完成后就不再使用
4. GSEventReceiveRunLoopMode:接受系统事件的内部Mode,通常用不到。
5. kCFRunLoopCommonModes,这是一个占位的Mode,没有实际作用
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了
“CommonModes”:一个mode可以将自己标记为”Common”属性(通过将其ModeName添加到RunLoop的”CommonModes”中)。每当RunLoop的内容发生变化时,RunLoop都会自动将_commonModeItems里的Source/Observer/Timer同步到具有”Common”标记的所有Mode里。
应用场景举例:主线程的RunLoop里有两个预置的Mode:kCFRunLoopDefaultMode和UITrackingRunloopMode。这两个Mode都已经被标记为”Common”属性。DefaultMode是App平时所处的状态,TrackingRunLoopMode是追踪ScrollView滑动时的状态。当你创建一个Timer并添加到DefaultMode时,TImer会得到重复回调,但此时滑动一个TableView时,RunLoop会将Mode切换为TrackingRunLoop,这时timer就不会被回调,并且并不会影响滑动操作。
有时没需要一个Timer在两个Mode中都能得到回调,一种办法就是将timer分别加入这两个Mode。还有另一种方式就是将TImer加入到顶层的RunLoop的””commonModeItems”中。”CommonModeItems”被RunLoop自动更新到虽有具有”Common”属性的Mode里去。
CFRunLoop对外暴露的管理Mode接口主要有2个:
CF_EXPORT CFRunLoopMode CFRunLoopCopyCurrentMode(CFRunLoopRef rl);
CF_EXPORT CFArrayRef CFRunLoopCopyAllModes(CFRunLoopRef rl);
CF_EXPORT void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFRunLoopMode mode);
CF_EXPORT CFAbsoluteTime CFRunLoopGetNextTimerFireDate(CFRunLoopRef rl, CFRunLoopMode mode);
CF_EXPORT CFRunLoopRunResult CFRunLoopRunInMode(CFRunLoopMode mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);
CF_EXPORT Boolean CFRunLoopIsWaiting(CFRunLoopRef rl);
CF_EXPORT void CFRunLoopWakeUp(CFRunLoopRef rl);
CF_EXPORT void CFRunLoopStop(CFRunLoopRef rl);
只能通过mode name来操作内部的mode,当传入一个新的mode name但RunLoop内部没有对应mode时;RunLoop会自动创建对应的CFRunLoopModeRef。对于一个RunLoop来说,其内部的mode只能增加不能删除。
苹果公开提供的Mode有两个:kCFRunLoopDefaultMode(NSDefaultRunLoopMode)和UITrackingRunLoopMode,可以用这两个ModeName来操作对应的Mode。
同时苹果还提供了一个操作Common标记的字符串:kCFRunLoopCommonModes(NSRunLoopCommonModes),可以用这个字符串来操作Common Items,或标记一个mode为”Common”。使用时注意区分这个字符串和其他mode name
2. RunLoop实现逻辑
/// 用DefaultMode启动
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
/// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
/// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
/// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 如果mode里没有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;
/// 1. 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
/// 内部函数,进入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
/// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 4. RunLoop 触发 Source0 (非port) 回调。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
/// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
/// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
/// • 一个基于 port 的Source 的事件。
/// • 一个 Timer 到时间了
/// • RunLoop 自身的超时时间到了
/// • 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}
/// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
/// 收到消息,处理消息。
handle_msg:
/// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
/// 9.2 如果有dispatch到main_queue的block,执行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
/// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
/// 执行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);
if (sourceHandledThisLoop && stopAfterHandle) {
/// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}
/// 如果没超时,mode里没空,loop也没被停止,那继续loop。
} while (retVal == 0);
}
/// 10. 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
根据苹果在文档(https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW23)里的说明,RunLoop内部逻辑大致如下:
- Notify observers that the run loop has been entered.
- Notify observers that any ready timers are about to fire.
- Notify observers that any input sources that are not port based are about to fire.
- Fire any non-port-based input sources that are ready to fire.
- If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9.
- Notify observers that the thread is about to sleep.
- Put the thread to sleep until one of the following events occurs:
- An event arrives for a port-based input source.
- A timer fires.
- The timeout value set for the run loop expires.
- The run loop is explicitly woken up.
- Notify observers that the thread just woke up.
- Process the pending event.
- If a user-defined timer fired, process the timer event and restart the loop. Go to step 2.
- If an input source fired, deliver the event.
- If the run loop was explicitly woken up but has not yet timed out, restart the loop. Go to step 2.
- Notify observers that the run loop has exite
流程图引用自(https://blog.ibireme.com/wp-content/uploads/2015/05/RunLoop_1.png)
**
3. 什么时候使用RunLoop**
When Would You Use a Run Loop? The only time you need to run a run
loop explicitly is when you create secondary threads for your
application.Thread Safety and Run Loop Objects Thread safety varies depending on
which API you are using to manipulate your run loop. The functions in
Core Foundation are generally thread-safe and can be called from any
thread. If you are performing operations that alter the configuration
of the run loop, however, it is still good practice to do so from the
thread that owns the run loop whenever possible.The Cocoa NSRunLoop class is not as inherently thread safe as its Core
Foundation counterpart. If you are using the NSRunLoop class to modify
your run loop, you should do so only from the same thread that owns
that run loop. Adding an input source or timer to a run loop belonging
to a different thread could cause your code to crash or behave in an
unexpected way.
摘自苹果文档,可以看出,主要用于线程间的通信。
RunLoop的底层实现:
RunLoop的核心是基于mach port的,其进入休眠时调用的函数是mach_msg()。为了解释这个逻辑
苹果官方将整个系统大致划分为四个层次
应用层(包括用户能接触到的图形应用,例如:Spotlight、Aqua、SpringBoard)等。
应用框架层即(开发人员接触的cocoa等框架)
核心框架层(各种核心框架层,OpenGL)
Darwin即操作系统的核心(系统内核、驱动、shell等内容,这一层是开源的,其源码都可在 opensource.apple.com 里找到)
在硬件层上面的三个组成部分Mach、BSD、IOKit共同组成了XNU内核。
XNU内核的内环被称作Mach,其作为一个微内核,仅提供了诸如处理器调度、IPC(进程间通信)等非常少量的基础服务。
BSD层可以看做围绕Mach层的一个外环,其提供了诸如进程管理、文件系统和网络等功能。
IOKit层是为设备驱动提供了一个面向对象(C++)的一个框架
Mach本身提供的API非常有限,而且苹果也不鼓励使用Mach的API,但是这些API非常基础,如果没有这些API的话,其他任何工作都无法实施。在Mach中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为对象。和其他架构不同,Mach的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。”消息”是Mach中最基础的概念,消息在两个端口之间传递,这就是Mach的IPC(进程间通信的)核心
一条Mach消息实际上就是一个二进制数据包,其头部定义了当前端口local_port和目标端口remote_port,发送和接受消息都是通过同一个API进行的,其Opition标记了消息传递的方向;
为了实现消息的发送和接收,mach_msg()函数实际上是调用了一个Mach陷阱(trap),即函数mach_msg_trap(),陷阱这个概念在Mach中等同于系统调用。当你在用户态调用mach_msg_trap()时会触发陷阱机制,切换到内核态;内核态中内核实现的mach_msg()函数会完成实际的工作
在硬件层上面的三个组成部分Mach、BSD、IOKit共同组成了XNU内核。
XNU内核的内环被称作Mach,其作为一个微内核,仅提供了诸如处理器调度、IPC(进程间通信)等非常少量的基础服务。
BSD层可以看做围绕Mach层的一个外环,其提供了诸如进程管理、文件系统和网络等功能。
IOKit层是为设备驱动提供了一个面向对象(C++)的一个框架
Mach本身提供的API非常有限,而且苹果也不鼓励使用Mach的API,但是这些API非常基础,如果没有这些API的话,其他任何工作都无法实施。在Mach中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为对象。和其他架构不同,Mach的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。”消息”是Mach中最基础的概念,消息在两个端口之间传递,这就是Mach的IPC(进程间通信的)核心
一条Mach消息实际上就是一个二进制数据包,其头部定义了当前端口local_port和目标端口remote_port,发送和接受消息都是通过同一个API进行的,其Opition标记了消息传递的方向;
为了实现消息的发送和接收,mach_msg()函数实际上是调用了一个Mach陷阱(trap),即函数mach_msg_trap(),陷阱这个概念在Mach中等同于系统调用。当你在用户态调用mach_msg_trap()时会触发陷阱机制,切换到内核态;内核态中内核实现的mach_msg()函数会完成实际的工作
RunLoop的核心就是一个mach_msg(),RunLoop调用这个函数去接收消息,如果没有别人发送port消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个iOS的APP,然后App静止时点击暂停,你会看到主线程调用栈是停留在mach_msg_trap()这个地方。
事件响应
苹果注册了一个Source1(基于mach port)用来接收系统事件,其回调函数为_IOHIDEventSystemClientQueueCallback()。
当一个硬件事件(触摸,锁屏摇晃等)发生后,首先由IOKit.framework生成一个IOHIDEvent事件并由SpingBoard接收。SpringBoard只接受按键(锁屏,静音等),触摸,加速接近传感器等几种event,随后用mach port转发给需要的App进程。随后苹果注册的那个Source1就会触发回调,并调用_UIApplicationHandleEventQueue()进行应用内部的分发。
_UIApplicationHandleEventQueue()会把IOHIDEvuent处理包装成UIEvent进行处理或分发,其中包括识别UIGesture/处理屏幕旋转/发送给UIWindow等。通常事件比如UIButton点击,touchesBegin/Move/End/Cancle事件都在这个回调中完成
手势识别
当上面的_UIApplicationHandleEventQueue()识别了一个手势时,其首先会调用Cancel将当前的touchesBegain/Move/End系列回调打断.随后系统将对应的UIGestureRecognizer标记为待处理。
苹果注册了一个Observer检测BeforeWaiting(Loop即将进入休眠)事件,这个Observer的回调函数是_UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的GestureRecognizer,并执行GestureRecognizer的回调。
当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
界面更新
在操作UI时,比如改变Frame、更新了UIView/CALayer的层次时,或者手动调用UIView/CALayer的setNeedsLayout/setNeedsDisplay方法后,这个UIView/CALayer就被标记为待处理,并被提交到一个全局的容器去。
苹果注册了一个Observer监听BeforeWaiting(即将进入休眠)和Exit(即将退出Loop)事件,回调去执行一个很长的函数:_ZN2CA11Transaction17Observer_callbackEP19_CFRunLoopObserverMPv();这个函数会遍历所有待处理的UIView/CALayer以执行实际的绘制和调整,并更新UI界面,
定时器
是用XNU内核的mk_timer驱动.
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)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去,造成界面卡顿的感觉。在快速滑动TableView时,及时一帧的卡顿也能察觉,FaceBook开源的AsyncDisplayLink就是为了解决界面卡顿的问题,内部也用到了RunLoop
PerformSelector
当调用NSObject的performSelector:afterDelay:后,实际上其内部会创建一个Timer并添加到当前线程的RunLoop中,所以如果当前线程没有RunLoop,则这个方法会失效
当调用performSelector:onTread:时实际上会创建一个Timer加到对应的线程去,同样的若没有RunLoop该方法也会无效
关于GCD
实际上RunLoop底层也会用到GCD的东西
NSTimer是用XNU内核的mk_timer驱动. 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处理
关于网络请求
CFSocket是最底层接口,只负责socket通信
CFNetwork是基于CFSocket等接口的上层封装,ASIHTTPRequest工作于这一层
NSURLConnection基于CFNetwork的更高层封装,提供面向对象的接口,AFNetworking基于这一层
NSURLSession是iOS7中新增的接口,表面上是和NSURLConnection并列的,但底层仍然用到了NSURLConnection的部分功能(比如 com.apple.NSURLConnectionLoader 线程),AFNetworking2 和 Alamofire 工作于这一层。)
NSURLConnection工作流程
通常使用NSURLConnection时,会传入一个delegate,当调用了[connection start]后,这个Delegate就会不停收到事件回调。实际上,strat这个函数内部会获取CurrentRunLoop,然后
在其中的DefaultMode添加了4个Source0(即需要手动触发的source)。CFMultiplexerSource是负责各种Delegate回调的,CFHTTPCookieStorage是处理各种cookie的。
当开始网络传输时,我们可以看到NSURLConnection创建了两个新线程:
1️⃣com.apple.NSURLConnectionLoader和com.apple.CFSocket.private。其中CFSocket线程是处理底层socket链接的,NSURLConnectionLoader这个线程内部会用RunLoop来接收底层的socket事件,并通过之前添加的source0通知上层的Delegate
NSURLConnectionLoader中的RunLoop通过一些基于mach port 的Source接收来自底层CFSocket的通知,当接收到通知后,会在合适的时机向CFMultiplexerSource等Source0发送通知,同时唤醒Delegate线程的RunLoop来让其处理这些通知。CFMultiplexerSource会在Delegate线程的RunLoop对Delegate进行实际的回调
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 中。
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。
细节还是需要自己去下载源码看了,重在自己领悟。源码链接