你好2019!一起努力呀!

1、什么是runloop

runloop是通过内部维护的事件循环对事件/消息进行管理的一个对象。

事件循环(Event loop):通俗的解释:没有消息处理的时候,休眠以避免资源占用;有消息需要处理时,立即被唤醒!书面的解释:没有需要处理的消息时,用户态切换为内核态;有消息需要处理时,内核态切换为用户态!

用户态:一般时开发者开发使用的、常见的api

内核态:系统调用底层的相关,例如:开关机、来电等!

用户态与内核态的理解!

需要注意的是:接收消息->处理消息->等待 ,此处等待≠死循环!在之后runloop的是用场景以及分析中会解释这个!

可参考下图理解runloop的处理流程

2、runloop的组成

NSRunLoop是基于Foundation框架对CFRunLoop(基于CoreFoundation)的封装,因为NSRunloop是没有开源的,但是CFRunLoop是开源的,其结构基本一致,所以分析CFRunLoop的组成即可。

主要涉及到CFRunLoop、CFRunLoopMode、Source/Timer/oberser

CFRunLoop的构成:

pthread(这个可以知道runloop和线程一一对应)、currentMode、modes<mode型的集合,说明一个runloop可以用多个mode,如下图>、commondModes<字符串型的集合>、commonModeItems<多个Observer、多个timer、多个source的集合>

runloop与mode 一对多

关于mode:

一般我们常用的Mode有三种
1.kCFRunLoopDefaultMode(CFRunLoop)/NSDefaultRunLoopMode(NSRunLoop)
默认模式,在RunLoop没有指定Mode的时候,默认就跑在DefaultMode下。一般情况下App都是运行在这个mode下的
 
2.(CFStringRef)UITrackingRunLoopMode(CFRunLoop)/UITrackingRunLoopMode(NSRunLoop)
一般作用于ScrollView滚动的时候的模式,保证滑动的时候不受其他事件影响。
 
3.kCFRunLoopCommonModes(CFRunLoop)/NSRunLoopCommonModes(NSRunLoop)
这个并不是某种具体的Mode,而是一种模式组合,在主线程中默认包含了NSDefaultRunLoopMode和 UITrackingRunLoopMode。子线程中只包含NSDefaultRunLoopMode。
注意:
①在选择RunLoop的runMode时不可以填这种模式否则会导致RunLoop运行不成功。
②在添加事件源的时候填写这个模式就相当于向组合中所有包含的Mode中注册了这个事件源。
③你也可以通过调用CFRunLoopAddCommonMode()方法将自定义Mode放到 kCFRunLoopCommonModes组合。
 

mode是由source/timer/observer组成

Source就是输入源事件,分为:source0,诸如UIEvent(触摸,滑动等),performSelector这种需要手动触发的操作;source1,处理系统内核的mach_msg事件(系统内部的端口事件)。诸如唤醒RunLoop或者让RunLoop进入休眠节省资源等。我们需要对常驻线程进行操作的事件大多都是source0。一般来说日常开发中我们需要关注的是source0,source1只需要了解。

Timer即为定时源事件,常见到的就是NSTimer,NSTimer定时器的触发正是基于RunLoop运行的,所以使用NSTimer之前必须注册到RunLoop,但是RunLoop为了节省资源并不会在非常准确的时间点调用定时器,如果一个任务执行时间较长,那么当错过一个时间点后只能等到下一个时间点执行,并不会延后执行(NSTimer提供了一个tolerance属性用于设置宽容度,如果确实想要使用NSTimer并且希望尽可能的准确,则可以设置此属性)。

Oberver相当于消息循环中的一个监听器,随时通知外部当前RunLoop的运行状态。NSRunLoop没有相关方法,只能通过CFRunLoop相关方法创建

 // 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, , ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { NSLog(@"----监听到RunLoop状态发生改变---%zd", activity); }); // 添加观察者:监听RunLoop的状态
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

runloop是如何创建的呢?

苹果是不允许直接创建runloop,它提供了四个函数获取runloop

