iOS runloop和线程有什么关系?
Run loop,正如其名,loop表示某种循环,和run放在一起就表示一直在运行着的循环。实际上,run loop和线程是紧密相连的,可以这样说run loop是为了线程而生,没有线程,它就没有存在的必要。Run loops是线程的基础架构部分, Cocoa 和 CoreFundation 都提供了 run loop 对象方便配置和管理线程的 run loop (以下都以 Cocoa 为例)。每个线程,包括程序的主线程( main thread )都有与之相应的 run loop 对象。
runloop 和线程的关系:
1 一条线程对应一个RunLoop对象,每条线程都有唯一一个与之对应的RunLoop对象。
2 我们只能在当前线程中操作当前线程的RunLoop,而不能去操作其他线程的RunLoop。
3 RunLoop对象在第一次获取RunLoop时创建,销毁则是在线程结束的时候。
4 主线程的RunLoop对象系统自动帮助我们创建好了(原理如下),而子线程的RunLoop对象需要我们主动创建。
1. 主线程的run loop默认是启动的。
iOS的应用程序里面,程序启动后会有一个如下的main()函数
上边的代码中开启RunLoop的过程可以简单的理解为如下代码:
从上边可看出,程序一直在do-while循环中执行,所以UIApplicationMain函数一直没有返回,我们在运行程序之后程序不会马上退出,会保持持续运行状态。
重点是UIApplicationMain()函数,这个方法会为main thread设置一个NSRunLoop对象,这就解释了:为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。
2. 对其它线程来说,run loop默认是没有启动的,如果你需要更多的线程交互则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。
3. 在任何一个 Cocoa 程序的线程中,都可以通过以下代码来获取到当前线程的 run loop
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
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的几种应用。
1 NSTimer的使用
NSTimer的使用方法在讲解CFRunLoopTimerRef类的时候详细讲解过,具体参考上边2.3 CFRunLoopTimerRef。
2 ImageView推迟显示
有时候,我们会遇到这种情况:
当界面中含有UITableView,而且每个UITableViewCell里边都有图片。这时候当我们滚动UITableView的时候,如果有一堆的图片需要显示,那么可能会出现卡顿的现象。
怎么解决这个问题呢?
这时候,我们应该推迟图片的显示,也就是ImageView推迟显示图片。有两种方法:
1. 监听UIScrollView的滚动
因为UITableView继承自UIScrollView,所以我们可以通过监听UIScrollView的滚动,实现UIScrollView相关delegate即可。
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
这两方法一个是停止拖拽时调用 一个是当滚动视图嘎然而止 时调用 在这两方法里面写给ImageView加载图片的方法 就能避免因为加载图片导致UITableView滚动时卡顿的问题
*** -(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
//如果tableview停止滚动,开始加载图像
if(!decelerate){
[self loadImagesForOnscreenRows];
}
}
*** -(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
//如果tableview停止滚动,开始加载图像
[self loadImagesForOnscreenRows];
}
2. 利用PerformSelector设置当前线程的RunLoop的运行模式
kCFRunLoopDefaultMode:App的默认运行模式,通常主线程是在这个运行模式下运行
UITrackingRunLoopMode:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)
然后 我们滑动UITableView时候 RunLoop的运行模式就会变为UITrackingRunLoopMode
所以我们把给ImageView加载图片的方法用PerformSelector设置当前线程的RunLoop的运行模式kCFRunLoopDefaultMode 这样滑动时候就不会执行加载图片的方法了
也就能避免因为加载图片导致UITableView滚动时卡顿的问题
利用performSelector方法为UIImageView调用setImage:方法,并利用inModes将其设置为RunLoop下NSDefaultRunLoopMode运行模式。代码如下:
[cell performSelector:@selector(setImage)withObject:nil afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
下边利用Demo演示一下该方法。
在项目中的Main.storyboard中添加一个UIImageView,并添加属性,并简单添加一下约束(不然无法显示)如下图所示。
添加UIImageView
在项目中拖入一张图片,比如下图。
1 然后我们在touchesBegan方法中添加下面的代码,在Demo中请在touchesBegan中调用[self showDemo3];方法。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"tupian"] afterDelay:4.0 inModes:@[NSDefaultRunLoopMode];
}
1 运行程序,点击一下屏幕,然后拖动UIText View,拖动4秒以上,发现过了4秒之后,UIImageView还没有显示图片,当我们松开的时候,则显示图片,效果如下:
这样我们就实现了在拖动完之后,在延迟显示UIImageView。
3 后台常驻线程(很常用)
我们在开发应用程序的过程中,如果后台操作特别频繁,经常会在子线程做一些耗时操作(下载文件、后台播放音乐等),我们最好能让这条线程永远常驻内存。
那么怎么做呢?
添加一条用于常驻内存的强引用的子线程,在该线程的RunLoop下添加一个Sources,开启RunLoop。
具体实现过程如下:
在项目的ViewController.m中添加一条强引用的thread线程属性,如下图:
在viewDidLoad中创建线程self.thread,使线程启动并执行run1方法,代码如下。在Demo中,请在viewDidLoad调用[self showDemo4];方法。
运行之后发现打印了----run1-----,而未开启RunLoop则未打印。
这时,我们就开启了一条常驻线程,下边我们来试着添加其他任务,除了之前创建的时候调用了run1方法,我们另外在点击的时候调用run2方法。
那么,我们在touchesBegan中调用PerformSelector,从而实现在点击屏幕的时候调用run2方法。具体代码如下:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//利用performSelector,在self.thread的线程中调用run2方法执行任务
[self performSelector:@selector(run2)onThread:self.thread withObject:nil waitUntilDone:NO];
}
-(void)run2
{
NSLog(@"----run2------");
}
经过运行测试,除了之前打印的----run1-----,每当我们点击屏幕,都能调用----run2------。
这样我们就实现了常驻线程的需求。