美文网首页
iOS-RunLoop学习

iOS-RunLoop学习

作者: 小猫仔 | 来源:发表于2017-09-27 18:30 被阅读0次

    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/

    相关文章

      网友评论

          本文标题:iOS-RunLoop学习

          本文链接:https://www.haomeiwen.com/subject/gcvsextx.html