3.iOS开发之Runloop

RunLoop简介

运行循环,在程序运行过程中循环做一些事情,如果没有Runloop程序执行完毕就会立即退出,如果有Runloop程序会一直运行,并且时时刻刻在等待用户的输入操作。RunLoop可以在需要的时候自己跑起来运行,在没有操作的时候就停下来休息。充分节省CPU资源,提高程序性能。

do {

//接受消息->等待->处理

}while(message != quit)

RunLoop对象

在 CoreFoundation 框架为 CFRunLoopRef 对象,它提供了纯 C 函数的 API,并且这些 API 是线程安全的;

在 Foundation 框架中用 NSRunLoop 对象来表示,它是基于 CFRunLoopRef 的封装,提供的是面向对象的 API,但这些 API 不是线程安全的。

RunLoop的构成

CFRunLoopRef (NSRunloop)

CFRunLoopModeRef (NSRunloopMode)

CFRunLoopSourceRef

CFRunLoopTimerRef

CFRunLoopObserverRef

一个RunLoop包含若干个Mode(CFRunLoopModeRef),每个Mode又包含若干个source/timer/observer,而RunLoop启动时只能选择其中一个Mode作为currentMode。

CFRunLoopModeRef (NSRunloopMode)

公开的 mode 有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode。

还有一个伪Mode是kCFRunLoopCommonModes(NSRunLoopCommonModes) 若干个mode的集合。

程序运行的大多时候都处于该 DefaultMode 下,滑动 tableView 或 scrollerView 时为了界面流畅而用的TrackingRunLoopMode。

一个 RunLoop 在某个 mode 下运行时,不会接收和处理其他 mode 的事件 。

(tableview滑动时timer停止的原因,可将timer默认的DefaultMode改成CommonModes)

CFRunLoopSourceRef

官方文档在概念上把 source 分为三类:Port-Based Sources,Custom Input Sources,Cocoa Perform Selector Sources。在源码中根据标记则分为source0 和 source1。

Source0 : 负责App内部事件,由App负责管理触发,例如UIEvent、UITouch事件。包含了一个回调,不能主动触发事件。使用时,你需要先调用CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)来唤醒 RunLoop,让其处理这个事件。

-performSelector:onThread:withObject:waitUntilDone: inModes:创建的是source0任务。

Source1 : 包含一个 mach_port 和一个回调,可监听系统端口和通过内核和其他线程发送的消息,能主动唤醒runloop,接收分发系统事件。

Source1和Timer都属于端口事件源,不同的是所有的Timer都共用一个端口(Timer Port),而每个Source1都有不同的对应端口。

Source0属于input Source中的一部分,Input Source还包括cuntom自定义源,由其他线程手动发出。

CFRunLoopTimerRef

定时器、即NSTimer,还可以由方法 performSelector:afterDelay:来触发(本质上 afterDelay, 底层就是启动了 timer )。

CFRunLoopObserverRef

监听 RunLoop 7种状态的变化:

kCFRunLoopEntry = (1UL << 0), // 即将进入Loop

kCFRunLoopBeforeTimers = (1UL << 1), //即将处理Timer

kCFRunLoopBeforeSources = (1UL << 2), //即将处理Source

kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠

kCFRunLoopAfterWaiting = (1UL << 6), //刚从休眠中唤醒

kCFRunLoopExit = (1UL << 7), //即将退出Loop

kCFRunLoopAllActivities = 0x0FFFFFFFU //所有状态改变


RunLoop中几个类的关系图(转自网络)

RunLoop和线程间的关系

每条线程都有唯一的一个与之对应的RunLoop对象

RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value

主线程的RunLoop会自动创建,在UIApplicationMain函数中,保证了程序的持续运行,子线程的RunLoop需要主动创建(子线程timer不起作用的原因)

RunLoop在第一次获取时创建(有点像懒加载),在线程结束时销毁,依赖于线程

获取主线程或当前线程对应的 RunLoop,只能通过 CFRunLoopGetMain(NSRunLoop.mainRunLoop) 或 CFRunLoopGetCurrent (NSRunLoop.currentRunLoop)获取。

