iOS runloop 学习笔记(四) - 总结

通过前面几篇文章可以知道RunLoop实际上是一个事件处理的循环.只要一个线程启动了RunLoop,在它没有收到事件时候,它就会使得线程休眠,如果有事件,就调用相应的事件处理函数.

RunLoop与线程的关系

RunLoop与线程是一一对应的.每个新建的线程(MovedainThread除外,主线程会默认启动)在默认状态下,它的RunLoop是没有开启的,必须手动调用CurrentRunLoop等方法才会开启RunLoop.通常iOS开发中,接触到的API分别是Cocoa的NSRunLoop和CoreFoundation的CFRunLoop.

RunLoop的组成

RunLoop每次只能运行在一个mode中,它与mode是一对多的关系.RunLoopMode中包括不同类型的事件源,主要分成两大类:TimerSource, 各种异步输入InputSource.与此同时,RunLoop会在不同的声明周期给外界发送通知,告知观察者自己的状态.因此可以注册RunLoopObserver来观察RunLoop的状态.他们之间的关系如下:

  • 一个runLoop包含多个Mode,每个Mode内有多个个Source/Timer/Observer(底层会在RunLoopMode中用Array存储)
  • 每次启动RunLoop,需要指定一个Mode(CFRunLoopRunSpecific函数),如果需要切换Mode,只能退出RunLoop,再重新调用CFRunLoopRunSpecific,重新指定Mode
  • Source/Timer/Observer统称为mode item,每个item可以同时加入多个mode,但一个item被重复加入同一个mode无效
  • 如果一个mode中一个item都没有,RunLoop就会立即退出(AFNetworking2.x)

1. RunLoopMode

RunLoop会运行在某个特定的mode下,称为RunLoopMode,具体mode可以使用系统的NSDefaultMode或者NSTrackingMode,也可以自己定义mode.如果想让事件在任何一个mode中处理,可以把事件源加入到CommonMode中.

2. TimerSource

TimerSource在Cocoa框架中,一般是NSTimer.在使用它时,它会向RunLoop发送消息,RunLoop会根据Timer的类型去单次或者多次执行回调函数.在开始启动Timer时候,需要手动设定具体运行在哪一个RunLoopMode,默认情况下会运行在NSDefaultMode.

通过RunLoop的源码发现,TimerSource的触发实际是通过Source1(MachPort)唤醒RunLoop,然后调用回调方法的.

3. 异步输入源InputSource

异步事件其实会包括的类型:

  • source0: 非port事件,可以手动触发(内部只有一个callback函数指针)
  • source1: 基于MachPort的Source事件,大多是内核发出的,需要调用mach_msg方法

4. NSObject的selector源

由NSObject 的 performSelector:wait:...等产生,下文会细讲.

5. RunLoopObserver

RunLoop的事件源中,Timer是同步事件,另外的是异步事件Source0,Source1,selector. 而RunLoopObserver可以让你在某个特定的时期处理一些事情,比如在RunLoop马上进入休眠状态时,或者在刚刚被唤醒时等等.

具体的RunLoop可以被观察的状态有:

  • RunLoop进入时候(kCFRunLoopEntry)
  • RunLoop将要处理Timer时(kCFRunLoopBeforeTimers)
  • RunLoop将要处理Source0时(kCFRunLoopBeforeSources)
  • RunLoop将要进入睡眠的时候(kCFRunLoopBeforeWaiting)
  • RunLoop将要被唤醒的时(kCFRunLoopAfterWaiting)
  • RunLoop停止的时候(kCFRunLoopExit)

你可以在RunLoop中观察一个或者多个状态.观察者也可以设置是否只观察一次或者重复多次观察,如果只观察一次,那么调用回调函数以后,观察者就自动被移除了.

RunLoop具体循环逻辑

RunLoop源码中关键的函数有以下几个:

CFRunLoopRunSpecific(xxxx)//入口&出口
CFRunLoopRun(xxxx)//循环

具体的执行过程如下:

1 通知观察者runLoop已经启动(DoObserver)
2 记录RunLoop启动事件,并设定RunLoop运行的超时条件

**** RunLoop正式进入循环 *****

3 通知观察者任何即将要开始的定时器(DoObserver)
4 通知观察着任何即将启动的非基于端口的源(DoObserver)
5 调用RunLoopMode的blocks链中的block方法(DoBlocks)
6 启动任何准备好的非基于端口的source0源(DoSource0)
7 调用RunLoopMode的blocks链中的block方法(DoBlocks)
8 检查MachPort端口是否有消息要处理,如果有立即进入步骤12
9 如果没有消息处理,通知观察者线程进入休眠(DoObserver)