[NSRunLoop currentRunLoop];//获取当前线程的RunLoop
[NSRunLoop mainRunLoop];
CFRunLoopGetMain();
CFRunLoopGetCurrent();
这些函数内部的逻辑大概是下面这样(这是简化之后的逻辑,涉及到的链接在文末提供):
/// 全局的Dictionary,key 是 线程, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock; /// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock); if (!loopsDic) {
// 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
} /// 直接从 Dictionary 里获取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread)); if (!loop) {
/// 取不到时,创建一个
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
} OSSpinLockUnLock(&loopsLock);
return loop;
} CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
} CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}

线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。

除了主线程,其他线程的runloop默认是没有开启的,且从上面的源代码来看,任意一个子线程的RunLoop都会保证主线程的RunLoop的存在。

RunLoop正常运行的条件是:1.有Mode。2.Mode有事件源。3.运行在有事件源的Mode下。

源码分析之小结论:

①.RunLoop是寄生于线程的消息循环机制,它能保证线程存活,而不是线性执行完任务就消亡。

②.RunLoop与线程是一一对应的,每个线程只有唯一与之对应的一个RunLoop。我们不能创建RunLoop,只能在当前线程当中获取线程对应的RunLoop(主线程RunLoop除外)。

③.子线程默认没有RunLoop,需要我们去主动开启,但是主线程是自动开启了RunLoop的。

④.RunLoop想要正常启用需要运行在添加了事件源的Mode下。

⑤.RunLoop有三种启动方式run、runUntilDate:(NSDate *)limitDate、runMode:(NSString *)mode beforeDate:(NSDate *)limitDate。第一种无条件永远运行RunLoop并且无法停止,线程永远存在。第二种会在时间到后退出RunLoop,同样无法主动停止RunLoop。前两种都是在NSDefaultRunLoopMode模式下运行。第三种可以选定运行模式,并且在时间到后或者触发了非Timer的事件后退出。

3、runloop的是用场景

3.1:保持线程存活

自定义一个继承自NSThread的HFSThread:目的是重写其dealloc方法,看何时被释放掉的

3.1.1:Thread关联的方法中不开启runloop的情况下:

1 - (void)threadTest
2 {
3 HFSThread *testHead = [[HFSThread alloc]initWithTarget:self selector:@selector(threadAction) object:nil];
4 testHead.name = @"testThread";
5 // self.myThread = testHead;
6 [testHead start];
9 }
2019-01-13 15:09:38.664810+0800 HaiFeiTestProject[26482:874633] begin threadAction -[NSThread currentThread] = <HFSThread: 0x600002da9a40>{number = 3, name = testThread}
2019-01-13 15:09:38.667483+0800 HaiFeiTestProject[26482:874633] 121212
2019-01-13 15:09:38.667780+0800 HaiFeiTestProject[26482:874633] end threadAction -[NSThread currentThread] = <HFSThread: 0x600002da9a40>{number = 3, name = testThread}
2019-01-13 15:09:38.668259+0800 HaiFeiTestProject[26482:874633] HFSThread dealloc name = testThread

可以发现,此thread关联的方法执行完毕之后被释放掉!

可能我们会觉得此处创建的thread是临时变量,那将其设置为控制器的属性,再次执行一次相关操作

Thread设置为属性
运行结果

此时的运行结果中没有执行其dealloc操作,我们会认为此thread没有被释放

那么如果再次执行其start方法会如何?

thread连续执行两次start的运行结果

程序崩溃,原因如下:[HFSThread start]: attempt to start the thread again  也就是说虽然thread没有被释放,但是它处于死亡状态(线程执行结束之后就会进入这个状态),苹果不允许,已死亡的线程再次开启!

以上的操作会发现线程执行完其关联的任务之后就会死亡,如何保持其存活呢?

也许我们可以使用while循环保持线程存活,但是实践证明如果我们在线程关联的方法中执行如下操作

while循环保持线程存活

确实会让线程保持存活,但是此方法将会一直如此执行下去,显然不符合我们的期望!

3.1.2:初步尝试使用runloop

我们再次创建一个临时变量thread 在其关联的方法中如下操作

临时变量Thread中添加runloop
运行结果

运行结果可以看出,虽然是临时的thread,但是没有在其关联的方法结束之后没有执行dealloc操作,并且根据运行结果可以发现[runloop run]之后的代码没有在执行!

