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 timer

),而且不用考虑准不准的问题,因为苹果已经给我们封装好了。

   NSDefaultRunLoopMode和UITrackingRunLoopMode,各自中又包含3种runloop底层mode分别为:Source(事件源、输入源)、Observer、Timer。其中Source又分为Source0和Source1。Source0主要监听的是如触摸事件、网络事件。Source1主要监听的是系统内核事件、线程间通讯事件、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。

 利用runloop可以做的一些事情:

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

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

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

发布了89 篇原创文章 · 获赞 92 · 访问量 30万+

猜你喜欢

转载自blog.csdn.net/u013602835/article/details/80643930