iOS

iOS runloop和线程有什么关系?

2019-06-12  本文已影响9人  进击的阿牛哥

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------

这样我们就实现了常驻线程的需求。

上一篇下一篇

猜你喜欢

热点阅读