**** RunLoop休眠: zzz...****

10 将线程至于休眠直到任意下面的事件发生(调用`mach_msg`使RunLoop休眠)
    A.某一时间到达基于端口的源
    B.定时器启动
    C.runLoop设置的时间已经超过
    D.runLoop被显示唤醒
11 通知观察者线程将被唤醒(DoObserver)
12 处理未处理的事件(有dispatchPort显示是哪个port的事件需要处理)
    A.如果livePort为NULL,啥都不做(会跳出循环)
    B.如果livePort是wakeUpPort,说明RunLoop运行超时(会跳出循环)
    C.如果livePort是timerPort,说明timerSource启动(DoTimers),进入步骤3
    D.如果livePort是dispatchPort,说明系统的libDispatch向主线程送消息,
      会调dispatch_async(dispatch_get_main_queue(),block)
    E.如果livePort是其他port,通过mach_msg取出回调,处理事件(DoSource1),
      进入步骤3
    
**** RunLoop退出循环 ****

13 通知观察者runLoop结束

从RunLoop的底层源码可以看出,RunLoop中的各种DoXXXX函数与最终的callout系统调用关系如下:

  • DoObserver
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__()
  • DoBlocks
    __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__()
  • DoSource0 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__()
  • DoTimers __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__()
  • dispatchPort __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
  • DoSource1 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__()

注意,以上callout调用,对于传入RunLoopMode中的Timers,Source0s,Blocks都是数组,因此对数组内的每个满足条件的成员都会进行callout调用,而source1和dispatchPort由于唤醒时每次只能执行一个.

repeat Timers执行的时间不一定是完全准确的,如果错过某次执行,只能等到下次RunLoop循环时候再执行了

何时使用RunLoop

自己配置并启动它,你不需要再任何情况下都去启动一个线程的runLoop。runLoop在你
要和线程有更多的交互时才需要,比如以下情况:

  • 使用端口或者自定义输入源来和其他线程通信;
  • 使用线程的NSTimer (可以用dispatch_timer代替)
  • Cocoa中使用任何performSelector
  • 让线程周期性干活(AFNetworking2.x)

CFRunLoop相关内容

CoreFoundation中RunLoop的接口

一般使用的是CoreFoundation中的CFRunLoop,因此这里主要总结CFRunLoop的对外接口.RunLoop中对外主要有以下几个接口类:

  • CFRunLoopRef
  • CFRunLoopModeRef (并未对外暴露)
  • CFRunLoopSourceRef: 异步的source0和source1事件源
    • source0: 结构体中只有perform回调函数,它并不能主动触发事件,使用时,需要手动触发(CFRunLoopSourceSignal()).CFRunLoopSourceSignal将这个source标记为待处理,后调用CFRunLoopWakeUp来唤醒runLoop,让其处理这个事件(也就是source0无法唤醒RunLoop)
    • source1: 结构体中有mach_port和invoke回调函数,通常是内核和其他进程给该RunLoop发送的消息,从源码可以看出source1,是可以自动唤醒RunLoop的,因为它是通过port方式发送事件的.
  • CFRunLoopTimerRef: 同步的TimerSource, 在Cocoa中是NSTimer.它的底层结构体中包含触发时间和回调,当其加入到runLoop中是,runLoop会注册对应的时间点,当时
    间点到时,RunLoop会收到wakeUpPort(RunLoop的一个属性)source1类型的消息,将唤醒以执行Timer的回调;
  • CFRunLoopObserverRef: 注册观察者时候可以添加一个回调函数,当RunLoop的状态变化时会通知注册的观察者,注册的回调函数在此时调用

具体的demo可以参考:
https://github.com/brownfeng/RunLoopDemo

RunLoop底层结构体

CFRunLoop的结构如下

struct __CFRunLoop {
    CFMutableSetRef _commonModes;     
    CFMutableSetRef _commonModeItems; 
    CFRunLoopModeRef _currentMode;    
    CFMutableSetRef _modes;           
    ...
};

CFRunLoopMode的结构如下:

struct __CFRunLoopMode {
    CFStringRef _name;            
    CFMutableSetRef _sources0;    
    CFMutableSetRef _sources1;    
    CFMutableArrayRef _observers; 
    CFMutableArrayRef _timers;    
    ...
};

其中,CFRunLoop对外暴露的管理Mode的接口有两个:

CFRunLoopAddCommonMode
CFRunLoopRunInMode

