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;
    }
}

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • __CFRunLoops是一个字典已线程作为key CFRunLoopRef作为value 从以上代码分析可以看到...
    大墙66370阅读 3,843评论 1 10
  • 今天要介绍的RunLoop使用场景很有意思,在做长期项目,需要跟踪解决用户问题非常有用。 使用RunLoop 监测...
    咖啡绿茶1991阅读 472评论 1 1
  • iOS 中RunLoop 是一个事件循环对象 runloop跑一圈,只能执行一个事件。 一般一个线程执行任务完成后...
    小李不木阅读 589评论 0 0
  • 1.Runloop与线程 ①.RunLoop是寄生于线程的消息循环机制,它能保证线程存活,而不是线性执行完任务就消...
    罗马危机阅读 311评论 0 0
  • 一、概念 简单点说Runloop就是一个do while的运行循环。主要的作用就是保持程序的持续运行。比如主线程的...
    boundlessocean阅读 251评论 0 1