RunLoop基础概念
通俗的来说,RunLoop就是一个带有判断条件do-while
循环,不会一直消耗CPU,是一种闲等待
,可以唤醒和休眠,保持程序的持续运行,处理App中各种事务,在状态为Stop
和Finish
时退出。
OSX/iOS 系统中,提供了两个RunLoop对象:NSRunLoop
和 CFRunLoopRef
。
- CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
- NSRunLoop 是
基于
CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
如果想深入研究的话,可以下载源码
通过源码和调试可以得知以下几个知识点
- 在获取线程的runloop时,以线程为key,runloop为value保存到一个CFDictionary中,可以说明,线程和runloop是
一一对应
的。 - 保证线程不退出,则要往线程里加runloop。保证runloop不退出,则要往里面加timer、souce0、source1。注意:
source0可以保证子runloop不退出,但不能唤醒runloop
。 - 主线程的runloop是系统创建好的,并用一个静态static变量保存,所以一直存在,保存在一个
_CFTSDTable
里。所以GetCurrentRunloop时候子线程才会去创建子Runloop。 - 保证runloop不退出的判读里,主runloop不需要判断里面是否添加了timer、source、observer。所以主runloop和子runloop是分开判断的。
- 唤醒runloop的条件有:timer、source1、手动
CFRunLoopWakeUp
、超时(会短暂唤醒立马再退出) - AFNetworking2.0创建常驻线程的原理就是往一个子runloop中添加source1,即
[NSMarchPort port]
. - mach port是用来跨线程通讯的,可以发送消息message。例如:剪切板:剪切板的内容每个App都可以访问。
- runloop休眠后,会被一个接受消息mach_msg的mach port阻塞掉,以阻止CPU消耗, runloop唤醒或者添加timer本质都是通过mach port去取消掉block的线程。
-
performSelector:withObject:afterDelay
依赖于线程的 runloop,因为它本质上是由一个定时器负责定期加入到 runloop 中执行。 - run 方法的文档还可以知道,它的本质就是无限调用
runMode:beforeDate:
方法,那么在run方法的下面的操作代码都不会被执行到。同样地,runUntilDate
: 也会重复调用runMode:beforeDate:
,区别在于它超时后就不会再调用。总结来说,runMode:beforeDate
: 表示的是 runloop 的单次调用
,即唤醒过一次就退出了,不会再次唤醒,另外两者则是循环调用。想从 runloop 里面退出来,就不能用 run 方法。根据实践结果和文档,另外两种启动方法也无法手动退出,因为CFRunLoopIsStopped的是众多循环中的一次而已。 -
NSRunLoopCommonModes
是一个伪模式。Runloop的run方法内部其实大概就是不断调用:runMode:beforeDate:
.如果这里的mode传commonMode,则会立马返回Finished,停止run,也就是无法保活线程。因为commonMode相当于是default和tracking伪合集,而不是具体的一个mode,所以不能run。runloop的addTimer:timer forMode
可以设置commonMode,因为底层代码会去查找default和tracking模式。``
//前提是当前Runloop有timers或sources时保活了runloop,执行完不会退出
[sonLoop run]; //循环运行,此方法下的代码不被执行
[sonLoop runUntilDate:[NSDate distantFuture]]; //同上
[sonLoop runUntilDate:[NSDate date]]; //sonLoop立马退出
[sonLoop runMode:NSRunLoopCommonModes beforeDate:[NSDate distantFuture]]; //立马退出,因为commonMode是个伪模式
[sonLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; //线程只能唤醒一次,之后就退出,并且执行此行代码之后的代码,因为此方法是单循环,不同于上面两个方法的重复循环
[sonLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10.0]]; //10秒后自动退出,10秒内唤醒一次
Runloop和GCD的关系
- RunLoop 的超时时间就是使用 GCD 中的
dispatch_source_t
来实现的。 - 当调用
dispatch_async(dispatch_get_main_queue(), block)
时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并执行。GCD中将任务提交到主线程的主队列即dispatch_get_main_queue()时,这里的任务是由RunLoop负责执行。只有主队列的任务会交由RunLoop对象处理
,其他队列的则由GCD自行处理。
Runloop和AutoreleasePool的关系
- 主程序的RunLoop在每次事件循环之前之前,会自动创建一个
autoreleasePool
,并且会在事件循环结束时,执行drain
操作,释放其中的对象。 - 程序启动后,苹果在主线程 RunLoop 里注册了两个 Observer:
- 第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
- 第二个 Observer 监视了两个事件: BeforeWaiting(准备进入睡眠) 和 Exit(即将退出Loop),
-
BeforeWaiting
(准备进入睡眠)时调用_objc_autoreleasePoolPop
() 和_objc_autoreleasePoolPush
() 释放旧的池并创建新池; - Exit(即将退出Loop) 时调用
_objc_autoreleasePoolPop
() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
AutoreleasePool
-
自动释放池
的本质是一个AutoreleasePoolPage
结构体对象,是一个栈结构存储的页,每一个AutoreleasePoolPage都是以双向链表的形式连接。自动释放池的多层嵌套其实就是不停的pushs哨兵对象,在pop时,会先释放里面的,在释放外面的。 - 每一个自动释放池都是由一系列的 AutoreleasePoolPage 组成的,并且每一个 AutoreleasePoolPage 的大小都是 4096 字节(16 进制 0x1000)
- 每一页page结构体里都有 56 bit 用于存储 AutoreleasePoolPage 的成员变量,但第一页会额外存储一个哨兵对象8字节,所以第一页能存储504个对象,从第二页开始能存储505个。存储的时候分为有没有池,这页池子没有满hot 和满了三种情况。
- AutoreleasePoolPush的时候会返回一个哨兵POOL_SENTINEL,在Pop的时候,会不断release直到找到这个哨兵。POOL_SENTINEL 只是 nil 的别名。
- NSAutoreleasePool 中还提到,每一个线程都会维护自己的 autoreleasepool 堆 栈。换句话说 autoreleasepool 是与线程紧密相关的,每一个 autoreleasepool 只对应 一个线程。
SDWebImage中,由于encodedDataWithImage会把image解码成data,可能造成内存暴涨,所以加autoreleasepool避免内存暴涨。
RunLoop和performselector
-
performSelecor
是延迟到运行时才会去检查方法是否存在,编译时不会检查方法是否存在,比如我们运行时添加一个方法,而在编译时是不存在的,所以就需要用perform来调用 - 会将该方法和performSelector:withObject:作对比,那么performSelector:withObject:在不添加到子线程的Runloop中时是否能执行?
我当时想的是,performSelector:withObject:方法和延迟方法类似,只不过是马上执行而已,所以也需要添加到子线程的RunLoop中。- 这么想是错的,看过源码后知道
performSelector:withObject:
只是一个单纯的消息发送,和时间没有一点关系。所以不需要添加到子线程的Runloop中也能执行。 - 而
performSelector:withObject:afterDelay:
其实就是在内部创建了一个NSTimer,然后会添加到当前线程的Runloop中。所以当该方法添加到子线程中时,需要格外的注意
的地方: 子线程Runloop此时需要手动开启,并且在子线程中两者的顺序必须是先执行performSelector延迟方法之后再执行run方法。因为run方法只是尝试想要开启当前线程中的runloop,但是如果该线程中并没有任何事件(source、timer、observer)的话,并不会成功的开启。
利用PerformSelector设置当前线程的RunLoop的运行模式
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"tupian"] afterDelay:4.0 inModes:@[NSDefaultRunLoopMode]];
Source事件源
-
Source0
表示 非系统事件,即用户自定义的事件,例如:hitTestEvent、performSelector,不基于port,CFRunLoopSourceCreate创建一个source0,不能主动唤醒
runloop。但是如果想要系统处理自定义的source0,可以先用CFRunLoopSourceSignal(source)标记source0为待处理,然后CFRunLoopWakeUp(runloop)唤醒runloop,接着系统就能处理source0了。注意
:这不代表source0可以主动唤醒runloop! -
Source1
表示系统事件,主要负责底层的通讯,具备唤醒能力,Source1 创建了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息(mach_msg),这种 Source 能主动唤醒
RunLoop 的线程,基于port。
IOKit 是硬件驱动程序的运行环境,包含电源、内存、CPU 等信息。
我们触摸屏幕,先摸到硬件(屏幕),屏幕表面的事件会被IOKit先包装成Event,通过mach_Port传给正在活跃的APP , Event先告诉source1(mach_port),source1唤醒RunLoop, 然后将事件Event分发给source0,然后由source0来处理。
RunLoopObserver
如果想要观察runloop在程序运行中各种状态如何运作的,可以添加观察者观察runloop的各种状态切换。
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
//将观察者添加到主线程runloop的common模式下的观察中
CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
回调函数
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
switch (activity) {
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop休眠");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop唤醒");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop退出");
break;
case kCFRunLoopBeforeSources:
NSLog(@"RunLoop处理事件源");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop处理定时器");
break;
default:
break;
}
}
本文只是对本人印象笔记的总结,因为已经有许多优秀的文章了,并没有对很多细节展开。若有不对的地方,还请欢迎指出纠正,谢谢。