苹果用Runloop实现的功能(部分):
1、AutoreleasePool
2、监听和响应事件,如事件响应、手势识别、网络事件
3、UI更新
4、定时器
5、PerformSelecter方法
另外说一下主线程runloop的作用:1.保证程序不退出。2.监听事件(如触摸事件、时钟事件、网络事件)。
runloop当第一次调用的时候才会创建,即懒加载模式。比如在子线程中第一次调用currentRunloop的时候才会创建。主线程的runloop会自动创建并默认开启,这样才能保证主线程一直存在,不会被销毁。ps:一个线程中如果没有任务需要处理了,这个时候线程就会退出。所以,如果想让线程一直存活,可以实现类似runloop中do-while循环,这样线程会一直存活。
runloop的模式一共有5种,其中2种一般用不到(一个是内核相关的,一个是程序初始化相关的)。需要了解的也就下面这3种:
1.NSDefaultRunLoopMode //默认的
2.UITrackingRunLoopMode //UI模式(苹果为了用户体验,所以这个优先级较高),比如当滑动UIScrollView的时候,runloop会切到这个模式下,当滑动事件结束时,runloop会切到NSDefaultRunLoopMode
3.NSRunLoopCommonModes 占位模式,相当于 NSDefaultRunLoopMode + UITrackingRunLoopMode
比如NSTime在有UIScrollView滑动的页面不准的问题(加入NSDefaultRunLoopMode中),是因为当UIScrollView滑动时runloop切到了UITrackingRunLoopMode模式,而我们没有将NSTime加入到UITrackingRunLoopMode模式下,所以不会调用NSTime。
解决办法:将NSTime加到NSRunLoopCommonModes模式下,相当于在NSDefaultRunLoopMode和UITrackingRunLoopMode中都添加了,这样就解决定时器不准的问题了。另外,利用GCD也可以创建一个计时器(dispatch_source_t),而且不用考虑准不准的问题,因为苹果已经给我们封装好了。
NSDefaultRunLoopMode和UITrackingRunLoopMode,各自中又包含3种runloop底层mode分别为:Source(事件源、输入源)、Observer、Timer。其中Source又分为Source0和Source1。
Source0主要是基于非Mach Port的事件,如用户主动触发的触摸事件、PerformSelectors等。
Source1主要监听的是基于Mach Port的的事件,如和其他线程相互发送消息(与内核相关)的线程间通讯事件等。
UIKit框架不是线程安全的。为了保证在多线程中不会出现线程安全问题,所以,系统将UI操作统一放到主线程中执行。
其实,主线程正常退出后,比如在主线程调用了[NSThread exit],程序并不会崩溃,其他线程可以正常的跑,只是主线程退出了,在主线程上的操作无法进行了而已,比如UI的显示和操作。
在使用PerformSelecter方法时需要注意:
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
如果我们想做一个后台常驻线程,那么必须要保证线程一直有任务在处理或者Runloop一直在跑,否则,线程处理完任务之后会立马退出销毁。下面可以实现后台常驻线程(AFNetworking中):
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。从而,实现了一个后台常驻线程。当有需要处理的任务到来时,直接扔到后台常驻线程去处理就可以了。
App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。但是,自己创建的子线程默认是没有AutoreleasePool的,需要自己添加AutoreleasePool。
AutoReleasePool drain 和 release区别:
当我们向自动释放池pool发送 release 消息时,它会向池中的每一个发送了 autorelease 消息的对象发送一条 release 消息,并且自身也会销毁。当向它发送 drain 消息时,只会释放里面的对象,而不会销毁自己。
什么情况下会将对象放到自动释放池:
1、对象作为方法返回值时候
2、通过类方法创建对象的时候
3、使用如下的便捷语法来建立对象的时候:
NSArray *array = @[@"abc",@"def"];
NSNumber *number = @123;
注意:每个线程中都可以有多个AutoreleasePool,但每个AutoreleasePool只能在一个线程。AutoreleasePool可以嵌套,嵌套AutoreleasePool中的变量会被加入到最近的那个AutoreleasePool中。
圈内Runloop经典博客:深入理解RunLoop
附上一篇不错的Runloop相关知识点总结博客:【iOS程序启动与运转】- RunLoop个人小结
Autorelease Pool 的实现原理