Runloop和多线程总结

    苹果用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 不至于退出,并没有用于实际的发送消息。从而,实现了一个后台常驻线程。当有需要处理的任务到来时,直接扔到后台常驻线程去处理就可以了。

 

  AutoreleasePool的创建和释放与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可以做的一些事情:

  1. 监听主线程Runloop的状态,在主线程空闲的时候或者合适的时候去执行一些任务。比如,把一个大任务拆分成一个个小任务,当主线程处于休眠状态时(kCFRunLoopBeforeWaiting)依次提交到Runloop里执行,合理利用CPU,提高CPU使用效率。
  2. 根据主线程Runloop的状态(kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting,即正在处理任务的状态),做实时监控主线程卡顿。(可以参考:iOS实时卡顿监控)
  3. 可以自己创建一个后台线程,并且开启Runloop来等待任务或者用来专门处理某类任务。比如,AFNetworking中创建一个后台线程来专门接收Delegate回调。
  4. 根据Runloop的Mode来提交不同的任务,然后根据状态在Mode之间切换。比如TableView中将加载图片的任务放到NSDefaultRunLoopMode模式下去处理,保证UITrackingRunLoopMode模式下界面滑动的流畅性。

 

圈内Runloop经典博客:深入理解RunLoop

附上一篇不错的Runloop相关知识点总结博客:【iOS程序启动与运转】- RunLoop个人小结

Autorelease Pool 的实现原理

 

你可能感兴趣的:(iOS高级进阶)