Runloop是iOS和OSX开发中非常基础的一个概念,从概念开始学习。
RunLoop的概念
-般说,一个线程一次只能执行一个任务,执行完后线程退出。如果我们需要一个机制,让线程能随时处理事件但并不退出。通常代码逻辑是这样的:
function loop(){
initialize();
do{
var message = get_next_message();
process_message(message);
}while(message != quit);
}
这种模型通常被称作Event loop。Event loop 在很多系统和框架里都有实现,比如Node.js的事件处理,比如Windows程序的消息循环,再比如OSX/iOS里的RunLoop。实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时候休眠以避免资源占用、在有消息来时立刻被唤醒。
所以,RunLoop实际就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面的Eventloop的逻辑。线程执行了该函数后,就会一直处于这个函数内部“接受消息”->等待->处理的循环中,知道循环结束,函数退出。
OSX/iOS系统中提供了两个这样的对象:NSRunLoop和CFRunLoopRef。
CFRunLoopRef 是在CoreFoundation框架内,它提供了纯C的API,所有这些API都是线程安全的。NSRunLoop是基于CFRunLoopRef的封装,提供了面向对象的API,但是这些API不是线程安全的。
CFRunLoopRef是开源的,地址:http://opensource.apple.com/tarballs/CF/
Swift开源后,apple又维护了一个跨平台的CoreFounation,https://github.com/apple/swift-corelibs-foundation/,这个版本的源码可能和现有的iOS系统中实现略不一样,但更容易编译。
RunLoop与线程的关系
iOS开发中能遇到两个线程对象:pthread_t和NSThread。它们是一一对应的。比如,你可以通过pthread_main_thread_np()或者[NSThread mainThread]来获取主线程;也可以通过pthread_self()或者[NSThread currentThread]获取当前线程。CFRunLoop是基于pthread来管理的。
苹果不允许直接创建Runloop,它只提供了两个自动获取的函数:CFRunLoopGetMain()和CFRunLoopGetCurrent().这两个函数的内部逻辑大概是下面这样:
//全局的Dictionary,key是pthread_t,value 是CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
static CFpinlock_t loopslock;//访问loopsDic时的锁,防止被改变
//获取一个pthread对应的RunLoop。
CFRunloopRef _CFRunLoopGet(pthread_t thread){
OSSpinlocklock(&loopsLock)
if(!loopsDic){
//第一次进入时,初始化全局Dic,并为主线程创建一个RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainloop = _CFRunLoopCreat();
CFDictionarySetValue(loopsDic,pthread_main_thread_np(),mainloop);
}
///直接从Dictionary中获取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic,thread));
if(!loop){
///取不到、创建一个
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic,thread,loop);
///注册一个回调,当线程销毁时,顺便也销毁对应的Runloop。是这样吗?同一个Runloop里面不是可以对象很多thread吗?????
_CFSetTSD(...,thread, loop,_CFFinalizeRunloop);
}
OSSpinLockUnlock(&loopsLock)
return loop;
}
CFRunLoopRef CFRunLoopGetMain(){
return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent(){
return _CFRunLoopGet(pthread_self());
}
从上面的代码可以看到,线程和Runloop之间是一一对应的,其关系是保存在一个全局的Dictionary中,线程刚创建时并没有Runloop,如果你不主动获取,它一直都不会有。Runloop的创建时发生在第一次获取时,Runloop的销毁时发生在线程结束时。你只能在一个thread中获取其Runloop(主线程除外)
RunLoop对外的接口
在CoreFoundation中关于RunLoop有5个类:CFRunLoopRef、CFRunLoopModeRef、CFRunLoopSourceRef、CFRunloopTimerRef、CFRunLoopObserverRef
CFRunLoopModeRef并没有对外暴露,一个RunLoop包含若干个Mode,每个mode又包含若干Source/Timer/Observer。每次调用RunLoop的主函数时,只能指定一个Mode,这个Mode称为CurrentMode。如果需要切换Mode,只能退出Loop,再重新指定一个Mode进去。这样是为了分隔开不同组的Source/Timer/Observer,互不影响。
CFRunLoopSourceRef 是事件产生的地方。Source有两个版本Source0和Source1.
Source0包含了一个回调(函数指针),它并不主动出发时间。使用时,你需要先调用CFRunLoopSourceSignal(source),将这个Source标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)来唤醒Runloop,让其处理这个事件。
Source1包含了一个mach_port和一个回调,被用于通过内核和其他线程互相发送消息。这个Source 能主动唤醒Runloop的线程。原理稍后讲到。
CFRunloopTimerRef 是基于时间的触发器。它和NStimer是可以混用通过toll-free bridged。其包含了一个时间长度和一个回调。当其加入到Runloop时,RunLoop会注册对应的时间点儿,当时间点到了,RunLoop会被唤醒执行那个回调。
CFRunLoopObserverRef 是观察者,每个Observer都包含了一个回调,当RunLoop的状态发生变化时,观察者就能通过回调接受到这个变化。
typedefCF_OPTIONS(CFOptionFlags,CFRunLoopActivity){
kCFRunLoopEntry=(1UL<<0),// 即将进入Loop
kCFRunLoopBeforeTimers=(1UL<<1),// 即将处理 Timer
kCFRunLoopBeforeSources=(1UL<<2),// 即将处理 Source
kCFRunLoopBeforeWaiting=(1UL<<5),// 即将进入休眠
kCFRunLoopAfterWaiting=(1UL<<6),// 刚从休眠中唤醒
kCFRunLoopExit=(1UL<<7),// 即将退出Loop
};
上面的Source/Timer/Observer被统称为mode item,一个item可以 被同时加入多个mode。但是一个item被重复加入同一个mode是不会有效果的。如果一个mode中一个item都没有,则RunLoop会直接退出,不进入循环。
RunLoop的Mode
CFRunLoopMode和CFRunLoop的结构大致如下:
struct __CFRunLoopMode{
CFStringRef_name;// Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef_sources0;// Set
CFMutableSetRef_sources1;// Set
CFMutableArrayRef_observers;// Array
CFMutableArrayRef_timers;// Array
...
};
struct __CFRunLoop{
CFMutableSetRef _commonModes;// Set
CFMutableSetRef _commonModeItems;// Set
CFRunLoopModeRef _currentMode;// Current Runloop Mode
CFMutableSetRef _modes;// Set
...
};
这里有个概念叫“CommonModes”一个Mode可以将自己标记为“Common”属性(通过将其ModeName 添加到RunLoop的“commonModes”中)。每当RunLoop的内容发生变化时,RunLoop都会自动的_commonModeltems里的Source/Observer/Timer同步到具有“Common”标记的所有Mode里。
应用场景举例:主线程的RunLoop里有两个预置的Mode:kCFRunLoopDefaultMode和UITrackingRunLoopMode。这两个mode都已经标记为“Common”属性。DefaultMode是App平时所处的状态,TrackingRunLoopMode是追踪Scrollview滑动时的状态。当你创建一个Timer并加入DefaultMode时,Timer会得到重复调用,但此时滑动一个TablView时,RunLoop会将Mode切换为TrackingRunLoopMode,这时Timer就不会被调用,并且也不会影响到滑动操作。
有时候,你需要一个Timer,在两个Mode中都能得到回调,一种办法就是将这个Timer分别计入这两个Mode。还有一种方法,就是将Timer加入懂啊顶层的Runloop的“commonModeitems”中。commonModeItems被RunLoop自动更新到所有具有Common属性的Mode中。
CFRunLoopAddCommonMode(CFRunLoopRef rl, CFRunLoopMode mode)
CFRunLoopRunInMode(CFRunLoopMode mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled)
我们通过mode name来操作内部的mode,当你传入一个新的mode name但RunLoop内部没有对应的Mode时候,RunLoop会自动创建对应的CFRunLoopModeRef。对一个RunLoop说,其内部的mode只能增加不能删除。
苹果公开提供的 Mode 有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode,你可以用这两个 Mode Name 来操作其对应的 Mode。
同时苹果还提供了一个操作 Common 标记的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用这个字符串来操作 Common Items,或标记一个 Mode 为 “Common”。使用时注意区分这个字符串和其他 mode name
RunLoop的内部逻辑
/// 用DefaultMode启动
voidCFRunLoopRun(void){
CFRunLoopRunSpecific(CFRunLoopGetCurrent(),kCFRunLoopDefaultMode,1.0e10,false);
}
/// 用指定的Mode启动,允许设置RunLoop超时时间
intCFRunLoopRunInMode(CFStringRefmodeName,CFTimeIntervalseconds,BooleanstopAfterHandle){
returnCFRunLoopRunSpecific(CFRunLoopGetCurrent(),modeName,seconds,returnAfterSourceHandled);
}
/// RunLoop的实现
intCFRunLoopRunSpecific(runloop,modeName,seconds,stopAfterHandle){
/// 首先根据modeName找到对应mode
CFRunLoopModeRefcurrentMode=__CFRunLoopFindMode(runloop,modeName,false);
/// 如果mode里没有source/timer/observer, 直接返回。
if(__CFRunLoopModeIsEmpty(currentMode))return;
/// 1. 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(runloop,currentMode,kCFRunLoopEntry);
/// 内部函数,进入loop
__CFRunLoopRun(runloop,currentMode,seconds,returnAfterSourceHandled){
BooleansourceHandledThisLoop=NO;
intretVal=0;
do{
/// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop,currentMode,kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(runloop,currentMode,kCFRunLoopBeforeSources);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop,currentMode);
/// 4. RunLoop 触发 Source0 (非port) 回调。
sourceHandledThisLoop=__CFRunLoopDoSources0(runloop,currentMode,stopAfterHandle);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop,currentMode);
/// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if(__Source0DidDispatchPortLastTime){
BooleanhasMsg=__CFRunLoopServiceMachPort(dispatchPort,&msg)
if(hasMsg)gotohandle_msg;
}
/// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
if(!sourceHandledThisLoop){
__CFRunLoopDoObservers(runloop,currentMode,kCFRunLoopBeforeWaiting);
}
/// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
/// • 一个基于 port 的Source 的事件。
/// • 一个 Timer 到时间了
/// • RunLoop 自身的超时时间到了
/// • 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet,&msg,sizeof(msg_buffer),&livePort){
mach_msg(msg,MACH_RCV_MSG,port);// thread wait for receive msg
}
/// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(runloop,currentMode,kCFRunLoopAfterWaiting);
/// 收到消息,处理消息。
handle_msg:
/// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
if(msg_is_timer){
__CFRunLoopDoTimers(runloop,currentMode,mach_absolute_time())
}
/// 9.2 如果有dispatch到main_queue的block,执行block。
elseif(msg_is_dispatch){
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
/// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
else{
CFRunLoopSourceRefsource1=__CFRunLoopModeFindSourceForMachPort(runloop,currentMode,livePort);
sourceHandledThisLoop=__CFRunLoopDoSource1(runloop,currentMode,source1,msg);
if(sourceHandledThisLoop){
mach_msg(reply,MACH_SEND_MSG,reply);
}
}
/// 执行加入到Loop的block
__CFRunLoopDoBlocks(runloop,currentMode);
if(sourceHandledThisLoop&&stopAfterHandle){
/// 进入loop时参数说处理完事件就返回。
retVal=kCFRunLoopRunHandledSource;
}elseif(timeout){
/// 超出传入参数标记的超时时间了
retVal=kCFRunLoopRunTimedOut;
}elseif(__CFRunLoopIsStopped(runloop)){
/// 被外部调用者强制停止了
retVal=kCFRunLoopRunStopped;
}elseif(__CFRunLoopModeIsEmpty(runloop,currentMode)){
/// source/timer/observer一个都没有了
retVal=kCFRunLoopRunFinished;
}
/// 如果没超时,mode里没空,loop也没被停止,那继续loop。
}while(retVal==0);
}
/// 10. 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl,currentMode,kCFRunLoopExit);
}
可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。
苹果官方将整个系统大致划分为上述4个层次:
应用层包括用户能接触到的图形应用,例如 Spotlight、Aqua、SpringBoard 等。
应用框架层即开发人员接触到的 Cocoa 等框架。
核心框架层包括各种核心框架、OpenGL 等内容。
Darwin 即操作系统的核心,包括系统内核、驱动、Shell 等内容,这一层是开源的,其所有源码都可以在opensource.apple.com里找到。
其中,在硬件层上面的三个组成部分:Mach、BSD、IOKit (还包括一些上面没标注的内容),共同组成了 XNU 内核。
XNU 内核的内环被称作 Mach,其作为一个微内核,仅提供了诸如处理器调度、IPC (进程间通信)等非常少量的基础服务。
BSD 层可以看作围绕 Mach 层的一个外环,其提供了诸如进程管理、文件系统和网络等功能。
IOKit 层是为设备驱动提供了一个面向对象(C++)的一个框架。
Mach 本身提供的 API 非常有限,而且苹果也不鼓励使用 Mach 的 API,但是这些API非常基础,如果没有这些API的话,其他任何工作都无法实施。在 Mach 中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为”对象”。和其他架构不同, Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。”消息”是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。
Mach 的消息定义是在 头文件的,很简单:
typedef struct{
mach_msg_header_theader;
mach_msg_body_tbody;
}mach_msg_base_t;
typedef struct{
mach_msg_bits_tmsgh_bits;
mach_msg_size_tmsgh_size;
mach_port_tmsgh_remote_port;
mach_port_tmsgh_local_port;
mach_port_name_tmsgh_voucher_port;
mach_msg_id_tmsgh_id;
}mach_msg_header_t;
一条 Mach 消息实际上就是一个二进制数据包 (BLOB),其头部定义了当前端口 local_port 和目标端口 remote_port,
发送和接受消息是通过同一个 API 进行的,其 option 标记了消息传递的方向:
mach_msg_return_t mach_msg(
mach_msg_header_t *msg,
mach_msg_option_toption,
mach_msg_size_tsend_size,
mach_msg_size_trcv_size,
mach_port_name_trcv_name,
mach_msg_timeout_ttimeout,
mach_port_name_tnotify);
为了实现消息的发送和接收,mach_msg() 函数实际上是调用了一个 Mach 陷阱 (trap),即函数mach_msg_trap(),陷阱这个概念在 Mach 中等同于系统调用。当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到内核态;内核态中内核实现的 mach_msg() 函数会完成实际的工作,如下图:
RunLoop 的核心就是一个 mach_msg() (见上面代码的第7步),RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap() 这个地方
苹果用RunLoop实现的功能
首先我们看下App启动后RunLoop的状态:
CFRunLoop{
currentmode=kCFRunLoopDefaultMode
commonmodes={
UITrackingRunLoopMode
kCFRunLoopDefaultMode
}
commonmodeitems={
// source0 (manual)
CFRunLoopSource{order=-1,{
callout=_UIApplicationHandleEventQueue}}
CFRunLoopSource{order=-1,{
callout=PurpleEventSignalCallback}}
CFRunLoopSource{order=0,{
callout=FBSSerialQueueRunLoopSourceHandler}}
// source1 (mach port)
CFRunLoopSource{order=0,{port=17923}}
CFRunLoopSource{order=0,{port=12039}}
CFRunLoopSource{order=0,{port=16647}}
CFRunLoopSource{order=-1,{
callout=PurpleEventCallback}}
CFRunLoopSource{order=0,{port=2407,
callout=_ZL20notify_port_callbackP12__CFMachPortPvlS1_}}
CFRunLoopSource{order=0,{port=1c03,
callout=__IOHIDEventSystemClientAvailabilityCallback}}
CFRunLoopSource{order=0,{port=1b03,
callout=__IOHIDEventSystemClientQueueCallback}}
CFRunLoopSource{order=1,{port=1903,
callout=__IOMIGMachPortPortCallback}}
// Ovserver
CFRunLoopObserver{order=-2147483647,activities=0x1,// Entry
callout=_wrapRunLoopWithAutoreleasePoolHandler}
CFRunLoopObserver{order=0,activities=0x20,// BeforeWaiting
callout=_UIGestureRecognizerUpdateObserver}
CFRunLoopObserver{order=1999000,activities=0xa0,// BeforeWaiting | Exit
callout=_afterCACommitHandler}
CFRunLoopObserver{order=2000000,activities=0xa0,// BeforeWaiting | Exit
callout=_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
CFRunLoopObserver{order=2147483647,activities=0xa0,// BeforeWaiting | Exit
callout=_wrapRunLoopWithAutoreleasePoolHandler}
// Timer
CFRunLoopTimer{firing=No,interval=3.1536e+09,tolerance=0,
nextfiredate=453098071(-4421.76019@96223387169499),
callout=_ZN2CAL14timer_callbackEP16__CFRunLoopTimerPv(QuartzCore.framework)}
},
modes={
CFRunLoopMode{
sources0={/* same as 'common mode items' */},
sources1={/* same as 'common mode items' */},
observers={/* same as 'common mode items' */},
timers={/* same as 'common mode items' */},
},
CFRunLoopMode{
sources0={/* same as 'common mode items' */},
sources1={/* same as 'common mode items' */},
observers={/* same as 'common mode items' */},
timers={/* same as 'common mode items' */},
},
CFRunLoopMode{
sources0={
CFRunLoopSource{order=0,{
callout=FBSSerialQueueRunLoopSourceHandler}}
},
sources1=(null),
observers={
CFRunLoopObserver>{activities=0xa0,order=2000000,
callout=_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
)},
timers=(null),
},
CFRunLoopMode{
sources0={
CFRunLoopSource{order=-1,{
callout=PurpleEventSignalCallback}}
},
sources1={
CFRunLoopSource{order=-1,{
callout=PurpleEventCallback}}
},
observers=(null),
timers=(null),
},
CFRunLoopMode{
sources0=(null),
sources1=(null),
observers=(null),
timers=(null),
}
}
}
可以看到,系统默认注册了5个Mode:
1、kCFRunLoopDefaultMode:App的默认Mode,通常主线程就是在这个Mode下运行。
2、UITackingRunLoopMode:界面跟踪Mode,用于Scrollview追踪触摸滑动,保证界面滑动时不受其他mode影响。
3、UIinitializationRunLoopMode:在刚启动App时第一Mode,启动后就不再使用。
4、GSEventReceiveRunLoopMode:接受系统事件的内部Mode,通常用不到。
5、kCFRunLoopCommonModes:这是一个占位Mode,没有实际作用。
当RunLoop进行回调时,一般都是通过一个很长的函数调用出去(call out),当你在呢的代码中下断点调试时,通常在调试栈上能看到这些函数。
{
/// 1. 通知Observers,即将进入RunLoop
/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do{
/// 2. 通知 Observers: 即将触发 Timer 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 触发 Source0 (非基于port的) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers,即将进入休眠
/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// 7. sleep to wait msg.
mach_msg()->mach_msg_trap();
/// 8. 通知Observers,线程被唤醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9. 如果是被Timer唤醒的,回调Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
}while(...);
/// 10. 通知Observers,即将退出RunLoop
/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
AutoreleasePool
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 了。
事件响应
苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考这里。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。
_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
手势识别
当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。
苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
界面更新
当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去
苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面
定时器
NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。
CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop
PerformSelecter
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效
关于GCD
实际上 RunLoop 底层也会用到 GCD 的东西,NSTimer 是用了 XNU 内核的 mk_timer, NSTimer 确实是由 mk_timer 驱动,而非 GCD 驱动的)。但同时 GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。
当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。
网络请求
iOS 中,关于网络请求的接口自下至上有如下几层:
CFSocket
CFNetwork->ASIHttpRequest
NSURLConnection->AFNetworking
NSURLSession->AFNetworking2,Alamofire
• CFSocket 是最底层的接口,只负责 socket 通信。
• CFNetwork 是基于 CFSocket 等接口的上层封装,ASIHttpRequest 工作于这一层。
• NSURLConnection 是基于 CFNetwork 的更高层的封装,提供面向对象的接口,AFNetworking 工作于这一层。
• NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底层仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 线程),AFNetworking2 和 Alamofire 工作于这一层。
下面主要介绍下 NSURLConnection 的工作过程。
通常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0 (即需要手动触发的Source)。CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 的
当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。
NSURLConnectionLoader 中的 RunLoop 通过一些基于 mach port 的 Source 接收来自底层 CFSocket 的通知。当收到通知后,其会在合适的时机向 CFMultiplexerSource 等 Source0 发送通知,同时唤醒 Delegate 线程的 RunLoop 来让其处理这些通知。CFMultiplexerSource 会在 Delegate 线程的 RunLoop 对 Delegate 执行实际的回调。
RunLoop 的应用
AFNetworking
AFURLConnectionOperation 这个类时基于NSURLConnection 构建的,其希望能在后台线程接收Delegate回调。为此AFnetWorking单独创建一个线程,并在线程中启动了一个Runloop:
+(void) networkRequestThreadEntryPint:(id) __unused object {
@autoreleasepool {
[NSThread currentThread] set Name:@" 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(networkRequestThreadEntryPint:) object:nil];
[_networkRequestThread start]; });
return _networkRequestThread;
}
RunLoop 启动前内部必现至少有一个timer/observer/Source,所以AFNetworking 在[runloop run] 之前先创建一个NSMachPort添加进去,产生一个source1.通常情况下,调用者需要持有这个NSMachPort(mach_port)并在外部线程通过这个port发送信息到loop内;但是此处添加port就是为了让loop不退出,并没有实际发送消息。
-(void)start {
[self.lock lock];
if( [self isCancelled]){
[self performSelector:@selector(cancelConnection) onThread:[[ self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self. runloopModes allObjects]];
}else if ([self isReady]){
self.state = AFOperatinExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[ self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self. runloopModes allObjects]];
}
[self.lock unlock];
}
当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中
总结
这是一篇学习笔记,大部分参考了大神的作品,反复看了几遍,也查看了开源的代码,发现苹果最新的维护CoreFoundation中的用法和大神说的有出入,不过大部分原理都是一致的。个人学习体会如下:
1、Runloop是在线程的基础上存在的,pthread_t.也就是说thread可以没有runloop,但是后者不行。问题来了,一个thread开一个runloop 对app性能有影响吗?为什么?这个问题是有待进一步研究。
总结,学习必须动手验证。。。。
本文参考:https://blog.ibireme.com/2015/05/18/runloop/