RunLoop总结

RunLoop基础概念

通俗的来说,RunLoop就是一个带有判断条件do-while循环,不会一直消耗CPU,是一种闲等待,可以唤醒和休眠,保持程序的持续运行,处理App中各种事务,在状态为StopFinish时退出。
OSX/iOS 系统中,提供了两个RunLoop对象:NSRunLoopCFRunLoopRef

  • CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
  • NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

如果想深入研究的话,可以下载源码

通过源码和调试可以得知以下几个知识点

  1. 在获取线程的runloop时,以线程为key,runloop为value保存到一个CFDictionary中,可以说明,线程和runloop是一一对应的。
  2. 保证线程不退出,则要往线程里加runloop。保证runloop不退出,则要往里面加timer、souce0、source1。注意:source0可以保证子runloop不退出,但不能唤醒runloop
  3. 主线程的runloop是系统创建好的,并用一个静态static变量保存,所以一直存在,保存在一个_CFTSDTable里。所以GetCurrentRunloop时候子线程才会去创建子Runloop。
  4. 保证runloop不退出的判读里,主runloop不需要判断里面是否添加了timer、source、observer。所以主runloop和子runloop是分开判断的。
  5. 唤醒runloop的条件有:timer、source1、手动CFRunLoopWakeUp、超时(会短暂唤醒立马再退出)
  6. AFNetworking2.0创建常驻线程的原理就是往一个子runloop中添加source1,即[NSMarchPort port].
  7. mach port是用来跨线程通讯的,可以发送消息message。例如:剪切板:剪切板的内容每个App都可以访问。
  8. runloop休眠后,会被一个接受消息mach_msg的mach port阻塞掉,以阻止CPU消耗, runloop唤醒或者添加timer本质都是通过mach port去取消掉block的线程。
  9. performSelector:withObject:afterDelay 依赖于线程的 runloop,因为它本质上是由一个定时器负责定期加入到 runloop 中执行。
  10. run 方法的文档还可以知道,它的本质就是无限调用 runMode:beforeDate: 方法,那么在run方法的下面的操作代码都不会被执行到。同样地,runUntilDate: 也会重复调用 runMode:beforeDate:,区别在于它超时后就不会再调用。总结来说,runMode:beforeDate: 表示的是 runloop 的单次调用,即唤醒过一次就退出了,不会再次唤醒,另外两者则是循环调用。想从 runloop 里面退出来,就不能用 run 方法。根据实践结果和文档,另外两种启动方法也无法手动退出,因为CFRunLoopIsStopped的是众多循环中的一次而已。
  11. 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的关系

  1. RunLoop 的超时时间就是使用 GCD 中的 dispatch_source_t来实现的。
  2. 当调用 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;
    }
}

本文只是对本人印象笔记的总结,因为已经有许多优秀的文章了,并没有对很多细节展开。若有不对的地方,还请欢迎指出纠正,谢谢。

你可能感兴趣的:(RunLoop总结)