iOS-RunLoop学习

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/

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

推荐阅读更多精彩内容