iOS - RunLoop总结 by:nixs

详细示例代码github仓库总结:NIiOS

##### (二十) iOS RunLoop

```
Runloop
	RunLoop的应用
		- [x] 常驻线程
		NSTimer
			- [x] 1. 定时器的使用
			- [x] 2. 滑动时失效
			- [x] 3. 不准时
		- [x] AutoreleasePool
		- [x] 事件响应
		- [x] 手势识别
		- [x] 界面更新
		PerformSelecter
			// 1.和RunLoop不相干,底层直接调用objc_sendMsg方法
				- [x] (id)performSelector:(SEL)aSelector withObject:(id)object;
			// 2. 和RunLoop相关,封装成Source0事件,依赖于RunLoop,若线程无对应的RunLoop,会调用objc_sendMsg执行
				- [x] (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
			// 3. 和RunLoop相关,封装成Timers事件
				- [x] (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
		关于 GCD
			- [x] 在 RunLoop 的源代码中可以看到用到了 GCD 的相关内容,但是 RunLoop 本身和 GCD 并没有直接的关系。
	RunLoop中的Mode
		- [x] 一个RunLoop包含若干个Mode,每个Mode又包含若干个 Source/Timer/Observer。这句话真的是点晴之笔,一句话就把5个相关类的关系说的一清二楚。
		- [x] 一个CFRunLoopModeRef对象有一个name,若干source0,source1,timer,observer和port,可以看出事件都是由mode在管理,而RunLoop管理着Mode。
		__CFRunLoopMode的五种运行模式(系统默认注册的五个Mode)
			- [x] 1. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
			- [x] 2. UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
			- [x] 3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode
			- [x] 4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
			- [x] 5. kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode 
		CFRunLoopSourceRef
			- [x] Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal (source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp (runloop) 来唤醒 RunLoop,让其处理这个事件。
			- [x] Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。
		CFRunLoopTimerRef
			- [x] 是基于时间的触发器
			- [x] CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是 toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop 会注册对应的时间点,当时间点到时,RunLoop 会被唤醒以执行那个回调。
		CFRunLoopObserverRef
			- [x] CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。
	RunLoop的相关类之间关系
		RunLoop相关的类有5个
			- [x] 1.CFRunLoopRef
			- [x] 2.CFRunLoopModeRef
			- [x] 3.CFRunLoopSourceRef
			- [x] 4.CFRunLoopTimerRef
			- [x] 5.CFRunLoopObserverRef
		那么每个类都是什么呢?
			- [x] 1.第一个类我在前面已经剖析过了,它就是RunLoop对象所属于的类
			- [x] 2.CFRunLoopModeRef 是 RunLoop 当前的一个运行模式,什么是运行模式呢?我会在RunLoop和Mode这一节仔细讲解
			- [x] 3.CFRunLoopSourceRef和CFRunLoopTimerRef是RunLoop处理的消息类型
			- [x] 4.CFRunLoopObserverRef监听RunLoop运行状态的一个类
		各个类之间的关系
			- [x] 1.一个RunLoop包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer。
			- [x] 2.每次调用 RunLoop的主函数时,只能指定其中一个 Mode,这个Mode被称作CurrentMode。
			- [x] 3.如果需要切换 Mode,只能退出Loop,再重新指定一个Mode进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
			- [x] 4.如果一个 mode中一个Source/Timer/Observer 都没有,则RunLoop会直接退出,不进入循环。
		各个类的作用
			- [x] 1.CFRunLoopRef是一个CFRunLoop结构体的指针,所以说它的职责就是CFRunLoop的职责,运行循环,处理事件,保持运行
			- [x] 2.CFRunLoopModeRef运行模式,模式下对应多个处理源,具体有哪些模式我会在RunLoop和Mode这一节仔细讲解
			3.CFRunLoopSourceRef是事件产生的地方。Source有两个版本:Source0 和 Source1。
				- [x] 1.Source0触摸事件处理
				- [x] 2.Source1基于Port的线程见通信
			- [x] 4.CFRunLoopTimerRefNSTimer的运用
			- [x] 5.CFRunLoopObserverRef用于监听RunLoop的状态,UI刷新,自动释放池
	RunLoop和线程
		- [x] 1.RunLoop是基于线程来管理的,它们一一对应,共同存储在一个全局区的runLoopDict中,线程是key,RunLoop是value。
		- [x] 2.RunLoop的创建:主线程所对应RunLoop在程序一启动创建主线程的时候系统就会自动为我们创建好,而子线程所对应的RunLoop并不是在子线程创建出来的时候就创建好的,而是在我们获取该子线程所对应的RunLoop时才创建出来的,换句话说,如果你不获取一个子线程的RunLoop,那么它的RunLoop就永远不会被创建。
		- [x] 3.RunLoop的获取:我们可以通过一个指定的线程从runLoopDict中获取它所对应的RunLoop。
		- [x] 4.RunLoop的销毁:系统在创建RunLoop的时候,会注册一个回调,确保线程在销毁的同时,也销毁掉其对应的RunLoop。
	CFRunLoopRef对象源码剖析
		- [x] NSRunLoop对象是基于CFRunLoopRef的,并且CFRunLoopRef是基于c语言的,线程安全
	RunLoop对象的获取
		CoreFoundation
			CFRunLoopRef对象
				- [x] CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
				- [x] CFRunLoopGetMain();   // 获得主线程的RunLoop对象
		Fundation框架 (基于CFRunLoopRef的封装)
			NSRunLoop对象
				- [x] [NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
				- [x] [NSRunLoop mainRunLoop];   // 获得主线程的RunLoop对象
	RunLoop在何处开启?
		- [x] 在UIApplicationMain函数中,开启了一个和主线程相关的RunLoop,导致UIApplicationMain不会返回,一直在运行中,也就保证了程序的持续运行
	RunLoop的作用
		1.保持程序持续运行
			- [x] 程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的RunLoop,RunLoop保证主线程不会被销毁,也就保证了程序的持续运行
		2.处理App中的各种事件
			- [x] 1.定时器(Timer)、方法调用(PerformSelector)
			- [x] 2.GCD Async Main Queue
			- [x] 3.事件响应、手势识别、界面刷新
			- [x] 4.网络请求
			- [x] 5.自动释放池 AutoreleasePool
		3.节省CPU资源,提高程序性能
			- [x] 程序运行起来时,当什么操作都没有做的时候,RunLoop就告诉CUP,现在没有事情做,我要去休息,这时CUP就会将其资源释放出来去做其他的事情,当有事情做的时候RunLoop就会立马起来去做事情
	什么是RunLoop?
		- [x] 之所以,iOS App 能持续响应,保证程序运行状态,在于其有一个事件循环——Event Loop
		- [x] 事件循环机制,即线程能随时响应并处理事件的机制。这种机制要求线程不能退出,而且需要高效的完成事件调度与处理。
		- [x] 事件循环在很多编程语言,或者说不同的操作系统层面都支持。比如 JS中的事件循环、Windows下的消息循环,在 iOS/macOS 下,该机制就称为 RunLoop。
		举一个生活中的????
			- [x] 进程是一家工厂,线程是一个流水线,RunLoop就是流水线上的主管;当工厂接到商家的订单分配给这个流水线时,RunLoop就启动这个流水线,让流水线动起来,生产产品;当产品生产完毕时,RunLoop就会暂时停下流水线,节约资源。
			- [x] RunLoop管理流水线,流水线才不会因为无所事事被工厂销毁;而不需要流水线时,就会辞退RunLoop这个主管,即退出线程,把所有资源释放。
```