原因:RunLoop本质就是个Event Loop的do while循环,所以运行到这一行以后子线程就一直在进行接受消息->等待->处理的循环。所以不会运行[runLoop run];之后的代码(这点需要注意,在使用RunLoop的时候如果要进行一些数据处理之类的要放在这个函数之前否则写的代码不会被执行),也就不会因为任务结束导致线程死亡进而销毁。

关于runloop的使用方法在讲述其组成的时候会进一步讲述!

3.2:线程在我们需要的时候响应消息

我们实现可以保持线程存活之后,希望实现在我们需要的时候响应消息

我们知道系统提供了几个某个线程中执行任务的放

performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
在主线程中响应指定Selector。这两个方法给你提供了选项来阻断当前线程(不是执行Selector的线程而是调用上述方法的线程)直到selector被执行完毕。 performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
在某个子线程(NSThread对像)中响应指定Selector。这两个方法同样给你提供了选项来阻断当前线程直到Selector被执行完毕。 performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
在当前线程中执行Selector,并附加了延迟选项。多个排队的Selector会按照顺序一个一个的执行。

这几个方法都是向线程中的RunLoop发送了消息,然后RunLoop接收到了消息就唤醒线程,去做对应的事情。所以想要正常使用这几个方法,响应selector的线程必须开启了RunLoop。

例如:

运行结果

最后子线程任务结束然后被释放是因为之前提到的,runMode:(NSString *)mode beforeDate:(NSDate *)limitDate这种启动RunLoop的方式有一个特性,那就是这个接口在非Timer事件触发(此处是达成了这个条件)、显式的用CFRunLoopStop停止RunLoop或者到达limitDate后会退出。而例子当中也没有用while把RunLoop包围起来,所以RunLoop退出后子线程完成了任务最后退出了!如果使用的是 [runloop run];那相关的触发操作可以一直执行!

3.3:线程定时执行某个任务

我们最初使用NSTimer的时候 创建之后却并没有按照我们预期的那样执行,之后添加了[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];才可以按照预期执行,当时我们大约并没有仔细分析其中缘由,

因为timer还提供了一些方法(以scheduledTimerWithTimeInterval开头的方法),可以不必设置runloop,因为这些方法创建一个定时器并自动添加到当前线程RunLoop的NSDefaultRunLoopMode中所以不需要开发者为处理相关!

NSTimer常见的问题:

1、时好时不好

用Timer的时间长了总有一天突然发现,为啥我的Timer运行的好好的突然就时好时坏了?在进行Scrollview的滚动操作时Timer不进行响应,滑动结束后timer又恢复正常了。大约很多人都曾经遇到过吧!
我们知道每次runloop只能运行在一个mode上,当我们创建一个NSTimer的时候,默认都是讲定时器添加到了加到了主线程RunLoop的NSDefaultRunLoopMode中。一般情况下主线程RunLoop就运行在NSDefaultRunLoopMode下,所以定时器正常运行。

但是当Scrollview开始滑动时,主线程RunLoop自动切换了当前运行的Mode(currentMode),变成了UITrackingRunLoopMode。所以现在RunLoop要处理的就是UITrackingRunLoopMode中item。

而我们的timer是添加在NSDefaultRunLoopMode中的,并没有添加到UITrackingRunLoopMode中。即我们的timer不是UITrackingRunLoopMode中的item。因为不同mode的item相关没有影响,所以RunLoop也就不会处理非当前Mode的item,所以定时器就不会响应。

当Scrollview滑动结束,主线程RunLoop自动切换了当前运行的Mode(currentMode),变成了NSDefaultRunLoopMode。我们的Timer是NSDefaultRunLoopMode的item,所以RunLoop会处理它,所以又正常响应了。

想Timer在两种Mode中都得到响应怎么办?前面提到过,一个item可以被同时加入多个mode。让Timer同时成为两种Mode的item就可以了(分别添加或者直接加到commonMode中),这样不管RunLoop处于什么Mode,timer都是当前Mode的item,都会得到处理。

我们还可以使用commonMode讲timer同步添加到多个mode中

commonMode它不是实际的一种mode,是同步source、timer、observer到多个mode的一种技术方案!

