每次面试,Runloop这个概念几乎是必问的。所以,还是写点东西出来做个记录,同时也加深一下自己的记忆。
一.什么是RunLoop
RunLoop 既运行循环机制,在应用级别考虑,应用程序中所有的任务处理(用户交互事件、网络请求回调数据接收等)都是在线程中执行,一般来讲一个线程一次只能执行一个任务,执行完线程销毁(OC 中子线程异步销毁,主线程除外),但是问题来了如何让线程保活呢?并且如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒呢?RunLoop 就是解决这些问题的,它所做的一切都是基于线程,可以说是为线程而生
OSX/iOS 系统中,提供的 两个 RunLoop 对象:CFRunLoopRef是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,且其是开源的,开源下载地址(CoreFoundation 源码)。NSRunLoop是基于 CFRunLoopRef 的封装,提供了面向对象的 API,并不开源。分析 CoreFoundation 库内的 RunLoop 源码分析。CoreFoundation 源码中 CFRunLoop 关于 RunLoop 的有五个类:
CFRunLoopRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
CFRunLoopModeRef
这五大类是如何通过C语言封装成的呢 ?
typedefstruct__CFRunLoop*CFRunLoopRef;
typedefstruct__CFRunLoopSource*CFRunLoopSourceRef;
typedefstruct__CFRunLoopObserver*CFRunLoopObserverRef;
typedefstruct__CFRunLoopTimer*CFRunLoopTimerRef;
由 CFRunLoop.h文件查看到如上源码
typedefstruct__CFRunLoopMode*CFRunLoopModeRef;
由 CFRunLoopModeRef 类于CFRunLoop.c 第 521 行可找到,下面具体分析其各种的源码结构
RunLoop
CFRunLoop.c文件中 第 636 行:
struct__CFRunLoop{
CFRuntimeBase _base;
pthread_mutex_t_lock;/* locked for accessing mode list */
__CFPort _wakeUpPort;// used for CFRunLoopWakeUp 内核向该端口发送消息可以唤醒
runloopBoolean _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;//等同NSMutableSet 存储所有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,若干个 commonMode,还有一个当前运行的 Mode(_currentMode)。
我们并不能去创建这个 CFRunLoopRef(为什么呢?),而是通过如下方法去获取当前线程的 RunLoop:
// CFRunLoop.h 中73行源码
CF_EXPORT CFRunLoopRef CFRunLoopGetCurrent(void);// 获取当前线程runloop
CF_EXPORT CFRunLoopRef CFRunLoopGetMain(void);// 获取主线程 runloop
具体使用姿势:
// 获取当前Runloop
CFRunLoopRef runloop =CFRunLoopGetCurrent();
RunLoopMode - 运行模式
CFRunLoop.c文件中 第 523 行:
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;
#ifUSE_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;
#end
if#ifUSE_MK_TIMER_TOOmach_port_t _timerPort;
Boolean _mkTimerArmed;
#endif
#ifDEPLOYMENT_TARGET_WINDOWS
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
uint64_t _timerSoftDeadline;/* TSR */
uint64_t _timerHardDeadline;/* TSR */
};
一个 CFRunLoopMode 对象有一个 name ,若干 source0、source1、timer、observer和若干port,所有事件都是由 Mode 在管理,而 一个线程下的 RunLoop 管理着若干个 Mode (ModeItems)。在一个线程中 runloop 保活线程并且在 指定 Mode下使其接受处理事件,又在每次 runloop 循环中(之前讲的死循环概念)进行 Mode间的切换。
Cocoa框架和Core Foundation框架中定义了五种 Mode (Guides and Sample Code )
Default 默认Mode,APP运行起来之后,主线程的RunLoop默认在该Mode下运行
NSDefaultRunLoopModeCocoa 框架
kCFRunLoopDefaultModeCore Foundation 框架
Event tracking 追踪触摸的手势,所有 UI 交互事件都运行在这个Mode下
UITrackingRunLoopMode(Cocoa)
Common modes 共有型 Model 含有上面两种Mode模式的意义
NSRunLoopCommonModes(Cocoa)
kCFRunLoopCommonModes(Core Foundation)
Connection 系统内核模式,系统调用事件发生会切换到相应模式下,开发者无法操作
GSEventReceiveRunLoopMode(Cocoa)
Modal 项目初始化模式,只会走一次
UIInitializationRunLoopMode(Cocoa)
RunLoop Source - 事件源
CFRunLoop.c文件中 第 943 行:
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits;//用于标记Signaled状态,source0只有在被标记为Signaled状态,才会被处理
pthread_mutex_t _lock;
CFIndex_order;/* immutable */
CFMutableBagRef _runLoops;
union{
CFRunLoopSourceContextversion0;/* immutable, except invalidation */
CFRunLoopSourceContext1version1;/* immutable, except invalidation */
} _context;
};
事件源顾名思义事件的产生地,由上源码会发现 Source 分为两种:
Source0 非基于Port的
只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理 signal 状态,然后手动调 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
其作用范围是应用程序中事件,由App自己管理的UIEvent、CFSocket都是source0。以下是source0的结构体:
typedef struct{
CFIndex version;
void* info;
const void*(*retain)(constvoid*info);
void(*release)(constvoid*info);
CFStringRef(*copyDescription)(constvoid*info);
Boolean (*equal)(constvoid*info1,constvoid*info2);
CFHashCode(*hash)(constvoid*info);
void(*schedule)(void*info,CFRunLoopRefrl,CFStringRefmode);
void(*cancel)(void*info,CFRunLoopRefrl,CFStringRefmode);
void(*perform)(void*info);
}CFRunLoopSourceContext;
Source1
包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。可以接收内核消息并触发回调,这种 Source 能主动唤醒 RunLoop 的线程。
其作用范围是由RunLoop和内核管理,source1带有mach_port_t,可以接收内核消息并触发回调,以下是source1的结构体:
typedef struct{
CFIndex version;
void* info;
constvoid*(*retain)(constvoid*info);
void(*release)(constvoid*info);
CFStringRef(*copyDescription)(constvoid*info);
Boolean (*equal)(constvoid*info1,constvoid*info2);
CFHashCode(*hash)(constvoid*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,CFIndexsize,CFAllocatorRefallocator,void*info);
#else
void* (*getPort)(void*info);
void(*perform)(void*info);
#endif
}CFRunLoopSourceContext1;
CFRunLoopObserver - 观察者
CFRunLoop.c文件中 第 981 行:
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 是 mode 下数组保存 ,其都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。回调函数CFRunLoopObserverCallBack
CFRunLoopObserver 可以观察的状态有如下6种:
/* Run Loop Observer Activities */
typedefCF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL <<0),
kCFRunLoopBeforeTimers = (1UL <<1),
kCFRunLoopBeforeSources = (1UL <<2),
kCFRunLoopBeforeWaiting = (1UL <<5),
kCFRunLoopAfterWaiting = (1UL <<6),
kCFRunLoopExit = (1UL <<7),
kCFRunLoopAllActivities =0x0FFFFFFFU
};
CFRunLoopTimer
CFRunLoop.c文件中 第 1049 行:
struct__CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFMutableSetRef _rlModes;
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; /* immutable */
CFTimeInterval _tolerance; /* mutable */
uint64_t _fireTSR; /* TSR units */
CFIndex _order; /* immutable */
CFRunLoopTimerCallBack _callout; /* immutable */
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};
是基于时间的触发器,它和 NSTimer 是toll-free bridged的,可以相互转换的。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,可以在设定的时间点 RunLoop 会被唤醒并执行回调。
RunLoop 与线程的关系
苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 这两个函数内部的逻辑大概是下面这样:
CFRunLoopRef CFRunLoopGetMain(void) {
CHECK_FOR_FORK();
staticCFRunLoopRef __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)returnrl;
return_CFRunLoopGet0(pthread_self());
}
获取RunLoop函数
staticCFMutableDictionaryRef __CFRunLoops =NULL;
staticCFSpinLock_t loopsLock = CFSpinLockInit;
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if(pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFSpinLock(&loopsLock);
if(!__CFRunLoops) {
__CFSpinUnlock(&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);
__CFSpinLock(&loopsLock);
}
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFSpinUnlock(&loopsLock);
if(!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFSpinLock(&loopsLock);
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
__CFSpinUnlock(&loopsLock);
CFRelease(newLoop);
}
if(pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void*)loop,NULL);
if(0== _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void*)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void(*)(void*))__CFFinalizeRunLoop);
}
}
returnloop;
从上面的代码可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 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,优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
事件响应
苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考这里。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用mach port 转发给需要的App进程。
随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发,此过程是Source0 完成的。
_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改变( Frame变化、 UIView/CALayer 的继承结构变化等)时,或手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理。
苹果注册了一个用来监听BeforeWaiting和Exit的Observer,在它的回调函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面
定时器
当使用NSTimer的scheduledTimerWithTimeInterval方法时。事实上此时Timer会被加入到当前线程的Run Loop中,且模式是默认的NSDefaultRunLoopMode。而如果当前线程就是主线程,也就是UI线程时,某些UI事件,比如UIScrollView的拖动操作,会将Run Loop切换成NSEventTrackingRunLoopMode模式,在这个过程中,默认的NSDefaultRunLoopMode模式中注册的事件是不会被执行的。也就是说,此时使用scheduledTimerWithTimeInterval添加到Run Loop中的Timer就不会执行。
所以为了设置一个不被UI干扰的Timer,我们需要手动创建一个Timer,然后使用NSRunLoop的addTimer:forMode:方法来把Timer按照指定模式加入到Run Loop中。这里使用的模式是:NSRunLoopCommonModes,这个模式等效于NSDefaultRunLoopMode和NSEventTrackingRunLoopMode的结合。
无论是单次执行的NSTimer还是重复执行的NSTimer都不是准时的,这与当前NSTimer所处的线程有很大的关系,如果NSTimer当前所处的线程正在进行大数据处理(假设为一个大循环),NSTimer本次执行会等到这个大数据处理完毕之后才会继续执行
这期间有可能会错过很多次NSTimer的循环周期,但是NSTimer并不会将前面错过的执行次数在后面都执行一遍,而是继续执行后面的循环,也就是在一个循环周期内只会执行一次循环。
无论循环延迟的多离谱,循环间隔都不会发生变化,在进行完大数据处理之后,有可能会立即执行一次NSTimer循环,但是后面的循环间隔始终和第一次添加循环时的间隔相同。这个事件是怎么执行的?并且为什么有的时候会延迟?为什么子线程中创建的Timer并不执行?
首先,在进入循环开始以后,就要处理source0事件,处理后检测一下source1端口是否有消息,如果一个Timer的时间间隔刚好到了则此处有可能会得到一个消息,则runLoop直接跳转至端口激活处从而去处理Timer事件。
第二,为什么会延迟?我们知道,两次端口事件是在两个runLoop循环中分别执行的。比如Timer的时间间隔为1秒,在第一次Timer回调结束后,在很短时间内立即进入runLoop的下一次循环,这次并不是Timer回调并且是一个计算量非常大的任务,计算时间超过了1秒,那么runLoop的第二个循环就要执行很久,无法进入下一个循环等待有可能即将到来的Timer第二次回调的信号,所以Timer第二次回调就会推迟了。
第三,为什么在子线程中创建的Timer并且提交到当前runLoop中并不会运行?这还是要从runLoop的获取函数中看,当调用currentRunLoop的时候会取当前线程对应的runLoop,而首次是取不到的,则会创建一个新的runLoop。但是!这个runLoop并没有run。就是没有开启
- (void)applicationDidBecomeActive:(UIApplication*)application{
// NSThread 创建一个子线程
[NSThreaddetachNewThreadSelector:@selector(testTimerSheduleToRunloop1) toTarget:selfwithObject:nil];
}
// 测试把timer加到不运行的runloop上的情况
- (void)testTimerSheduleToRunloop1
{
NSLog(@"Test timer shedult to a non-running runloop");
SvTestObject *testObject4 = [[SvTestObject alloc] init];
NSTimer*timer = [[NSTimeralloc] initWithFireDate:[NSDatedateWithTimeIntervalSinceNow:1] interval:1target:testObject4 selector:@selector(timerAction:) userInfo:nilrepeats:NO];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 打开下面一行输出runloop的内容就可以看出,timer却是已经被添加进去//
NSLog(@"the thread's runloop: %@", [NSRunLoop currentRunLoop]);
// 下面一行, 该线程的runloop就会运行起来,timer才会起作用
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
NSLog(@"invoke release to testObject4");
}
- (void)applicationWillResignActive:(UIApplication*)application
{
NSLog(@"SvTimerSample Will resign Avtive!");
}
PerformSelecter
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
Runloop 开发中运用
滑动与图片刷新:
当tableview的cell上有需要从网络获取的图片的时候,滚动tableView,异步线程会去加载图片,加载完成后主线程就会设置cell的图片,但是会造成卡顿。可以让设置图片的任务在CFRunLoopDefaultMode下进行,当滚动tableView的时候,RunLoop是在 UITrackingRunLoopMode 下进行,不去设置图片,而是当停止的时候,再去设置图片。
GCD
实际上 RunLoop 底层也会用到 GCD 的东西,当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。
友情链接:
源码级 RunLoop 剖析 -
iOS - RunLoop 底层源码详解及具体运用 -