先不扯什么概念,因为自己之前对概念理解不深刻,只有在项目中真正用到了才能真正体会。
使用场景一:NSTimer 倒计时
- 倒计时有两种创建形式
- 第一种 需要手动将定时器添加到NSRunLoop
[NSThread detachNewThreadSelector:@selector(startSteamTimer) toTarget:self withObject:nil];
- (NSTimer *) startSteamTimer{
return [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
}
//这样的run方法永远不会调用必须 加入下面的代码
[[NSRunLoop currentRunLoop] addTimer:_streamTimer forMode:NSDefaultRunLoopMode];
//若在非主线程 需要自己启动RunLoop [[NSRunLoop currentRunLoop] run]
- 第二种 创建NSTimer 自动添加NSRunLoop
[NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:
@selector(_steamTimerAction) userInfo:nil repeats:YES];
使用场景二:用FTP 协议上传和下载文件时 用到了NSRunLoop
CFReadStreamRef readStreamRef = CFReadStreamCreateWithFTPURL(NULL, ( __bridge CFURLRef) url);
CFReadStreamSetProperty(readStreamRef,
kCFStreamPropertyFTPAttemptPersistentConnection,
kCFBooleanFalse);
CFReadStreamSetProperty(readStreamRef, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
CFReadStreamSetProperty(readStreamRef, kCFStreamPropertyFTPUsePassiveMode, kCFBooleanTrue);
CFReadStreamSetProperty(readStreamRef, kCFStreamPropertyFTPFetchResourceInfo, kCFBooleanTrue);
CFReadStreamSetProperty(readStreamRef, kCFStreamPropertyFTPUserName, (__bridge CFStringRef) self.ftpUsername);
CFReadStreamSetProperty(readStreamRef, kCFStreamPropertyFTPPassword, (__bridge CFStringRef) self.ftpPassword);
//
CFReadStreamSetProperty(readStreamRef, kCFStreamPropertyFTPProxy, kCFBooleanTrue);
//
self.dataStream = ( __bridge_transfer NSInputStream *) readStreamRef;
self.dataStream.delegate = self;
if (self.dataStream == nil) {
[self.delegate ftpError:self withErrorCode:FTPClientCantReadStream];
}
//这里是重点
[self performSelector:@selector(scheduleInCurrentThread:)
onThread:[[self class] networkThread]
withObject:self.dataStream
waitUntilDone:YES];
// [self.dataStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[self.dataStream open];
#pragma thread management
+ (NSThread *)networkThread {
static NSThread *networkThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
networkThread =
[[NSThread alloc] initWithTarget:self
selector:@selector(networkThreadMain:)
object:nil];
[networkThread start];
});
return networkThread;
}
+ (void)networkThreadMain:(id)unused {
do {
@autoreleasepool {
[[NSRunLoop currentRunLoop] run];
}
} while (YES);
}
- (void)scheduleInCurrentThread:(NSStream*)aStream
{
[aStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
forMode:NSRunLoopCommonModes];
}
看了下方法的含义:Unless the client is polling the stream, it is responsible for ensuring that the stream is scheduled on at least one run loop and that at least one of the run loops on which the stream is scheduled is being run
确保流至少在一个运行循环上调度,并且流调度所在的至少一个运行循环正在运行
在流对象放入run loop且有流事件(有可读数据)发生时,流对象会向代理对象发送stream:handleEvent:消息。在打开流之前,我们需要调用流对象的scheduleInRunLoop:forMode:方法,这样做可以避免在没有数据可读时阻塞代理对象的操作。我们需要确保的是流对象被放入正确的run loop中,即放入流事件发生的那个线程的run loop中。
那为什么要RunLoop
字面意思运行的循环,就像操作系统一样,手机一有电话就有反应,系统里面在循环“跑圈”,也借助do-while 死循环理解
- RunLoop 实际上是一个对象,这个对象在循环中用来处理程序运行过程中出现的各种事件(比如说触摸事件、UI刷新事件、定时器事件、Selector事件),从而保持程序的持续运行。
- RunLoop 在没有事件处理的时候,会使线程进入睡眠模式,从而节省 CPU 资源,提高程序性能。
RunLoop 和线程
RunLoop 和线程是息息相关的,我们知道线程的作用是用来执行特定的一个或多个任务,在默认情况下,线程执行完之后就会退出,就不能再执行任务了。这时我们就需要采用一种方式来让线程能够不断地处理任务,并不退出。所以,我们就有了 RunLoop。
一条线程对应一个RunLoop对象,每条线程都有唯一一个与之对应的 RunLoop 对象。
RunLoop 并不保证线程安全。我们只能在当前线程内部操作当前线程的 RunLoop 对象,而不能在当前线程内部去操作其他线程的 RunLoop 对象方法。
RunLoop 对象在第一次获取 RunLoop 时创建,销毁则是在线程结束的时候。
主线程的 RunLoop 对象系统自动帮助我们创建好了(原理如 1.3 所示),而子线程的 RunLoop对象需要我们主动创建和维护。
官方模型
通过 Input sources(输入源)和 Timer sources(定时源)两种来源等待接受事件;然后对接受到的事件通知线程进行处理,并在没有事件的时候让线程进行休息
RunLoop 相关类
下面我们来了解一下Core Foundation框架下关于 RunLoop 的 5 个类,只有弄懂这几个类的含义,我们才能深入了解 RunLoop 的运行机制。
CFRunLoopRef:代表 RunLoop 的对象
CFRunLoopModeRef:代表 RunLoop 的运行模式
CFRunLoopSourceRef:就是 RunLoop 模型图中提到的输入源 / 事件源
CFRunLoopTimerRef:就是 RunLoop 模型图中提到的定时源
CFRunLoopObserverRef:观察者,能够监听 RunLoop 的状态改变
一个RunLoop对象(CFRunLoopRef)中包含若干个运行模式(CFRunLoopModeRef)。而每一个运行模式下又包含若干个输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef)。
- 每次 RunLoop 启动时,只能指定其中一个运行模式(CFRunLoopModeRef),这个运行模式(CFRunLoopModeRef)被称作当前运行模式(CurrentMode)。
- 如果需要切换运行模式(CFRunLoopModeRef),只能退出当前 Loop,再重新指定一个运行模式(CFRunLoopModeRef)进入。
- 这样做主要是为了分隔开不同组的输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef),让其互不影响
CFRunLoopRef 类 代表 RunLoop 的对象
获取方式
- Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的 RunLoop 对象
CFRunLoopGetMain(); // 获得主线程的 RunLoop 对象
当然,在Foundation 框架下获取 RunLoop 对象类的方法如下:
- Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的 RunLoop 对象
[NSRunLoop mainRunLoop]; // 获得主线程的 RunLoop 对象
CFRunLoopModeRef. 运行模式
1、kCFRunLoopDefaultMode:App的默认运行模式,通常主线程是在这个运行模式下运行
2、UITrackingRunLoopMode:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)
3、UIInitializationRunLoopMode:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用
4、GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到
5、kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式(后边会用到)
其中kCFRunLoopDefaultMode、UITrackingRunLoopMode、kCFRunLoopCommonModes是我们开发中需要用到的模式
CFRunLoopTimerRef
CFRunLoopTimerRef是定时源(RunLoop模型图中提到过),理解为基于时间的触发器,基本上就是NSTimer(哈哈,这个理解就简单了吧)
下面我们来演示下CFRunLoopModeRef和CFRunLoopTimerRef结合的使用用法,从而加深理解
UITextView *tv = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width/2, self.view.frame.size.height/2)];
tv.text =@"放很多字出现滚动条";
tv.backgroundColor = UIColor.redColor;
[self.view addSubview:tv];
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_streamTimer forMode:NSDefaultRunLoopMode];
然后运行,这个时候我们发现如何我们不拖动UITextView的滚动条,定时器会稳定的每隔2秒调用run方法打印
但拖动的时候,我们发现没有打印
这是因为:
当我们不做任何操作的时候,RunLoop处于NSDefaultRunLoopMode下
而当我们拖动UITextView 的时候,RunLoop就结束NSDefaultRunLoopMode,切换到了UITrackingRunLoopMode模式下,这种模式下没有添加NSTimer,所有我们定时器不工作了
但当我送松开滚动条,RunLoop就结束UITrackingRunLoopMode模式,又切换回NSDefaultRunLoopMode模式,所以NSTimer就又开始正常工作了你可以试着将上述代码中的[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];语句换为[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];,也就是将定时器添加到当前RunLoop的UITrackingRunLoopMode下,你就会发现定时器只会在拖动Text View的模式下工作,而不做操作的时候定时器就不工作。
那难道我们就不能在这两种模式下让NSTimer都能正常工作吗?当然可以,这就用到了我们之前说过的伪模式(kCFRunLoopCommonModes),这其实不是一种真实的模式,而是一种标记模式,意思就是可以在打上Common Modes标记的模式下运行
CFRunLoopSourceRef
CFRunLoopSourceRef是事件源(RunLoop模型图中提到过),CFRunLoopSourceRef有两种分类方法。
- 第一种按照官方文档来分类(就像RunLoop模型图中那样):
Port-Based Sources(基于端口)
Custom Input Sources(自定义)
Cocoa Perform Selector Sources
- 第二种按照函数调用栈来分类:
Source0 :非基于Port
Source1:基于Port,通过内核和其他线程通信,接收、分发系统事件
这两种分类方式其实没有区别,只不过第一种是通过官方理论来分类,第二种是在实际应用中通过调用函数来分类。
备注:断点可以看到函数调用栈“Source0”
CFRunLoopObserverRef
CFRunLoopObserverRef是观察者,用来监听RunLoop的状态改变
CFRunLoopObserverRef可以监听的状态改变有以下几种:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop:1
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer:2
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source:4
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠:32
kCFRunLoopAfterWaiting = (1UL << 6), // 即将从休眠中唤醒:64
kCFRunLoopExit = (1UL << 7), // 即将从Loop中退出:128
kCFRunLoopAllActivities = 0x0FFFFFFFU // 监听全部状态改变
};
- 1、在ViewController.m中添加如下代码
// 创建观察者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"监听到RunLoop发生改变---%zd",activity);
});
// 添加观察者到当前RunLoop中
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 释放observer,最后添加完需要释放掉
CFRelease(observer);
可以看到RunLoop的状态在不断的改变,最终变成了状态 32,也就是即将进入睡眠状态,说明RunLoop之后就会进入睡眠状态
RunLoop原理
好了,五个类都讲解完了,下边开始放大招了。这下我们就可以来理解RunLoop的运行逻辑了。
这张图对于我们的理解RunLoop来说太有帮助了,下边我们可以理解RunLoop逻辑
每次在开启RunLoop的时候,所在线程所在线程的RunLoop会自动处理之前未处理的事件,并且通知相关的观察者
具体的顺序如下:
- 1、通知观察者RunLoop已经启动
- 2、通知观察者即将要开始的定时器
- 3、通知观察者任何即将启动的非基于端口的源
- 4启动任何准备好的非基于端口的源
- 5、如果基于端口的源准备好并处于等待状态,立即启动;并进入步骤9
- 6、通知观察者线程进入休眠状态
- 7、将线程置于休眠知道任一下面的事件发生:
某一事件到达基于端口的源
定时器启动
RunLoop设置的时间已经超时
RunLoop被显示唤醒
- 8、通知观察者线程将被唤醒
- 9、处理未处理的事件
如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2
如果输入源启动,传递相应的消息
如果RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2
- 10、通知观察者RunLoop结束。
runLoop一般的使用
我们在开发应用程序的过程中,如果后台操作特别频繁,经常会在子线程做一些好事的操作(下载文件、后台播放音乐等),我们最好能让这条线程永远常驻内存
那么怎么做呢?其实开头案例我有使用
添加一条常驻内存的子线程,在该线程的RunLoop下添加一Sources,开启RunLoop
具体实现过程如下:
_threadP = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
[_threadP start];
}
- (void)run1 {
NSLog(@"runn1");
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
NSLog(@"runloop没启动成功");
NSLog(@"---run:%@",[NSThread currentThread]);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self performSelector:@selector(run2222) onThread:self.threadP withObject:nil waitUntilDone:NO];
NSLog(@"runw222");
}
- (void)run2222 {
NSLog(@"我在这个线程想干啥就可以干啥");
NSLog(@"---run:%@",[NSThread currentThread]);
}
运行之后发现打印了----run1-----,而未开启RunLoop则未打印