我的runloop学习笔记

时间:2022-11-02 20:37:30

前言:公司项目终于忙的差不多了,最近比较闲,想起叶大说过的iOS面试三把刀,GCD、runtime、runloop,runtime之前已经总结过了,GCD在另一篇博客里也做了一些小总结,今天准备把runloop搞一下,之前看了很多资料,也按照对应的在项目中的应用点写了几个demo,其中两个demo非原创,直接拿过来借花献佛了。今天才有时间把它们总结一下,并记录下来。关于runloop的基础知识我就不多介绍了,网上一堆介绍的文章,这里只说实际项目中的使用点,毕竟东西是拿来用的。

1、关于轮播图

第一个使用场景是比较常见的,现在大部分app首页都会有一个轮播图,而和轮播图在同一个界面的通常会有一个scrollView,如果想到不到,可以看一下淘宝首页。在我们实际去实现类似界面的时候,会发现,当我们滚动scrollView的时候,轮播图是会停止自动轮播的,这是为什么呢?这里就需要了解到runloop。

1、简便起见,我在demo里放了一个textView,因为它的父控件也是scrollView,也是可以滚动的。同时,轮播图的自动轮播是有NStime(定时器)实现的,所以,我们在向主界面放了一个textView之后,再在主线程添加一个timer,代码如下:

1 - (void)timer
2 {
3 NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
4 //添加一个定时器,需要将它添加到NSRunLoopCommonModes状态才能在scroll滚动的时候不受影响,常用于tableView或CollectionView中有轮播图的情况
5 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
6
7 //子线程的情况下需要自己run,主线程不要这行代码
8 [[NSRunLoop currentRunLoop] run];
9 }

第3行代码可以看到每两秒执行一次run方法,run方法:

1 - (void)run {
2 NSLog(@"run--%@",[NSThread currentThread]);
3 }

打印当前所在线程。如果简单了解过runloop就会知道runloop的几种运行模式,其中默认模式是是NSDefaultRunLoopMode,存在scroll滚动的时候的模式是UITrackingRunLoopMode,当scroll没有滚动的时候主线程runloop是NSDefaultRunLoopMode模式,而当存在scroll滚动的时候主线程runloop的模式会改变为UITrackingRunLoopMode,而在UITrackingRunLoopMode模式下,timer是不生效的,因此此时打印就会停止,如同实际应用中轮播图会停止滚动。这就需要第5行代码,将timer添加到NSRunLoopCommonModes状态,才能在scroll滚动的时候不受影响,常用于tableView或CollectionView中有轮播图的情况。NSRunLoopCommonModes:这是一个伪模式,为一组runloop mode的集合,将timer加入此模式意味着在Common Modes中包含的所有模式下都可以处理。在Cocoa应用程序中,默认情况下Common Modes包含default modes,modal modes,event Tracking modes。第8行代码暂时不用管,实际用的时候也不要加这行代码,下面会提到。

 

其实对于我们平常使用来说,这一节到现在已经可以结束了,上面的东西在实际应用当中对于这个场景已经足够了,但是我想再补充一点其他的,设计到GCD,感兴趣可以看一下。

2、首先了解一个常识性的东西,GCD创建的定时器是不受runloop影响的,所以我们其实还可以用GCD来创建定时器。

 1 - (void)useGCD {
2 // 获得队列
3 //在子线程执行
4 // dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
5 //在主线程执行
6 dispatch_queue_t queue = dispatch_get_main_queue();
7
8 // 创建一个定时器(dispatch_source_t本质还是个OC对象)
9 self.GCDtimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
10
11 // 设置定时器的各种属性(几时开始任务,每隔多长时间执行一次)
12 // GCD的时间参数,一般是纳秒(1秒 == 10的9次方纳秒)
13 // 比当前时间晚1秒开始执行
14 dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
15
16 //每隔一秒执行一次
17 uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
18 dispatch_source_set_timer(self.GCDtimer, start, interval, 0);
19
20 // 设置回调
21 dispatch_source_set_event_handler(self.GCDtimer, ^{
22 NSLog(@"------------%@", [NSThread currentThread]);
23
24 });
25
26 // 启动定时器
27 dispatch_resume(self.GCDtimer);
28 }

在子线程应该用不到,因为UI操作一般在主线程,所以第4行可以忽略,当然如果你还有其他需求要选择在子线程执行,也可以用。步骤很简单:1、获取主线程;2、创建定时器;3、设置定时器;4、设置定时器回调;5、启动定时器。上面注释已经很详细了就不做过多解释了,下面就看一下在子线程添加timer。