Mode暴露的管理mode item的接口有下面几个:

CFRunLoopAddSource
CFRunLoopAddObserver
CFRunLoopAddTimer
CFRunLoopRemoveSource
CFRunLoopRemoveObserver
CFRunLoopRemoveTimer

Cocoa框架中RunLoop相关的内容

1 AutoReleasePool

AutoReleasePool是Apple中清理临时变量,释放内容的机制,在app的main函数中,所有的内容都是包裹在一个AutoReleasePool中的.这个过程中,Apple在RunLoop中注册三个RunLoopObserver:

  • 第一个RunLoopObserver关注kCFRunLoopEntry状态,callback函数中会自动创建autoReleasePool,并且observer优先级最高(RunLoop第一个调用它的回调),保证应用启动后所有的操作都在autoreleasePool中运行.
  • 第二个RunLoopObserver关注kCFRunLoopBeforeWaiting状态,在此时autoreleasePool会释放旧的池,并创建一个新的autoreleasePool.
  • 第三个RunLoopObserver关注kCFRunLoopExit状态,此时释放autoreleasePool,并且保证observer的优先级最低,即在所有其他的observers都执行完以后才执行autorelease相关的observer的callback函数.

深入学习可以参考:
http://blog.sunnyxx.com/2014/10/15/behind-autorelease/

2 iOS事件响应

RunLoop解释了为何iOS应用能够接受到屏幕触摸等事件.Apple在iOS app启动时候在main RunLoop中注册一个Source1事件,是基于Mach Port(进程间通信)的系统层面的进程.

当一个触摸事件发生以后,IOKit会生成一个IOHIDEvent事件,并由系统层面的SpringBoard接受按键(锁屏/静音),触摸,加速,接近传感器等几种Event,然后通过进程间通信的mach port发送给App的main RunLoop.然后在DoSource1中,source1的回调会触发,内部会调用系统_UIApplicationHandleEventQueue()进行事件分发.

_UIApplicationHandleEventQueue()方法会把IOHIDEvent处理,并包装成常见的UIEvent事件进行处理或分发,其中包括识别UIGesture/处理屏幕旋转/发送给UIWindow等.通常事件比如UIButton点击,touchesBegin/Move/End/Cancel等事件都是在这个Source1回调中完成.

3 手势识别GestureRecognizer

在上面的事件响应中,回调方法_UIApplicationHandleEventQueue()识别了一个手势以后,首先会调用Cancel将当前的touchesBegin/Move/End系列的回调打断.随后系统会将UIGestureRecognizer标记为等待处理.

然后苹果会在RunLoop中注册一个Observer观察kCFRunLoopBeforeWaiting(睡眠前).这个观察者的回调函数是_UIGestureRecognizerUpdateObserver(),它会获取刚才所有被标记为等待处理等的GestureRecognizer,并且执行GestureRecognizer的回调.

4 界面更新(UI update)

在App改变UI时,例如修改view的Frame,更新UIView/CALayer的层次,或者手动调用UIView/CALayer的setNeedsLayout/setNeedsDisplay方法以后,这个UIView/CALayer就被标记为等待处理,并且提交到一个全局容器.

然后苹果在RunLoop中注册一个Observer观察kCFRunLoopBeforeWaiting和kCFRunLoopExit状态.在状态触发DoObserver的callback中会调用函数_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv().这个函数中会遍历所有的等待处理的UIView/CALayer,执行绘制和调整,更新UI界面.

这个函数的调用栈如下:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
    QuartzCore:CA::Transaction::observer_callback://DoObserver
        CA::Transaction::commit();//提交到全局容器
            CA::Context::commit_transaction();
                CA::Layer::layout_and_display_if_needed();
                    CA::Layer::layout_if_needed();
                        [CALayer layoutSublayers];
                            [UIView layoutSubviews];
                    CA::Layer::display_if_needed();
                        [CALayer display];
                            [UIView drawRect];

5 NSTimer定时器

NSTimer的CF层是CFRunLoopTimerRef,它们可以toll-free bridged.当使用CFRunLoopAddTimer()将NSTimer&CFRunLoopTimerRef注册到RunLoop中以后,RunLoop会为它重复的时间点注册好事件(有一定的时间容忍度,触发的时间误差).如果中间某次触发被错过,那么这次触发时间点的回调也会被跳过去. 实际中NSTimer的触发是通过source1触发的,如果timer被触发会将RunLoop的有一个Mach Port-timePort-发消息,然后会唤醒RunLoop,调用DoTimers.可以参考上面的RunLoop运行流程.