##### (二十一) iOS RunLoop(二)

```
RunLoop-2-iOS 多线程:RunLoop详细总结
	4. RunLoop 实战应用
		- [x] 4.1 NSTimer 的使用(详细见2.3CFRunLoopTimerRef)
		- [x] 4.2ImageView 推迟显示
		4.3 后台常驻线程(很常用)
			- [x] 我们再开发应用程序的过程中,如果后台操作特别频繁,经常会在子线程做一些耗时操作(下载文件,后台播放音乐等),我们最好能让这条线程永远常驻内存。
			做法如下:
				- [x] 添加一条用于常驻内存的强引用的子线程,在该线程的 RunLoop 下添加一个 Source,开启 RunLoop。
			具体操作如下:
				- [x] 1.添加一条强引用的 thread 线程属性。
				- [x] 2.创建线程平启动方法
	3. RunLoop 原理
		在每次运行开启 RunLoop 的时候,所在线程的 RunLoop 会自动处理之前未处理的事件,并且通知相关的观察者。
		RunLoop 的运行逻辑
			- [x] 1.通知观察者 RunLoop 已经启动
			- [x] 2.通知观察者即将要开始的定时器
			- [x] 3.通知观察者任何即将启动的非基于端口的源
			- [x] 4.启动任何准备好的非基于端口的源
			- [x] 5.如果基于端口的源准备好并处于等待状态,立即启动,并进入步骤9
			- [x] 6.通知观察者线程进入休眠状态
			7.将线程置于休眠,知道任一下面的事件发生:
				- [x] 某一事件到达基于端口的源
				- [x] 定时器启动
				- [x] RunLoop 设置的时间已经超时
				- [x] RunLoop 被显示唤醒
			- [x] 8.通知观察者线程将被唤醒
			9.处理未处理的事件
				- [x] 如果用户定义的定时器启动,处理定时器事件并重启 RunLoop,进入步骤2
				- [x] 如果输入源启动,传递相应的消息
				- [x] 如果 RunLoop 被显示唤醒而且时间还没超时,重启 RunLoop,进入步骤2
			- [x] 10.通知观察者 RunLoop 结束
	2. RunLoop 相关类
		- [x] 1.CFRunLoopRef:代表 RunLoop 的对象
		- [x] 2.CFRunLoopModeRef:RunLoop 的运行模式
		3.CFRunLoopSourceRef:就是上图提到的输入源、事件源
			- [x] input sources(输入源)
			- [x] Timer sources(定时源)
		- [x] 4.CFRunLoopTimerRef:就是上图提到的定时源
		- [x] 5.CFRunLoopObserverRef:观察者,可以监听 RunLoop 的状态改变
		2.1 CFRunLoopRef
			Core Foundation
				- [x] CFRunLoopGetCurrent() 获得当前线程的 RunLoop 对象
				- [x] CFRunLoopGetMain() 获得主线程的 RunLoop 对象
			Foundation
				- [x] [NSRunLoop currentRunLoop] 获得当前线程的RunLoop对象
				- [x] [NSRunLoop mainRunLoop] 获得主线程的RunLoop对象
		2.2 CFRunLoopModeRef
			- [x] kCFRunLoopDefaultMode App 的默认运行模式,通常主线程是在这个运行模式下运行的
			- [x] UITrackingRunLoopMode 跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响)
			- [x] UIInitializationRunLoopMode 在刚启动 App 时首次进入的第一个 Mode,启动完成后就不再使用
			- [x] GSEventReceiveRunLoopMode 接受系统内部事件,通常用不到
			- [x] kCFRunLoopCommonModes 伪模式,不是一种真正的运行模式
			- [x] 其中kCFRunLoopDefaultMode,UITrackingRunLoopMode,kCFRunLoopCommonModes是我们开发中需要用到的模式。
		2.3 CFRunLoopTimerRef
			- [x] CFRunLoopTimerRef是定时源,理解即为基于时间的触发器,可以将其理解为定时器。
			然道我们就不能在这两种模式都让NSTimer 都能正常工作吗?
				- [x] 那么哪些模式被标记上了 Common Modes 呢?
				- [x] 当然可以,这就用到了我们之前说的伪模式 (kCFRunLoopCommonModes),这其实不是一种真实的模式,而是一种标记模式,意思就是可以在打上Common Modes 标记的模式下运行。
				- [x] NSDefaultRunLoopMode 和 UITrackingRunLoopMode
		2.4 CFRunLoopSourceRef
			CFRunLoopSourceRef是事件源,(上图中有提到过),CFRunLoopSourceRef有两种分类方法。
				第一种按照官方文档来分类(就像 RunLoop 模型图中的那样)
					- [x] Port-Based Sources(基于端口)
					- [x] Custom Input Source(自定义)
					- [x] Cocoa Perform Selector Sources
				第二种按照函数调用栈来分类:
					- [x] Source0:非基于 Port
					- [x] Source1:基于 Port,通过内核和其他线程通信,接收,分发系统事件
				总结:这两种分类方式其实没有什么区别,只不过第一种是通过官方理论来分类,第二种是在实际应用中通过调用函数来分类。
		2.5 CFRunLoopObserverRef
			- [x] CFRunLoopObserverRef是观察者,用来监听 RunLoop 的状态改变
	1. RunLoop 简介
		1.1 什么是 RunLoop?
			- [x] 根据字面意思:Run 表示运行,Loop 表示循环,结合在一起就是运行的循环。
			- [x] RunLoop 实际上是一个对象,这个对象在循环中用来处理程序运行过程中出现的各种事件(比如触摸事件,UI 刷新事件,定时器事件,selector 事件),从而保持程序的持续运行;而且在没有事件处理的时候,会进入睡眠模式,从而节省 CPU 资源,提高程序性能。
		1.2 RunLoop 和线程
			- [x] RunLoop 和线程息息相关,我们知道线程的作用是用来执行特定的一个或多个任务,但是在默认情况下,线程执行完之后就会退出,就不能再执行任务了。这时我们就需要采用一种方式来让线程能够处理任务,并不退出。所以就有了 RunLoop。
			- [x] 1.一条线程对应一个 RunLoop 对象,每条线程都有唯一一个与之对应的 RunLoop 对象。
			- [x] 2.我们只能在当前线程中操作当前线程的 RunLoop,而不能去操作其他线程的 RunLoop。
			- [x] 3.RunLoop 对象在第一次获取 RunLoop 时创建,销毁则是在线程结束的时候。
			- [x] 4.主线程的 RunLoop 对象,系统自动帮我们创建好了(原理如下)。而子线程的 RunLoop 对象需要我们主动创建.
		1.3 默认情况下主线程的 RunLoop 原理
			- [x] 其中UIApplicationMain函数内部帮我们开启了主线程RunLoop,UIApplicationMain内部拥有一个无线循环的代码
			- [x] RunLoop 就是线程中的一个循环,RunLoop 在循环中会不断检测,通过input sources(输入源)和Timer sources(定时源)两种来源等待接受事件;然后对接受到的事件通知线程进行处理,并在没有事件的时候进行休息。
```