3、子线程添加timer

子线程添加一个timer有两种方式,一个是用NSThread开子线程,一个是用GCD,注意这一小节不适用于上面的轮播图场景,因为操作UI不会在子线程。同时,子线程添加的runloop的定时器不会受主线程runloop的模式的影响,所以如果定时器在子线程runloop,主线程runloop无论有没有scroll滚动,也就是无论有没有改变runloop模式,子线程runloop定时器都不会受影响。

下面直接看代码:

1 - (void)timer1 {
2 self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(timer) object:nil];
3 //需要自己开启thread
4 [self.thread start];
5 }

很简单,在子线程调用上面的timer方法,但是注意,这时候就需要这行代码了哦“[[NSRunLoop currentRunLoop] run];”。

 第二种方式也很简单,用GCD开子线程:

1     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
2 [self timer];
3 });

好了,到这里第一小节就结束了。

demo:https://github.com/alan12138/Classification-of-knowledge-points/tree/master/runloop/ATRunLoopScroll

2、runloop实现线程保活

这个比较经典的案例是AFNetworking中用过这个方法。有两种方法,添加事件源,或添加timer。

1、下面先看添加事件源。

先开一个子线程:

1  self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
2 self.thread.name = @"alan";
3 [self.thread start];

在子线程中为runloop添加事件源:

 1 //添加source避免runloop退出
2 - (void)run
3 {
4 NSLog(@"----------run----%@", [NSThread currentThread]);
5 //程序开始时创建,会看到一个默认的Autorelease pool,程序退出时销毁,按照对Autorelease的理解,岂不是所有autorelease pool里的对象在程序退出时才release, 这样跟内存泄露有什么区别?结果是,对于每一个Runloop, 系统会隐式创建一个Autorelease pool,这样所有的release pool会构成一个象CallStack一样的一个栈式结构,在每一个Runloop结束时,当前栈顶的Autorelease pool会被销毁,这样这个pool里的每个Object会被release。
6 //thread是不会为runloop自动创建autorelease pool的,所以我们可以看到子线程中会有手动写的autorelease pool代码。
7 @autoreleasepool{
8 /*如果不加这句,会发现runloop创建出来就挂了,因为runloop如果没有CFRunLoopSourceRef事件源输入或者定时器,就会立马消亡。
9 下面的方法给runloop添加一个NSport,就是添加一个事件源,也可以添加一个定时器,或者observer,让runloop不会挂掉*/
10 [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
11
12 [[NSRunLoop currentRunLoop] run];
13 }
14 }

注释比较多,但是比较重要,慢慢看。现在我们可以测试一下,这个线程有没有死:

1 - (void)test
2 {
3 NSLog(@"----------test----%@", [NSThread currentThread]);
4 }
5
6 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
7 {
8 [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
9 }

会发现打印的线程就是之前创建的线程,证明线程一直存活。

2、第二种方式,添加一个timer:

1 //添加timer避免runloop退出
2 - (void)run1 {
3 NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
4 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
5 [[NSRunLoop currentRunLoop] run];
6 }

每两秒执行一次test方法,线程一直存活。

demo:https://github.com/alan12138/Classification-of-knowledge-points/tree/master/runloop/ResidentThread

 

3、runloop监听

这里只做简单介绍,关于runloop监听的具体使用场景下面会有案例具体介绍。

先创建一个按钮,并设置点击事件。

1 UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
2 [btn setBackgroundColor:[UIColor redColor]];
3 [btn addTarget:self action:@selector(btnClick:) forControlEvents:UIControlEventTouchUpInside];
4 [self.view addSubview:btn];
5 self.btn = btn;

添加监听

 1 - (void)observer
2 {
3 /**
4 * 这个有一个常见应用场景是cell的高度缓存,这个操作应该在runloop空闲的时候进行,
5 * 也就是块要休眠之前;还有一个是检测卡顿,后面会介绍。
6 */
7 // 创建observer,参数kCFRunLoopBeforeWaiting表示监听休眠前的状态,也就是在休眠前做一些操作
8 CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopBeforeWaiting, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
9 NSLog(@"do something---%zd", activity);
10 });
11 // 添加观察者:监听RunLoop的状态
12 CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
13
14 // 释放Observer
15 CFRelease(observer);
16 }

注意到kCFRunLoopBeforeWaiting,用来说明什么状态的时候监听,这个的意思是runloop进入休眠之前的状态。

运行一下demo,会看见监听回调方法实现的打印。