CADisplayLink 是一个和屏幕刷新一致的定时器,如果两次屏幕刷新时候在执行一个长时间任务,那其中就会有一帧被跳过,造成界面卡顿.Facebook的AsyncDisplayLink使用RunLoop来解决丢帧的问题.

还有一个GCD的定时器dispatch_timer,RunLoop的超时机制也是使用dispatch_timer.

6 部分PerformSelector事件源

NSObject的部分与时间相关的PerformSelector方法与RunLoop密切相关.当调用NSObject的performSelector:afterDelay以后,实际内部会创建一个Timer并添加RunLoopMode中,等待Timer触发,然后调用DoTimers,其中包含的回调方法就是selector.因此如果当前线程没有开启RunLoop,这个方法无效.类似的NSObject的performSelector:onThread:也是类似,需要线程开启RunLoop.具体涉及到的perform方法包括:

- performSelector:afterDelay:
– performSelector:withObject:afterDelay:
– performSelectorOnMainThread:withObject:waitUntilDone:
– performSelectorOnMainThread:withObject:waitUntilDone:modes:
– performSelector:onThread:withObject:waitUntilDone:modes:
- performSelector:onThread:withObject:waitUntilDone:

7 GCD与RunLoop

GCD的中部分接口使用了RunLoop,提交到mainQueue的blocks.当调用dispatch_async(dispatch_get_main_queue(), block)时,libDispatch(系统的某个进程)会向app的进程的main runloop的dispatchPort(是一个mach port)发送消息.此时RunLoop如果在休眠状态,会被唤醒,并从dispatch port中取出消息,并在会在回调方法__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()中执行这个block.

libDispatch分发到main queue的block才会与RunLoop有关,分发到其他的子线程的内容,还是又libDispatch处理.

RunLoop的运行超时是由GCD的dispatch_timer控制的.

8 关于网络请求

关于网络请求的接口,主要有以下几层:

  • CFSocket:是最底层的接口,只负责socket的通信
  • CFNetwork:是基于CFSocket等接口的上层封装,ASIHttpRequest工作在这层
  • NSURLConnection:是基于CFNetwork的更高层的封装,提供面向对象的接口,
    AFNetworking2.x工作于这一层
  • NSURLSession:是ios7中新增的接口,表面上和NSURLConnection并列,但底层
    仍然用到NSURLConnection的部分功能,AFNetworking3.x和Alamofire在这层

因此,现在使用的大多网络库都与CFSocket,CFNetwork层相关.

通常使用NSURLConnection时,你会传入一个Delegate,当调用[connection start]后,这个Delegate就会不停收到事件回调。实际上,start 这个函数的内部会
会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0 (即需要手动
触发的Source)。CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookie
Storage 是处理各种 Cookie 的。

NSURLConnection的工作过程可以参考:
http://blog.ibireme.com/2015/05/18/runloop/#base

RunLoop的应用举例

1 AFNetworking2.x

使用NSURLConnection时候,需要在后台自定义线程中接受Delegate回调.AFNetworking创建一个自定义线程,然后在其中添加一个Mach Port,然后启动RunLoop(前面提到过,如果RunLoop中没有source/timer/observer会立即退出).在以后线程以后通过NSObject的performSelector:onThread:withObject:waitUnitlDone:modes:方法,将[self.connection scheduleInRunLoop:runLoop forMode:runLoopMode].

可以参考:
https://github.com/brownfeng/SourceSet/tree/master/AFNetworking2.x

2 AsyncDisplayKit

AsyncDisplayKit是Facebook推出的用于保持界面流畅性的框架,其原理大致如下:

UI线程中一旦出现繁重的任务就会导致界面卡顿,这类任务通常分为3类:排版,绘制,UI对象操作。通过各种方法将前两种任务丢到后台运行,最后一类操作只能在主线程中执行,因此,ASDK仿照QuartzCore/UIKit框架的模式,实现了一套类似的界面更新的机制:即在主线程的RunLoop中添加一个 Observer,监听kCFRunLoopBeforeWaiting和 kCFRunLoopExit事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。

3 NSTimer在TrackingMode运行

4 TableView延迟加载图片

5 RunLoop解决大图加载的问题

6 RunLoop监控App卡顿

可以参考文章:

http://www.jianshu.com/p/929d855c5a5a
http://www.jianshu.com/p/924cb2b218f5

参考内容

http://blog.ibireme.com/2015/05/18/runloop/

http://yun.baidu.com/share/link?shareid=2268593032&uk=2885973690

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

推荐阅读更多精彩内容