RunLoop的运行逻辑

runloop 整个的运行逻辑都是在于三个重要的对象如何运作:source (输入源)、timer (定时器)、observer (观察者)。


RunLoop运行逻辑(图片转自网络)

runloop 的运行逻辑就是 do-while 循环下运用观察者(observer)模式(或者说是消息发送),根据7种状态的变化,处理事件输入源(source)和定时器(timer)。

RunLoop 实际应用

1、自动释放池

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 了。

2、事件响应

App响应触摸事件

当一个硬件事件(触摸/锁屏/摇晃/加速等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收, 随后由mach port 转发给需要的App进程。【系统接收部分】

(1).APP进程的mach port接收来自SpringBoard的触摸事件,主线程的runloop被唤醒,触发source1回调。

(2).source1回调又触发了一个source0回调,调用_UIApplicationHandleEventQueue() 进行应用内部的分发,将接收到的IOHIDEvent对象封装成UIEvent对象,此时APP将正式开始对于触摸事件的响应。

(3).source0回调将触摸事件添加到UIApplication的事件队列,当触摸事件出队后UIApplication为触摸事件寻找最佳响应者。

(4).寻找到最佳响应者之后,接下来的事情便是事件在响应链中传递和响应。

3、手势识别

调用_UIApplicationHandleEventQueue() 识别到是一个guesture手势,会调用Cancel方法将当前的touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,其回调函数为 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。

当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

4、界面刷新

当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。

苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

5、GCD

当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

6、定时器

NSTimer 其实就是 CFRunLoopTimerRef

performSelecter:afterDelay: 实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中

当调用 performSelector:onThread: 时如果waitUntilDone为NO,如果对应线程没有 RunLoop 该方法也会失效。

7、常驻线程

子线程默认是完成任务后结束。当要经常使用子线程,每次开启子线程比较耗性能。此时可以开启子线程的 RunLoop,保持 RunLoop 运行,则使子线程保持不死。

因为 RunLoop 启动前必须设置一个 mode,而 mode 要存在则至少需要一个 source / timer。可以为 RunLoop 的 DefaultMode 添加一个 NSMachPort 对象,虽然消息是可以通过 NSMachPort 对象发送到 loop 内,但这里添加的 port 只是为了 RunLoop 一直不退出,而没有发送什么消息。当然我们也可以添加一个超长启动时间的 timer 来既保持 RunLoop 不退出也不占用资源。

代表:AF2.x (AF3.0不再需要线程常驻)

8、异步渲染

UI 线程中一旦出现繁重的任务就会导致界面卡顿,这类任务通常分为3类:排版,绘制,UI对象操作。

排版通常包括计算视图大小、计算文本高度、重新计算子式图的排版等操作。

绘制一般有文本绘制 (例如 CoreText)、图片绘制 (例如预先解压)、元素绘制 (Quartz)等操作。

UI对象操作通常包括 UIView/CALayer 等 UI 对象的创建、设置属性和销毁。

其中前两类操作可以通过各种方法扔到后台线程执行,而最后一类操作只能在主线程完成,并且有时后面的操作需要依赖前面操作的结果 (例如TextView创建时可能需要提前计算出文本的大小)。尽量将能放入后台的任务放入后台,不能的则尽量推迟 (例如视图的创建、属性的调整)。

在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。

代表:YYKit Texture



参考 runloop源码  最全Runloop 深入Runloop

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

推荐阅读更多精彩内容

  • Runloop是iOS和OSX开发中非常基础的一个概念,从概念开始学习。 RunLoop的概念 -般说,一个线程一...
    小猫仔阅读 995评论 0 1
  • 转自bireme,原地址:https://blog.ibireme.com/2015/05/18/runloop/...
    乜_啊_阅读 1,372评论 0 5
  • ======================= 前言 RunLoop 是 iOS 和 OSX 开发中非常基础的一个...
    i憬铭阅读 877评论 0 4
  • 转自http://blog.ibireme.com/2015/05/18/runloop 深入理解RunLoop ...
    飘金阅读 983评论 0 4
  • RunLoop 的概念 一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线...
    Mirsiter_魏阅读 618评论 0 2