我的runloop学习笔记

时间:2022-08-27 00:23:00

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

1、关于轮播图

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

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

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

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

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

打印当前所在线程。如果简单了解过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来创建定时器。

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

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

3、子线程添加timer

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

下面直接看代码:

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

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

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

     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, ), ^{
[self timer];
});

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

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

2、runloop实现线程保活

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

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

先开一个子线程:

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

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

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

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

 - (void)test
{
NSLog(@"----------test----%@", [NSThread currentThread]);
} - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

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

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

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

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

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

3、runloop监听

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

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

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

添加监听

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

注意到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];。

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

假想一下图片是在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方法中添加子控件:

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

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

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

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

 BOOL result = NO;
while (result == NO && runLoopWorkDistribution.tasks.count) {
DWURunLoopWorkDistributionUnit unit = runLoopWorkDistribution.tasks.firstObject;
result = unit();
[runLoopWorkDistribution.tasks removeObjectAtIndex:];
[runLoopWorkDistribution.tasksKeys removeObjectAtIndex:];
}

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

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

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

再回到cellForRowAtIndexPath方法中:

 [[DWURunLoopWorkDistribution sharedRunLoopWorkDistribution] addTask:^BOOL(void) {
if (![cell.currentIndexPath isEqual:indexPath]) {
return NO;
}
[ViewController task_2:cell indexPath:indexPath];
return YES;
} withKey:indexPath];

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

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

5、runloop实现卡顿检测器

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

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

我的runloop学习笔记的更多相关文章

  1. 主线程 RunLoop 学习笔记

    以下为主RunLoop 的输出,能够看到不同的source0,source1,observer ---------------------------------- CFRunLoop{wakeup ...

  2. RAC学习笔记

    RAC学习笔记 ReactiveCocoa(简称为RAC),是由Github开源的一个应用于iOS和OS开发的新框架,Cocoa是苹果整套框架的简称,因此很多苹果框架喜欢以Cocoa结尾. 在学习Re ...

  3. iOS学习笔记-精华整理

    iOS学习笔记总结整理 一.内存管理情况 1- autorelease,当用户的代码在持续运行时,自动释放池是不会被销毁的,这段时间内用户可以安全地使用自动释放的对象.当用户的代码运行告一段 落,开始 ...

  4. iOS学习笔记总结整理

    来源:http://mobile.51cto.com/iphone-386851_all.htm 学习IOS开发这对于一个初学者来说,是一件非常挠头的事情.其实学习IOS开发无外乎平时的积累与总结.下 ...

  5. js学习笔记:webpack基础入门(一)

    之前听说过webpack,今天想正式的接触一下,先跟着webpack的官方用户指南走: 在这里有: 如何安装webpack 如何使用webpack 如何使用loader 如何使用webpack的开发者 ...

  6. PHP-自定义模板-学习笔记

    1.  开始 这几天,看了李炎恢老师的<PHP第二季度视频>中的“章节7:创建TPL自定义模板”,做一个学习笔记,通过绘制架构图.UML类图和思维导图,来对加深理解. 2.  整体架构图 ...

  7. PHP-会员登录与注册例子解析-学习笔记

    1.开始 最近开始学习李炎恢老师的<PHP第二季度视频>中的“章节5:使用OOP注册会员”,做一个学习笔记,通过绘制基本页面流程和UML类图,来对加深理解. 2.基本页面流程 3.通过UM ...

  8. 2014年暑假c&num;学习笔记目录

    2014年暑假c#学习笔记 一.C#编程基础 1. c#编程基础之枚举 2. c#编程基础之函数可变参数 3. c#编程基础之字符串基础 4. c#编程基础之字符串函数 5.c#编程基础之ref.ou ...

  9. JAVA GUI编程学习笔记目录

    2014年暑假JAVA GUI编程学习笔记目录 1.JAVA之GUI编程概述 2.JAVA之GUI编程布局 3.JAVA之GUI编程Frame窗口 4.JAVA之GUI编程事件监听机制 5.JAVA之 ...

随机推荐

  1. PyQt4入门学习笔记(五)

    PyQt4里的对话框 对话框是大多数GUI应用中不可分割的一部分.一个对话框是两者或多者的会话.在GUI内,对话框是应用向人说话的方式.一个对话框可以用来输入数据,修改数据,改变应用设置等等. QtG ...

  2. petapoco定制&comma;比较SQL事务&comma;存储过程&comma;分布式事务&lpar;MSDTC&rpar;的区别和场景

    使用分布式事务时 就锁死了,而且是只锁编辑的行 使用.netSQL事务一定要执行了一个CUD的SQL才会锁死,而且也是锁行,但是也锁读的行 .netSQL事务要在这里才锁死 结论,对于产品要求细粒度的 ...

  3. 网页UI视觉设计规范

  4. FileUtil

    package com.wiseweb.util; import java.io.BufferedReader; import java.io.File; import java.io.FileInp ...

  5. 新手安装使用codeblocks

    很多第一次使用codeblocks的新手总会遇到很多的问题,首先作为一名新手应该下一个完整版的codeblocks,下载地址链接: http://pan.baidu.com/s/1dDxKeD7 密码 ...

  6. 使用expect的自动化交互

    Q:利用shell脚本实现ssh自动登录远程服务器? A:expect命令 #!/usr/bin/expect spawn ssh root@172.16.11.99 expect "*pa ...

  7. 50 years&comma; 50 colors

    50 years, 50 colors Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others) ...

  8. win10显示许可证即将过期,但在激活界面显示的仍是已激活问题解决

    win10开机显示"许可证即将过期"怎么办? 很多win10用户在开机的时候遇见了"许可证即将过期"请转到设置种激活windows的问题,但是查询自己的win1 ...

  9. AngularJS服务及注入--Provider

    Provider简介 在AngularJS中,app中的大多数对象通过injector服务初始化和连接在一起. Injector创建两种类型的对象,service对象和特别对象. Service对象由 ...

  10. 剑指Offer 14&period; 链表中倒数第k个结点 (链表)

    题目描述 输入一个链表,输出该链表中倒数第k个结点. 题目地址 https://www.nowcoder.com/practice/529d3ae5a407492994ad2a246518148a?t ...