iOS之RunLoop详解与实践
目录
-RunLoop的概念
-RunLoop逻辑与实现
-RunLoop在iOS中运用
-RunLoop实践
-RunLoop的概念
苹果在文档里,是这样定义RunLoop的 :
Run loops are part of the fundamental infrastructure associated with threads.
A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.
RunLoop
是与线程相关的基础功能,RunLoop
是用于调度工作,并协调接收传入事件的事件处理循环。RunLoop
的目标是让线程有任务时工作,没有任务处理时休眠。
RunLoop与线程的关系
线程在处理完自己的任务后一般会退出,为了实现线程不退出能够随时处理任务的机制被称为EventLoop
,node.js
的事件处理,windows程序的消息循环,iOS、OSX的RunLoop
都是这种机制。
线程和RunLoop
是一一对应的,关系保存在全局的字典里。
在主线程中,程序启动时,系统默认添加了有kCFRunLoopDefaultMode
和 UITrackingRunLoopMode
两个预置Mode
的RunLoop
,保证程序处于等待状态,如果接收到来自触摸事件等,就会执行任务,否则处于休眠中。
线程创建时并没有RunLoop
,(主线程除外),RunLoop
不能创建,只能主动获取才会有。RunLoop
的创建是在第一次获取时,RunLoop
的销毁是发生在线程结束时。只能在一个线程中获取自己和主线程的RunLoop
。
RunLoop的挂起与唤醒堆栈调用
指定用于唤醒的 mach_port 端口调用 mach_msg 监听唤醒端口,被唤醒前系统内核将这个线程挂起,停留在mach_msg_trap状态。
由另一个线程向内核发送这个端口的msg后,trap状态被唤醒,RunLoop继续工作。
一个运行环接收来自两个不同类型的源事件。输入源传递异步事件,通常消息从另一个线程或从不同的应用程序。定时源提供的同步事件,在预定的时间发生的或重复间隔。这两种类型的源的使用应用特定处理例程,当它到达处理该事件。
EE37B277-0C9E-4C31-B8E1-719F7397D0D9.jpg上图显示了一个运行循环和各种来源的概念结构:
输入源传递异步事件到相应的处理程序,并调用
runUntilDate:
方法(称为线程的相关NSRunLoop
对象)退出。Timer 定时源提供的事件他们的处理程序例程,但不会导致运行循环退出。
-RunLoop逻辑与实现
3026BBF4-3C5A-4501-B6A0-29413F9DF308.png上面的 Source/Timer/Observer
被统称为 mode item
,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。
CFRunLoopSourceRef
是事件产生的地方。Source有两个版本:Source0
和Source1
Source0
只包含了一个回调(函数指针),并不能主动触发事件,需要手动触发,
需要先调用 CFRunLoopSourceSignal(source)
,将这个 Source
标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop)
来唤醒 RunLoop
,让其处理这个事件
Source1
包含了一个 mach_port
和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source
能主动唤醒 RunLoop
的线程,其原理在下面会讲到。
CFRunLoopTimerRef
是基于时间的触发器,可以和NSTimer 混用,包含一个时间长度和回调,加入
RunLoop时,RunLoop会注册对应的时间点,当时间点时,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
};
// 用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(url, currentMode, kCFRunLoopExit);
}
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
0A1E5A26-9719-459F-A07E-8B1ED0D28160.jpg
-RunLoop在iOS中运用
Run Loop的作用
1.使程序一直运行并接受用户输入
2.决定程序在何时应该处理那些Event
3.调用解耦(Message Queue)
4.节省CPU时间
何时调用
1.使用端口port
或自定义输入源input sources
和其他线程通信
2.使用线程的定时器timer
3.Cocoa中使用任何performSelector的方法
4.线程执行周期性任务
RunLoop应用场景
AutoreleasePool
-
主线程App运行时堆栈调用
E513FD63-F9BF-4101-A618-336239CA0194.jpg
程序启动时,苹果在主线程的RunLoop
中添加了两个Observer
用于监视RunLoop
的kCFRunLoopEntry
(唤起)和BeforeWating
(准备休眠)
- RunLoopObserver与Autorelease Pool的关系
A13CAFDB-D73A-47D0-A229-22F88219630C.jpg
UIKit 通过 RunLoopObserver 在 RunLoop 两次 Sleep 间对 Autorelease Pool 进行 Pop 和 Push 将这次 Loop 中产生的 Autorelease 对象释放。
实际开发中,在线程中添加RunLoop时一般也会加上@autoreleasepool
PerformSelecter
调用PerformSelecter:afterDelay
方法时,系统会默认创建一个timer添加到当前线程中去,如果在线程中调用该方法,而没有获取currentRunLoop
,则会失效
GCD
GCD与RunLoop
的部分实现相互用到了对方,RunLoop
的timer
是用dispatch_source_t
实现的,而GCD的dispatch_async()
也用到了RunLoop
当调用dispatch_async(dispatch_get_main_queue(), block)
时,dispatch
会向主线程RunLoop
发送消息,RunLoop
会被唤醒,并从消息中获取block
后回调
定时器 NStimer
NSTimer
实际上就是CFRunLoopTimerRef
,RunLoop
不会非常准确的时间回调,延迟到程度跟当前线程是否繁忙有关,当线程空闲时,timer比较准时,线程繁忙时延迟会比较明显。Timer
有个属性Tolerance
宽容度,到达了某个时间点后容许有多少误差。
當創建一個Timer並添加到DefaultMode
時,Timer就會重復回調,此時滑動TableView時,RunLoop會將mode切換成TrackingRunLoopMode
這時Timer就會被回調,並且也不會影響到滑動操作。如果需要Timer在多個mode下得到回調,有一個辦法就是將Timer分別加入這兩個Mode中,或者加入到頂層RunLoop的commonModeItems
中去,commonModeItems被RunLoop自動更新到具有Common屬性的Mode里去。
网络请求
在基于CFNetwork
的NSURLConnection
发起的NSURLConnectionLoader
线程中,runloop通过基于mach port
的Source0
接收来自底层CFSocket
的通知,同时唤醒delegate
和RunLoop
来处理这些通知。
事件响应 用到了 __IOHIDEventSystemClientQueueCallback()
手势识别 用到了_UIGestureRecognizerUpdateObserver()
界面更新 用到了_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
等等
RunLoop实践
// RunLoop - 创建生命周期跟App相同的常驻线程
#pragma mark - 常驻线程
- (void)viewWillAppear:(BOOL)animated
{
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(longRunLoop) object:nil];
[thread start];
}
- (void)longRunLoop
{
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
//RunLoop - 维护线程的生命周期,让线程不自动退出,isFinished为Yes时退出。 在启动RunLoop之前,必须添加监听的Port或输入源事件sources或者定时源事件timer,否则调用[runloop run]会直接返回,而不会进入循环让线程长驻。如果没有添加任何输入源事件或Timer事件,线程会一直在无限循环空转中,会一直占用CPU时间片,没有实现资源的合理分配。没有while循环且没有添加任何输入源或Timer的线程,线程会直接完成,被系统回收。
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
while (!self.isCancelled && !self.isFinished) {
@autoreleasepool {
[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
}
}
// RunLoop - 不断检测直到满足条件
#pragma mark - 不断检测直到满足条件
- (NSString *)userAgentString
{
NSString *string;
while (string == nil)
{
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
return string;
}
// tableView延迟加载图片的新思路
#pragma mark - 图片延迟加载
- (void)imageViewLoad{
//ImageView的显示 滑动时不加载 只在NSDefaultRunLoopMode模式下显示图片
UIImageView *img;
[img performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
}
// RunLoop - 在一段时间内每隔一会执行某种任务的线程
//Run Loop 有 [acceptInputForMode:beforeDate:] 和[runMode:beforeDate:]方法来指定在一时间之内运行模式。如果不指定时间,Run Loop默认会运行在Default模式下(不断重复调用runMode:NSDefaultRunLoopMode beforeDate:...) 例如需要在应用启动之后,在一定时间内持续更新某项数据。
-(void)loopEvent{
@autoreleasepool {
//在30分钟内,每隔30s执行 run 方法
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
NSTimer * udpateTimer = [NSTimer timerWithTimeInterval:30
target:self
selector:@selector(run) userInfo:nil
repeats:YES];
[runLoop addTimer:udpateTimer forMode:NSRunLoopCommonModes];
[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:60*30]];
}
}
// RunLoop - NSTimer 在不同模式下工作
NSTimer *timer = [NSTimer timerWithTimeInterval:3.0
target:self
selector:@selector(run)
userInfo:nil
repeats:YES];
// timerWithTimeInterval: 方式创建的如果不加入RunLoop,则不会执行
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
/或者/
NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(doSomeThing)
userInfo:nil
repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
//或 [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
[timer fire];
// RunLoop - NSTimer 在线程中创建一个定时器
- (void)viewDidLoad {
[super viewDidLoad];
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(createTimer) object:nil];
[thread start];
}
- (void)createTimer{
[NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(run)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] run];
}
// RunLoop - ReactNative 框架中RCTWebSocket中用到的RunLoop
#pragma mark - ReactNative 框架中RCTWebSocket中用到的RunLoop
NSRunLoop *_runLoop;
dispatch_group_t _waitGroup;
- (void)main
{
_waitGroup = dispatch_group_create();
dispatch_group_enter(_waitGroup);
@autoreleasepool {
_runLoop = [NSRunLoop currentRunLoop];
dispatch_group_leave(_waitGroup);
NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate distantFuture] interval:0.0 target:self selector:@selector(step) userInfo:nil repeats:NO];
[_runLoop addTimer:timer forMode:NSDefaultRunLoopMode];
while ([_runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) { }
assert(NO);
}
}
- (void)step
{
// Does nothing
}
- (NSRunLoop *)runLoop;
{
dispatch_group_wait(_waitGroup, DISPATCH_TIME_FOREVER);
return _runLoop;
}
// RunLoop AFNetWorking之 AFURLRequestSerilization
#pragma mark - AFNetWorking之 AFURLRequestSerilization
//额外提供的一个方法,把request里的bodyStream写到文件
//即可以把multipart发送的内容先生成好保存到文件
- (NSMutableURLRequest *)requestWithMultipartFormRequest:(NSURLRequest *)request
writingStreamContentsToFile:(NSURL *)fileURL
completionHandler:(void (^)(NSError *error))handler
{
NSParameterAssert(request.HTTPBodyStream);
NSParameterAssert([fileURL isFileURL]);
NSInputStream *inputStream = request.HTTPBodyStream;
NSOutputStream *outputStream = [[NSOutputStream alloc] initWithURL:fileURL append:NO];
__block NSError *error = nil;
//
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//新开一条线程做这个事,stream的read和write是阻塞的
[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
/*
*NSInputStream子类
NSURLRequest的setHTTPBodyStream接受的是一个NSInputStream*参数,那我们要自定义inputStream的话,创建一个NSInputStream的子类传给它是不是就可以了?实际上不行,这样做后用NSURLRequest发出请求会导致crash,提示[xx _scheduleInCFRunLoop:forMode:]: unrecognized selector。
这是因为NSURLRequest实际上接受的不是NSInputStream对象,而是CoreFoundation的CFReadStreamRef对象,因为CFReadStreamRef和NSInputStream是toll-free bridged,可以自由转换,但CFReadStreamRef会用到CFStreamScheduleWithRunLoop这个方法,当它调用到这个方法时,object-c的toll-free bridging机制会调用object-c对象NSInputStream的相应函数,这里就调用到了_scheduleInCFRunLoop:forMode:,若不实现这个方法就会crash。详见这篇文章。
*/
[inputStream open];
[outputStream open];
while ([inputStream hasBytesAvailable] && [outputStream hasSpaceAvailable]) {
uint8_t buffer[1024];
NSInteger bytesRead = [inputStream read:buffer maxLength:1024];
if (inputStream.streamError || bytesRead < 0) {
error = inputStream.streamError;
break;
}
NSInteger bytesWritten = [outputStream write:buffer maxLength:(NSUInteger)bytesRead];
if (outputStream.streamError || bytesWritten < 0) {
error = outputStream.streamError;
break;
}
if (bytesRead == 0 && bytesWritten == 0) {
break;
}
}
//NSURLRequest怎么知道数据读完了?
//应该是这个函数返回0时外部就知道stream已经读完,调用它的close方法。
[outputStream close];
[inputStream close];
if (handler) {
dispatch_async(dispatch_get_main_queue(), ^{
handler(error);
});
}
});
//
NSMutableURLRequest *mutableRequest = [request mutableCopy];
mutableRequest.HTTPBodyStream = nil;
return mutableRequest;
}
//AFNetworking之前的版本中AFURLConnectionOperationRunLoop用到了常驻线程
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
// 为了使接收delegate回调能够在后台线程中执行,且该线程不会提前被回收
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 在这里通过监听MachPort使线程不会回收,MachPort并没有发送消息
[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;
}
//
1432798974517485.png相关资料:
官方文档 NSRunLoop Class Reference , CFRunLoop Reference.
CFRunLoopRef 的代码是开源的,你可以在这里http://opensource.apple.com/tarballs/CF/ 下载到整个 CoreFoundation 的源码来查看。
Swift 版跨平台的 CoreFoundation :https://github.com/apple/swift-corelibs-foundation/,这个版本的源码可能和现有 iOS 系统中的实现略不一样,但更容易编译,而且已经适配了 Linux/Windows。