##### (二十二) iOS RunLoop(三)

```
RunLoop-3-iOS - 深入理解 RunLoop
	3. RunLoop的实际应用举例
		- [x] 3.1 AFNetworking
		- [x] 3.1 AsyncDisplayKit
	2. 苹果用RunLoop实现的功能
		2.1 AutoreleasePool
			- [x] App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
			- [x] 第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
			- [x] 第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
		2.2 事件响应
			- [x] 苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。
		1.3 手势识别
			- [x] 苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是_UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。
			- [x] 当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
		1.4 界面更新
			- [x] 苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
		1.5 定时器
			- [x] NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。
		1.6 PerformSelector
			- [x] 当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
			- [x] 当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
		1.7 关于GCD
			- [x] 实际上 RunLoop 底层也会用到 GCD 的东西,NSTimer 是用了 XNU 内核的 mk_timer,但同时 GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。
		1.8 关于网络请求
			iOS 中,关于网络请求的接口自下至上有如下几层:
				- [x] CFSocket 是最底层的接口,只负责socket通信。
				- [x] CFNetwork 是基于CFSocket等接口的上层封装,ASIHttpRequest工作与这一层。
				- [x] NSURLConnection 是基于CFNetwork的更高层的封装,提供面向对象的接口,AFNetworking工作于这一层。
				- [x] NSURLSession 是iOS7中新增的接口,表面上是和 NSURLConnection 并列的,但底层仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 线程),AFNetworking2 和 Alamofire 工作于这一层。
				主要介绍下 NSURLConnection 的工作过程
					- [x] 通常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0 (即需要手动触发的Source)。CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 的。
					- [x] 当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。
					- [x] NSURLConnectionLoader 中的 RunLoop 通过一些基于 mach port 的 Source 接收来自底层 CFSocket 的通知。当收到通知后,其会在合适的时机向 CFMultiplexerSource 等 Source0 发送通知,同时唤醒 Delegate 线程的 RunLoop 来让其处理这些通知。CFMultiplexerSource 会在 Delegate 线程的 RunLoop 对 Delegate 执行实际的回调。
	1.RunLoop 的概念
		- [x] RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面的Event Loop 的逻辑。线程执行了这个函数之后,就会一直处于这个函数内部"接受消息->等待->处理"的循环中,直到这个循环结束(比如传入 quit的消息),函数返回。
		1.1 RunLoop 与线程的关系
			- [x] 线程和RunLoop 之间是一一对应的,其关系保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那么它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁时发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)
		- [x] 1.2 RunLoop 对外的接口
		- [x] 1.3 RunLoop 的 Mode
		1.4 RunLoop的内部逻辑
			- [x] 实际上RunLoop就是这样一个函数,其内部是一个do-while循环。当你调用CFRunLoopRun()时,线程会一直停留在这个循环里,直到超时或被手动停止,该函数才会返回。
		1.5 RunLoop的底层实现
			OSX/iOS的系统架构
				- [x] 应用层:包括用户能接触到的图形应用,例如Spotlight,Aqua,SpringBoard等
				- [x] 应用框架层:开发人员接触到的Cocoa等框架
				- [x] 核心框架层:包括各种核心框架,OpenGL等内容
				Darwin:即操作系统的核心,包括系统内核,驱动,Shell等内容,这一层是开源的。其所有源码都可以在opensource.apple.com中找到
					- [x] 为了实现消息的发送和接收,mach_msg() 函数实际上是调用了一个 Mach 陷阱 (trap),即函数mach_msg_trap(),陷阱这个概念在 Mach 中等同于系统调用。当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到内核态;内核态中内核实现的 mach_msg() 函数会完成实际的工作
					- [x] RunLoop 的核心就是一个 mach_msg() (见上面代码的第7步),RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap() 这个地方。
```

 

上一篇:你了解RunLoop线程保活吗?已封装好,2句代码直接使用


下一篇:精选面试题教你应对高级iOS开发面试官(提供底层进阶规划蓝图)