RunLoop

作者: Vinc | 来源:发表于2016-08-11 21:39 被阅读163次

    一、基本作用

    二、main函数中的RunLoop

    三、RunLoop对象

    1. iOS API
    2. NSRunLoop和CFRunLoopRef

    四、RunLoop与线程

    1. RunLoop和线程的关系
    2. RunLoop的创建
    3. RunLoop的生命周期

    五、获得RunLoop对象

    1. 获得当前RunLoop对象
    2. 拿到当前应用程序的主RunLoop(主线程对应的RunLoop)
    3. 注意点
    4. RunLoop对象存储

    六、RunLoop相关类

    1. RunLoop运行原理图
    2. 五个相关的类
    3. RunLoop和相关类之间的关系图
    4. RunLoop要想跑起来,它的内部必须要有一个mode,这个mode里面必须有sourceobservertimer,至少要有其中的一个。
      CFRunLoopModeRef
      CFRunLoopTimerRef
      DISPATCH_SOURCE_TYPE SOURCE类型
      CFRunLoopSourceRef
      CFRunLoopObserverRef

    七、Runloop运行逻辑

    八、以下内容全部转载自YY 的深入理解RunLoop | Garan no dou
    RunLoop 调用栈上函数的整理
    AutoreleasePool
    事件响应
    手势识别
    界面更新
    定时器
    PerformSelecter
    关于GCD
    关于网络请求
    RunLoop 的实际应用举例
    AFNetworking
    AsyncDisplayKit


    一、基本作用

    1. 保持程序的持续运行(iOS程序为什么能一直活着不会死)
    • 处理app中的各种事件(比如触摸事件、定时器事件【NSTimer】、selector事件【选择器·performSelector···】)
    • 节省CPU资源,提高程序性能,有事情就做事情,没事情就休息

    二、main函数中的RunLoop

    • 在UIApplication函数内部就启动了一个RunLoop
      该函数返回一个int类型的值
    • 这个默认启动的RunLoop是跟主线程相关联

    三、RunLoop对象

    1. iOS API
    1. Foundation框架【NSRunLoop
    • Core Foundation框架【CFRunLoopRef
    2. NSRunLoop和CFRunLoopRef
    • 都代表着RunLoop对象,它们是等价的,可以互相转换
    • NSRunLoop是基于CFRunLoopRef的一层OC包装,所以要了解RunLoop内部结构,需要多研究CFRunLoopRef层面的API(Core Foundation层面)

    四、RunLoop与线程

    1. RunLoop和线程的关系

    一个RunLoop对应着一条唯一的线程

    问题:如何让子线程不死
    回答:给这条子线程开启一个RunLoop
    
    2. RunLoop的创建

    主线程RunLoop已经创建好了,子线程的RunLoop需要手动创建

    3. RunLoop的生命周期

    在第一次获取时创建,在线程结束时销毁


    五、获得RunLoop对象

    1. 获得当前RunLoop对象
    //01 NSRunLoop
    NSRunLoop * runloop1 = [NSRunLoop currentRunLoop];
    //02 CFRunLoopRef
    CFRunLoopRef runloop2 =   CFRunLoopGetCurrent();
    
    2. 拿到当前应用程序的主RunLoop(主线程对应的RunLoop)
    //01 NSRunLoop
    NSRunLoop * runloop1 = [NSRunLoop mainRunLoop];
    //02 CFRunLoopRef
    CFRunLoopRef runloop2 =   CFRunLoopGetMain();
    
    3. 注意点
    • 开一个子线程创建RunLoop,不是通过alloc init方法创建,而是直接通过调用currentRunLoop方法来创建,它本身是一个懒加载的。
    • 在子线程中,如果不主动获取RunLoop的话,那么子线程内部是不会创建RunLoop的。可以下载CFRunLoopRef的源码,搜索_CFRunLoopGet0,查看代码。
    4. RunLoop对象存储

    利用字典来进行存储,而且key是对应的线程Value为该线程对应的RunLoop。


    六、RunLoop相关类

    1. RunLoop运行原理图
    RunLoop和相关类之间的关系图
    2. 五个相关的类
    a.CFRunLoopRef
    b.CFRunLoopModeRef【RunLoop的运行模式】
    c.CFRunLoopSourceRef【RunLoop要处理的事件源】
    d.CFRunLoopTimerRef【Timer事件】
    e.CFRunLoopObserverRef【RunLoop的观察者(监听者)】
    
    3. RunLoop和相关类之间的关系图
    RunLoop和相关类之间的关系图
    4. RunLoop要想跑起来,它的内部必须要有一个mode,这个mode里面必须有source\observer\timer,至少要有其中的一个。

    CFRunLoopModeRef
    1.CFRunLoopModeRef代表着RunLoop的运行模式
    2.一个RunLoop中可以有多个mode,一个mode里面又可以有多个source\observer\timer等等
    3.每次RunLoop启动的时候,只能指定一个mode,这个mode被称为该RunLoop的当前mode
    4.如果需要切换mode,只能先退出当前RunLoop,再重新指定一个mode进入
    5.这样做主要是为了分割不同组的定时器等,让他们相互之间不受影响
    6.系统默认注册了5个mode
        a.kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
        b.UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
        c.UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
        d.GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
        e.kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode
    
    

    CFRunLoopTimerRef

    (1)NSTimer相关代码

    /*
        说明:
        (1)RunLoop一启动就会选中一种模式,当选中了一种模式之后其它的模式就都不管。一个mode里面可以添加多个NSTimer,也就是说以后当创建NSTimer的时候,可以指定它是在什么模式下运行的。
        (2)它是基于时间的触发器,说直白点那就是时间到了就触发一个事件,触发一个操作。基本上说的就是NSTimer
        (3)相关代码
    */
    - (void)timer2
    {
        //NSTimer 调用了scheduledTimer方法,那么会自动添加到当前的RunLoop里面去,而且RunLoop的运行模式kCFRunLoopDefaultMode
    
        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    
        //更改模式
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    
    }
    
    - (void)timer1
    {
        //    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    
        NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    
        //定时器添加到UITrackingRunLoopMode模式,一旦RunLoop切换模式,那么定时器就不工作
        //    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    
        //定时器添加到NSDefaultRunLoopMode模式,一旦RunLoop切换模式,那么定时器就不工作
        //    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
        //占位模式:common modes标记
        //被标记为common modes的模式 kCFRunLoopDefaultMode  UITrackingRunLoopMode
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    
        //    NSLog(@"%@",[NSRunLoop currentRunLoop]);
    }
    
    - (void)run
    {
        NSLog(@"---run---%@",[NSRunLoop currentRunLoop].currentMode);
    }
    
    - (IBAction)btnClick {
    
        NSLog(@"---btnClick---");
    }
    
    

    (2)GCD中的定时器

    //0.创建一个队列
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
        //1.创建一个GCD的定时器
        /*
         第一个参数:说明这是一个定时器
         第二个参数:描述信息,线程ID
         第三个参数:更详细的信息
         第四个参数:GCD的回调任务添加到那个队列中执行,如果是主队列则在主线程执行
         */
        dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
        //2.设置定时器的开始时间,间隔时间以及精准度
    
        //设置开始时间,三秒钟之后调用
        dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW,3.0 *NSEC_PER_SEC);
        //设置定时器工作的间隔时间
        uint64_t intevel = 1.0 * NSEC_PER_SEC;
    
        /*
         第一个参数:要给哪个定时器设置
         第二个参数:定时器的开始时间DISPATCH_TIME_NOW表示从当前开始
         第三个参数:定时器调用方法的间隔时间
         第四个参数:定时器的精准度,如果传0则表示采用最精准的方式计算,如果传大于0的数值,则表示该定时切换i可以接收该值范围内的误差,通常传0
         该参数的意义:可以适当的提高程序的性能
         注意点:GCD定时器中的时间以纳秒为单位
         */
    
        dispatch_source_set_timer(timer, start, intevel, 0 * NSEC_PER_SEC);
    
        //3.设置定时器开启后回调的方法
        /*
         第一个参数:要给哪个定时器设置
         第二个参数:回调block
         */
        dispatch_source_set_event_handler(timer, ^{
            NSLog(@"------%@",[NSThread currentThread]);
        });
    
        //4.执行定时器
        dispatch_resume(timer);
    
        //注意:dispatch_source_t本质上是OC类,在这里是个局部变量,需要强引用
        self.timer = timer;
    

    DISPATCH_SOURCE_TYPE SOURCE类型
     DISPATCH_SOURCE_TYPE_TIMER         定时响应(定时器事件)
     DISPATCH_SOURCE_TYPE_SIGNAL        接收到UNIX信号时响应
    
     DISPATCH_SOURCE_TYPE_READ          IO操作,如对文件的操作、socket操作的读响应
     DISPATCH_SOURCE_TYPE_WRITE         IO操作,如对文件的操作、socket操作的写响应
     DISPATCH_SOURCE_TYPE_VNODE         文件状态监听,文件被删除、移动、重命名
     DISPATCH_SOURCE_TYPE_PROC          进程监听,如进程的退出、创建一个或更多的子线程、进程收到UNIX信号
    
     下面两个都属于Mach相关事件响应
    DISPATCH_SOURCE_TYPE_MACH_SEND
    DISPATCH_SOURCE_TYPE_MACH_RECV
     下面两个都属于自定义的事件,并且也是由自己来触发
    DISPATCH_SOURCE_TYPE_DATA_ADD
    DISPATCH_SOURCE_TYPE_DATA_OR
    

    CFRunLoopSourceRef
    1. 是事件源也就是输入源,有两种分类模式;
    • 一种是按照苹果官方文档进行划分的
    • 另一种是基于函数的调用栈来进行划分的(source0和source1)。
    1. 具体的分类情况
      1. 以前的分法

        • Port-Based Sources
        • Custom Input Sources
        • Cocoa Perform Selector Sources
      2. 现在的分法

        • Source0:非基于Port的
        • Source1:基于Port的
    2. 可以通过打断点的方式查看一个方法的函数调用栈

    CFRunLoopObserverRef
    • CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变

    • 如何监听

        //创建一个RunLoop监听者
        /*
         第一个参数:怎么分配存储空间
         第二个参数:要监听的状态 kCFRunLoopAllActivities 所有的状态
         第三个参数:时候持续监听
         第四个参数:优先级 总是传0
         第五个参数:当状态改变时候的回调
         */
        CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(),kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    
            NSLog(@"监听RunLoop状态改变---%zd",activity);
        });
    
        //为RunLoop添加一个监听者
        /*
         第一个参数:要监听哪个RunLoop
         第二个参数:观察者
         第三个参数:运行模式
         */   
        //NSDefaultRunLoopMode == kCFRunLoopDefaultMode
        //NSRunLoopCommonModes == kCFRunLoopCommonModes
        CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    
        CFRelease(observer);
    
    • 监听的状态
    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry = (1UL << 0),   //即将进入RunLoop
        kCFRunLoopBeforeTimers = (1UL << 1),    //即将处理NSTimer
        kCFRunLoopBeforeSources = (1UL << 2),   //即将处理Sources
        kCFRunLoopBeforeWaiting = (1UL << 5),   //即将进入休眠
        kCFRunLoopAfterWaiting = (1UL << 6),    //刚从休眠中唤醒
        kCFRunLoopExit = (1UL << 7),            //即将退出RunLoop
        kCFRunLoopAllActivities = 0x0FFFFFFFU   //所有状态改变
    };
    

    七、Runloop运行逻辑

    **RunLoop的事件队列**
    每次运行RunLoop, 线程中的RunLoop会自动处理线程中的任务, 并且通知观察者, 汇报当前的状态, 顺序如下:

    1. 通知观察者RunLoop已经启动
    2. 通知观察者任何即将要开启的定时器
    3. 通知观察者任何即将要启动的非基于端口的事件源
    4. 启动任何准备好的非基于端口的事件源
    5. 如果基于端口的事件源准备好并处于等待状态, 就立即启动, 并且进入步骤9
    6. 通知观察者线程即将进入休眠
    7. 将线程置于休眠状态, 直至以下事件的发生
      • 某一事件到达基于端口的源事件
      • 定时器启动
      • RunLoop设置的事件已经超时
      • RunLoop被显式唤醒
    8. 通知观察者线程即将被唤醒
    9. 处理事件
      • 如果用户定义的定时器启动, 处理定时器事件并且重启RunLoop, 然后进入步骤2
      • 如果输入源启动, 传递响应的信息
      • 如果RunLoop被现实唤醒, 并且事件还没超时, 重启RunLoop, 进入步骤2
    10. 通知观察者RunLoop结束
    RunLoop的事件队列 网友总结图例

    八、以下内容全部转载自YY 的深入理解RunLoop | Garan no dou

    RunLoop 调用栈上函数的整理

    当 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 界面。

    这个函数内部的调用栈大概是这样的:

    _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
    
        QuartzCore:CA::Transaction::observer_callback:
    
            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];
    

    定时器

    NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

    如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

    CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop,这个稍后我会再单独写一页博客来分析。


    PerformSelecter

    当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

    当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。


    关于GCD

    实际上 RunLoop 底层也会用到 GCD 的东西,比如 RunLoop 是用 dispatch_source_t 实现的 Timer(评论中有人提醒,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.NSURLConnectionLoadercom.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。

    RunLoop_network

    NSURLConnectionLoader 中的 RunLoop 通过一些基于 mach port 的 Source 接收来自底层 CFSocket 的通知。当收到通知后,其会在合适的时机向 CFMultiplexerSource 等 Source0 发送通知,同时唤醒 Delegate 线程的 RunLoop 来让其处理这些通知。CFMultiplexerSource 会在 Delegate 线程的 RunLoop 对 Delegate 执行实际的回调。


    RunLoop 的实际应用举例

    AFNetworking

    AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop:

    +  (void)networkRequestThreadEntryPoint:(id)__unused  object  {
    
        @autoreleasepool  {
    
            [[NSThread currentThread]  setName:@"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(networkRequestThreadEntryPoint:)  object:nil];
    
            [_networkRequestThread start];
    
        });
    
        return  _networkRequestThread;
    
    }
    

    RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。

    -  (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  =  AFOperationExecutingState;
    
            [self  performSelector:@selector(operationDidStart)  onThread:[[self  class]  networkRequestThread]  withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    
        }
    
        [self.lock unlock];
    
    }
    

    当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中。


    AsyncDisplayKit

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

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

    排版通常包括计算视图大小、计算文本高度、重新计算子式图的排版等操作。
    绘制一般有文本绘制 (例如 CoreText)、图片绘制 (例如预先解压)、元素绘制 (Quartz)等操作。
    UI对象操作通常包括 UIView/CALayer 等 UI 对象的创建、设置属性和销毁。

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

    为此,ASDK 创建了一个名为 ASDisplayNode 的对象,并在内部封装了 UIView/CALayer,它具有和 UIView/CALayer 相似的属性,例如 frame、backgroundColor等。所有这些属性都可以在后台线程更改,开发者可以只通过 Node 来操作其内部的 UIView/CALayer,这样就可以将排版和绘制放入了后台线程。但是无论怎么操作,这些属性总需要在某个时刻同步到主线程的 UIView/CALayer 去。

    ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。
    具体的代码可以看这里:_ASAsyncTransactionGroup


    文章前半部分内容总结自小码哥的学习资料
    从『八』之后的所有内容原文转自YY 的深入理解RunLoop | Garan no dou

    相关文章

      网友评论

        本文标题:RunLoop

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