demo:https://github.com/alan12138/Classification-of-knowledge-points/tree/master/runloop/RunloopObserver

 

4、runloop实现图片加载性能优化

1、先来看个简单的

首先在主控制器中拖入一个textView,之后再viewDidLoad方法中添加一个UIImageView。然后,在touchesBegan方法中,调用这个方法[self useImageView];

1 - (void)useImageView
2 {
3 // 只在NSDefaultRunLoopMode模式下显示图片(为了使滚动更加流畅,scroll滚动的时候不显示图片,尤其是当图片很大的时候尤其有意义)
4 [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"appointment_duty_img"] afterDelay:1.0 inModes:@[NSDefaultRunLoopMode]];
5 }

假想一下图片是在tableView或collectionView中的,而且可能不止要加载一张,这样做可以使滑动界面更加流畅,尤其是大图的情况下,下面我们就看一下大图加载。

demo:https://github.com/alan12138/Classification-of-knowledge-points/tree/master/runloop/ShowPicture

2、runloop大图加载

非原创,就直接把demo拿过来用了.

场景:tableView里面每个cell需要显示三张图片,而且这些图片都是很大的图,就说明需要消耗较大的性能。如果不做优化用常规方法的话,会很卡。

建议继续往下看之前先把demo看一下,不然可能不知道在说什么,demo:https://github.com/alan12138/Classification-of-knowledge-points/tree/master/runloop/RunLoopWorkDistribution

所以在这里是这样做的:首先,在cellForRowAtIndexPath方法中添加子控件:

这里它使用了工具类中的一个方法

1 - (void)addTask:(DWURunLoopWorkDistributionUnit)unit withKey:(id)key;

其中第一个参数是一个block,在这个block中实现添加cell子控件的回调。

然后我们直接去看监听回调方法,因为它是监听到滑动停止,也就是kCFRunLoopBeforeWaiting的时候才开始调用监听回调方法

1 BOOL result = NO;
2 while (result == NO && runLoopWorkDistribution.tasks.count) {
3 DWURunLoopWorkDistributionUnit unit = runLoopWorkDistribution.tasks.firstObject;
4 result = unit();
5 [runLoopWorkDistribution.tasks removeObjectAtIndex:0];
6 [runLoopWorkDistribution.tasksKeys removeObjectAtIndex:0];
7 }

你会看到这段代码,这段代码是做什么的呢?我在demo中添加了很详细的注释来解释它的原理:

滑动列表的时候会把绘制任务添加到数组里面,但是限制数组中任务的数量最多30个,滑动停止的时候runloop状态进入待休眠状态,之后开始在数组中取出任务并回调添加图片的动作,这个时候才真正开始显示图片的动作;会发现每次执行显示图片就会返回YES,返回YES这个任务就会在执行完毕的时候从数组中移除并结束while循环,也就是结束了监听回调方法,只能等待下一次监听到待休眠状态,也就是下一个runloop才能再执行下一个显示图片的动作,这就实现了每个runloop只显示一张图片的效果。

     接着说一下返回NO的情况,返回NO的时候不会添加显示图片的任务,但即使是一个if判断也属于一个任务,也会在数组中作为一个任务存在;因为返回NO的时候while循环会继续执行,也就是没有图片任务的时候这个while不会结束。

再回到cellForRowAtIndexPath方法中:

1 [[DWURunLoopWorkDistribution sharedRunLoopWorkDistribution] addTask:^BOOL(void) {
2 if (![cell.currentIndexPath isEqual:indexPath]) {
3 return NO;
4 }
5 [ViewController task_2:cell indexPath:indexPath];
6 return YES;
7 } withKey:indexPath];

这里是靠什么判断YESNO的呢:我是这么理解的,因为滑动列表的时候这个block是不会执行的,但是任务会添加到数组中,但是你添加任务的时候的indexPath和停止滑动的时候显示出来的cellindexPath不一定是对应的,所以当执行block回调的时候,不在界面中的indexPath的任务是不应该添加图片的,否则会把之前添加任务的时候的indexPath行显示的内容,添加到现有界面的indexPathcell上。

写的不多,但是基本上核心思想都在这里了,有点绕,但是我在demo的相应位置也做了注释,仔细看一下demo应该就能理解了。

5、runloop实现卡顿检测器

先放个demo看一下,具体以后再补充吧,估计有了上边的基础,看这个也不成问题。

demo:https://github.com/alan12138/Classification-of-knowledge-points/tree/master/runloop/PerformanceMonitor-master