我的runloop学习笔记

前言:公司项目终于忙的差不多了,最近比较闲,想起叶大说过的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

posted @ 2016-09-27 17:06  Alan12138  阅读(1364)  评论(1编辑  收藏  举报