一、概念
简单点说
Runloop
就是一个do while
的运行循环。主要的作用就是保持程序的持续运行。比如主线程的runloop
从程序打开就一直在运行。一个线程对应着一个runloop
。RunLoop
在第一次获取时创建,在线程结束时销毁。
OSX/iOS
系统中,提供了两个这样的对象:NSRunLoop
和CFRunLoopRef
。
-
CFRunLoopRef
是在CoreFoundation
框架内的,它提供了纯C
函数的 API,所有这些 API 都是线程安全
的。 -
NSRunLoop
是基于CFRunLoopRef
的一层OC包装,提供了面向对象的 API,但是这些 API不是线程安全
的
苹果不允许
直接创建RunLoop
,获取Runloop
的两种方式
NSRunLoop
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
CFRunLoopRef
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
二、Runloop 与线程的关系
线程和
RunLoop
之间是一一对应的,其关系是保存在一个全局的Dictionary
里。线程刚创建时并没有RunLoop
,如果你不主动获取,那它一直都不会有。RunLoop
的创建是发生在第一次获取时,RunLoop
的销毁是发生在线程结束时。你只能在一个线程的内部获取其RunLoop
(主线程除外)
Runloop
创建的代码
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;
/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
// 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
/// 直接从 Dictionary 里获取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
/// 取不到时,创建一个
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}
OSSpinLockUnLock(&loopsLock);
return loop;
}
CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}
三、 Runloop
的组成(结构)
两张图都描述了
Runloop
的结构 ,从图中可以看出一个
Runloop
中由多个
Mode
组成,一个
Mode
可以包含多个
Source
,
Timer
,
Observer
.
Source/Timer/Observer
被统称为mode item
,一个item
可以被同时加入多个mode
。但一个item
被重复加入同一个mode
时是不会有效果的。如果一个mode
中一个item
都没有,则RunLoop
会直接退出,不进入循环。
在CoreFoundation
里面关于RunLoop
的这5个类:
- CFRunLoopRef
Runloop
- CFRunLoopModeRef
Mode
- CFRunLoopSourceRef
Source
- CFRunLoopTimerRef
Timer
- CFRunLoopObserverRef
Observer
3.1、CFRunLoopMode
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。
每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode,RunLoop在同一时段只能且必须在一种特定Mode下Run
如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
-
kCFRunLoopDefaultMode
: App的默认 Mode,通常主线程是在这个 Mode 下运行的。 -
UITrackingRunLoopMode
: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。 -
UIInitializationRunLoopMode
: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。 -
GSEventReceiveRunLoopMode
: 接受系统事件的内部 Mode,通常用不到。 -
kCFRunLoopCommonModes
: 这是一个占位的 Mode,没有实际作用。
3.2、CFRunLoopSource
CFRunLoopSource
是RunLoop的数据源抽象类,类似iOS中的protocol
,RunLoop定义两Source
- Source0:处理App内部事件,App自己负责管理(触发),如
UIEvent,CFSocket
,Source0
包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用CFRunLoopSourceSignal(source)
,将这个 Source 标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)
来唤醒RunLoop
,让其处理这个事件。 - Source1:由RunLoop和内核管理,
Mach port
驱动 如CFMach、CFMessage,Source1
包含了一个mach_port
和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程。
3.3、CFRunLoopTimerRef
CFRunLoopTimerRef
是基于时间的触发器,它和NSTimer
是toll-free bridged
的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes;
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;
都是封装于CFRunLoopTimerRef
3.4、CFRunLoopObserverRef
CFRunLoopObserver
向内部报告RunLoop当前状态的更改
CFRunLoopObserverRef
是观察者,每个 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
};
CFRunLoopObserver
的创建
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFStringRef runLoopMode = kCFRunLoopDefaultMode;
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler
(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {
// TODO here
});
CFRunLoopAddObserver(runLoop, observer, runLoopMode);
四、Runloop
本质:mach port
和mach_msg()
。
Mach
是XNU
的内核,进程、线程和虚拟内存等对象通过端口发消息进行通信,Runloop
通过mach_msg()
函数发送消息,如果没有port
消息,内核会将线程置于等待状态mach_msg_trap()
。如果有消息,判断消息类型处理事件,并通过modeItem
的callback
回调
五、OC中Runloop参与的地方
5.1、Autorelease Pool
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 了。
5.2、事件响应
苹果注册了一个Source1
(基于 mach port 的) 用来接收系统事件,其回调函数为__IOHIDEventSystemClientQueueCallback()
。
当一个硬件事件发生后,首先由IOKit.framework
处理,随后用runloop(mach port)
转发给需要的App进程。随后苹果注册的那个 Source1
就会触发回调,并调用 _UIApplicationHandleEventQueue()
进行应用内部的分发。
_UIApplicationHandleEventQueue()
会把IOHIDEvent
处理并包装成 UIEvent
进行处理或分发,其中包括识别UIGesture
/处理屏幕旋转/发送给UIWindow
等。
5.3、手势识别
当上面的_UIApplicationHandleEventQueue()
识别了一个手势时,其首先会调用 Cancel
将当前的 touchesBegin/Move/End
系列回调打断。随后系统将对应的UIGestureRecognizer
标记为待处理。
苹果注册了一个Observer
监测BeforeWaiting
(Loop即将进入休眠) 事件,这个Observer
的回调函数是 _UIGestureRecognizerUpdateObserver()
,其内部会获取所有刚被标记为待处理的 GestureRecognizer
,并执行GestureRecognizer
的回调。
当有 UIGestureRecognizer
的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
5.3、更新UI
当在操作 UI 时,比如改变了Frame
、更新了 UIView/CALayer
的层次时,或者手动调用了 UIView/CALayer
的 setNeedsLayout/setNeedsDisplay
方法后,这个 UIView/CALayer
就被标记为待处理,并被提交到一个全局的容器去。
苹果注册了一个 Observer
监听BeforeWaiting
(即将进入休眠) 和Exit
(即将退出Loop) 事件,回调去执行一个很长的函数:_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
。这个函数里会遍历所有待处理的 UIView/CAlayer
以执行实际的绘制和调整,并更新 UI 界面。
5.4、定时器
NSTimer
其实就是CFRunLoopTimerRef
,他们之间是toll-free bridged
的。一个 NSTimer
注册到RunLoop
后,RunLoop
会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20
这几个时间点。RunLoop
为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance
(宽容度),标示了当时间点到后,容许有多少最大误差。
5.5、PerformSelecter
当调用 NSObject
的 performSelecter:afterDelay:
后,实际上其内部会创建一个Timer
并添加到当前线程的RunLoop
中。所以如果当前线程没有RunLoop
,则这个方法会失效。
当调用 performSelector:onThread:
时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有RunLoop
该方法也会失效。
5.6、GCD回到主线程
当调用 dispatch_async(dispatch_get_main_queue(), block)
时,libDispatch 会向主线程的 RunLoop
发送消息,RunLoop
会被唤醒,并从消息中取得这个block
,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
里执行这个 block
。但这个逻辑仅限于 dispatch
到主线程,dispatch
到其他线程仍然是由libDispatch
处理的。
5.7、网络请求
通常使用 NSURLConnection
时,你会传入一个Delegate
,当调用了 [connection start]
后,这个 Delegate
就会不停收到事件回调。实际上,start
这个函数的内部会获取 CurrentRunLoop
,然后在其中的 DefaultMode
添加了4个 Source0 (即需要手动触发的Source)。CFMultiplexerSource
是负责各种 Delegate
回调的,CFHTTPCookieStorage
是处理各种 Cookie
。
六、Runloop的实际使用
6.1常驻线程
常驻线程的好处在于 Run Loops 可以让你使用最小的资源来创建长时间运行线程。因为 run loop 在没有任何事件处理的时候会把它的线程置于休眠状态,它消除了消耗 CPU 周期轮询,并防止处理器本身进入休眠状态并节省电源。
+ (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;
}
- (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];
}
6.2NSTimer
若不希望Timer被ScrollView影响,需添加到NSRunLoopCommonModes
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0
target:self
selector:@selector(timerTick:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
6.3tableview加载图片
避免滑动过程中加载图片,使界面卡顿
UIImage *downLoadImage = ...;
[self.avatarImageView performSelector:@selector(setImage:)
withObject:downloadImage
afterDelay:0
inModes:@[NSDefaultRunLoopMode]];
本文参考:
深入理解RunLoop