2、导致的ViewController无法释放问题

创建NSTimer会因为设置target为self导致Timer对ViewController有一个强引用,最后结果就是ViewController无法释放。

关于这个问题,目前个人有两个处理方法:创建一个中间对象,处理timer和控制器 ;使用GCDTimer(关于这个的使用,在之后会进行详细说明!)

针对第一种方法的实现代码说明:

 #import <Foundation/Foundation.h>
#import <UIKit/UIKit.h> NS_ASSUME_NONNULL_BEGIN @interface ManagerTimer : NSObject @property (nonatomic, weak) NSTimer *myTimer; @property (nonatomic, weak) UIViewController *myVC;
- (void)startTimer;
@end NS_ASSUME_NONNULL_END

ManagerTimer.h

 #import "ManagerTimer.h"

 @implementation ManagerTimer

 - (id)init{
if (self = [super init]) { } return self;
}
- (void)startTimer
{
self.myTimer = [NSTimer scheduledTimerWithTimeInterval: target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
} - (void)dealloc
{
NSLog(@"ManagerTimer dealloc ");
NSLog(@"self.myTimer = %@",self.myTimer); }
- (void)timerAction:(NSTimer *)timer
{
static NSInteger num = ;
NSLog(@"ManagerTimer num = %ld",num++);
if (self.myVC == nil) {
[timer invalidate]; timer = nil; } } @end

ManagerTimer.m

在控制器中使用

 ManagerTimer *managerTimer = [[ManagerTimer alloc]init];
managerTimer.myVC = self;
[managerTimer startTimer];
 -- ::00.438225+ HaiFeiTestProject[:] ManagerTimer num =
-- ::01.439114+ HaiFeiTestProject[:] ManagerTimer num =
-- ::02.439077+ HaiFeiTestProject[:] ManagerTimer num =
-- ::03.438536+ HaiFeiTestProject[:] ManagerTimer num =
-- ::04.438006+ HaiFeiTestProject[:] ManagerTimer num =
-- ::04.601969+ HaiFeiTestProject[:] FirstViewController dealloc
-- ::04.602102+ HaiFeiTestProject[:] (null)
-- ::05.439199+ HaiFeiTestProject[:] ManagerTimer num =
-- ::05.439585+ HaiFeiTestProject[:] ManagerTimer dealloc
-- ::05.440027+ HaiFeiTestProject[:] self.myTimer = <__NSCFTimer: 0x600003b78300>

运行结果

按照我们的预期实现了!

如果不是用这个中间对象,在我们离开当前控制器的时候,定时器无法停止,控制器也无法释放!

文中若有不对之处,还请劳驾之处,谢谢!

部分参考链接:RunLoop入门小结 这个链接中相关的其他关于runloop的一些博客分析也很值得去看!

RunLoop 之初探的更多相关文章

  1. 初探Runloop(一)

    iOS 的最大特点就是运行时. 保证运行时的就是RunLoop 1.什么是RunLoop呢? 从字面理解就是:运行循环 引用下官方文档的介绍: A run loop is an event proce ...

  2. RunLoop 总结:RunLoop的应用场景(一)

    参考资料 好的书籍都是值得反复看的,那好的文章,好的资料也值得我们反复看.我们在不同的阶段来相同的文章或资料或书籍都能有不同的收获,那它就是好文章,好书籍,好资料.关于iOS 中的RunLoop资料非 ...

  3. 初探领域驱动设计(2)Repository在DDD中的应用

    概述 上一篇我们算是粗略的介绍了一下DDD,我们提到了实体.值类型和领域服务,也稍微讲到了DDD中的分层结构.但这只能算是一个很简单的介绍,并且我们在上篇的末尾还留下了一些问题,其中大家讨论比较多的, ...

  4. CSharpGL(8)使用3D纹理渲染体数据 (Volume Rendering) 初探

    CSharpGL(8)使用3D纹理渲染体数据 (Volume Rendering) 初探 2016-08-13 由于CSharpGL一直在更新,现在这个教程已经不适用最新的代码了.CSharpGL源码 ...

  5. 从273二手车的M站点初探js模块化编程

    前言 这几天在看273M站点时被他们的页面交互方式所吸引,他们的首页是采用三次加载+分页的方式.也就说分为大分页和小分页两种交互.大分页就是通过分页按钮来操作,小分页是通过下拉(向下滑动)时异步加载数 ...

  6. JavaScript学习(一) —— 环境搭建与JavaScript初探

    1.开发环境搭建 本系列教程的开发工具,我们采用HBuilder. 可以去网上下载最新的版本,然后解压一下就能直接用了.学习JavaScript,环境搭建是非常简单的,或者说,只要你有一个浏览器,一个 ...

  7. 我的runloop学习笔记

    前言:公司项目终于忙的差不多了,最近比较闲,想起叶大说过的iOS面试三把刀,GCD.runtime.runloop,runtime之前已经总结过了,GCD在另一篇博客里也做了一些小总结,今天准备把ru ...

  8. .NET文件并发与RabbitMQ(初探RabbitMQ)

    本文版权归博客园和作者吴双本人共同所有.欢迎转载,转载和爬虫请注明原文地址:http://www.cnblogs.com/tdws/p/5860668.html 想必MQ这两个字母对于各位前辈们和老司 ...

  9. React Native初探

    前言 很久之前就想研究React Native了,但是一直没有落地的机会,我一直认为一个技术要有落地的场景才有研究的意义,刚好最近迎来了新的APP,在可控的范围内,我们可以在上面做任何想做的事情. P ...

随机推荐

  1. PHP中使用Jpgraph生成统计图

    Jpgraph是PHP图表类库,可以生成折线图.柱状图.大饼图等等统计图.如果你想使用PHP生成统计图来统计数据,使用它再方便不过啦. 如果说你要亲自使用GD库来写的话,那我只能膜拜大神啦(我不会哈哈 ...

  2. ArcGISPlotSilverlightAPI For WPF

    这两天有个需求,在地图上做标绘箭头,效果如下图. Arcgis for WPF 10.2.5.0版本,然而官方文档中没有这种API,自己去写一个呢,又感觉无从下手.无奈去网上搜索了一下,发现一篇好文: ...

  3. Unity3D-NGUI动态加载图片

    NGUI提供了很方便的UIAtlas,其主要作用是改进DrawCall,把众多图片整合在一张贴图上,由于UNITY3D简单易用的好处,所以只是用原生的GUI很容易忽视DrawCall的问题,所以NGU ...

  4. VISO画UML用例图添加Include关系的方法

    VISO画UML用例图添加Include关系的方法 今天用Microsoft Visio 2007画用例图时,发现visio UML用例里面找不到include关系,查到一个可行的解决办法:  1)创 ...

  5. ORM------多表操作

    上面介绍了单表操作 下面就好比我们的sql语句这只能满足于我们的一些简单的操作不能适应我们更多的需要 所以我们需要用到更多的需求来进行我们的关系的建立以及查找 其实ORM语句就对应着我们的sql语句  ...

  6. win8 便签工具

    启动或显示 Sticky Notes : Win+R--->StikyNot.exe 备份Sticky Notes 保存位置 : %AppData%\Microsoft\Sticky Notes ...

  7. 按钮在执行frame动画的时候怎么响应触发事件?

    按钮在执行frame动画的时候怎么响应触发事件? 代码中效果(请注意,我并没有点击到按钮,而是点击到按钮的终点frame值处): 对应的代码: // // ViewController.m // Ta ...

  8. 浅析Linux操作系统是如何工作的(思维导图)

    SA***189 多任务计算机运转机制如下思维导图所示: 小结: Linux操作系统是一个在时钟的节拍下,各个模块紧密协作.密不可分的整体,而整个Linux系统都是建立在存储程序的基础之上,正是有了程 ...

  9. Python初学者第十五天 文件处理3

    ---恢复内容开始--- 15day 1.智能检测文件编码: 1.1 导入第三方工具箱:chardet import chardet f = open('log',mode='rb') data = ...

  10. [COGS 2066]七十和十七

    2066. 七十和十七 ★★★   输入文件:xvii.in   输出文件:xvii.out   简单对比时间限制:1 s   内存限制:256 MB [题目描述] 七十君最近爱上了排序算法,于是Ta ...