美文网首页
IOS基础:RunLoop(上)

IOS基础:RunLoop(上)

作者: 时光啊混蛋_97boy | 来源:发表于2020-10-23 10:17 被阅读0次

原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 一、RunLoop
    • 1、概念
    • 2、作用
    • 3、结构
    • 4、原理
    • 5、循环流程
    • 6、Runloop与线程之间的关系
    • 7、答疑
  • 二、Model
    • 1、CFRunLoopModelRef
    • 2、CFRunLoopSourceRef
    • 3、CFRunLoopTimerRef
    • 4、CFRunLoopObserverRef
  • 三、RunLoop实际运用
    • 1、NSTimer
    • 2、GCD Timer
    • 3、CADisplayLink
    • 4、AutoreleasePool
    • 5、事件响应
    • 6、手势识别
    • 7、界面更新
    • 8、AFNetworking的常驻线程
    • 9、滚动Scrollview导致定时器失效
    • 10、图片下载后延迟显示
    • 11、观察事件状态,优化性能
    • 12、线程间通信
    • 13、AsyncDisplayKit
  • 四、RunLoop实现原理
    • 1、获取RunLoop
    • 2、添加Mode
    • 3、添加Run Loop Source(ModeItem)
    • 4、添加Observer和Timer
  • 五、RunLoop运行原理
    • 1、CFRunLoopRun
    • 2、CFRunLoopRunInMode
    • 3、CFRunLoopRunSpecific
    • 4、__CFRunLoopServiceMachPort
  • Demo
  • 参考文献

一、RunLoop介绍

1、概念

Runloop 是和线程紧密相关的一个基础组件,是很多线程有关功能的幕后功臣。尽管在平常使用中几乎不太会直接用到,理解 Runloop 有利于我们更加深入地理解 iOS 的多线程模型。

一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程随时处理事件并不退出,那么,能想到的可能就是死循环。当然,它肯定不仅仅是一个死循环那么简单,它要考虑如何管理事件,如何让线程在没有消息时休眠以避免占用资源,又如何在消息到来时被唤醒。

线程在处理完自己的任务后一般会退出,为了实现线程不退出能够随时处理任务的机制被称为EventLoop,即『事件循环』机制。在iOS中,程序启动,便自动创建一个和主线程对应的runloop。它一直处于处理消息和休息等待的循环中,直到退出。没有消息需要处理时,休眠以避免资源占用(用户态-->内核态),有消息需要处理时,立刻被唤醒(内核态-->用户态)。

RunLoop的核心

2、作用

(1)让程序一直活着
(2)处理APP活着时遇到的各种事件
(3)节省CPU时间(有事件处理事件,无事件休息)

3、结构

a、RunLoop 就是个对象

在 iOS 中,RunLoop 就是个对象,在 CoreFoundation 框架为 CFRunLoopRef 对象,它提供了纯 C 函数的 API,并且这些 API 是线程安全的;而在Foundation框架中用 NSRunLoop 对象来表示,它是基于CFRunLoopRef 的封装,提供的是面向对象的 API,但这些 API不是线程安全的。

RunLoop 是个对象
// 获得当前Runloop对象
NSRunLoop * runloop1 = [NSRunLoop currentRunLoop];
CFRunLoopRef runloop2 =   CFRunLoopGetCurrent();

// 获得主线程对应的Runloop
NSRunLoop * runloop1 = [NSRunLoop mainRunLoop];
CFRunLoopRef runloop2 =   CFRunLoopGetMain();
b、RunLoop内部结构

CFRunLoop结构

struct __CFRunLoop {
    __CFPort _wakeUpPort;// 内核向该端口发送消息可以唤醒runloop
    pthread_t _pthread;// RunLoop对应的线程
    CFMutableSetRef _commonModes;// Set,存储的是字符串,记录所有标记为common的mode
    CFMutableSetRef _commonModeItems;// Set<Source/Observer/Timer>,存储所有commonMode的item(source、timer、observer)
    CFRunLoopModeRef _currentMode;// 当前运行的mode
    CFMutableSetRef _modes;// Set,存储的是CFRunLoopModeRef
    ...
};

CFRunLoopModel结构

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};
RunLoop内部结构 RunLoop内部结构

4、原理

  • CFRunLoopMode:一个 run loop mode
  • CFTimeInterval:最多能运行多久(超时时间)
  • returnAfterSourceHandled:处理一个事件后是否返回

伪代码如下:

int main(void) {
    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    
    // 添加一些 source,timer,observer 或者 block
    
    while (message != 退出) {
        message = 获取消息();
        mode = // 处理消息看是否需要改变 mode,比如 scroll view 滑动
        time = // 设置一个超时时间
        CFRunLoopRunInMode(mode,
                           time,
                           true); // 猜测大部分时间为 true,因为需要更灵活的控制
    }
    return 0;
}

main 函数中调用的UIApplicationMain函数内部就启动了一个默认添加了有kCFRunLoopDefaultModeUITrackingRunLoopMode两个预置ModeRunLoop,保持程序的持续运行,而且这个默认启动的 RunLoop的与主线程相关联的。

因为 UIApplicationMain 一直在运行,没有返回,所以如果把 main函数改为下面这样,则 UIApplicationMain函数之后的代码在程序运行阶段都是不会执行的:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        int result = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        // 只要程序在运行,不会运行到下面这句
        NSLog(@"after UIApplicationMain");
        return result;
    }
}

5、循环流程

runloop循环图简略版 runloop循环图优化版 runloop循环图完整版
a、App是如何响应触摸事件的?
  1. APP进程的mach port接收来自SpringBoard的触摸事件,主线程的runloop被唤醒,触发source1回调。
  2. source1回调又触发了一个source0回调,将接收到的事件封装成UIEvent对象,此时APP将正式开始对于触摸事件的响应。
  3. source0回调将触摸事件添加到UIApplication的事件队列,当触摸事件出队后UIApplication为触摸事件寻找最佳响应者。
  4. 寻找到最佳响应者之后,接下来的事情便是事件在响应链中传递和响应。
b、同时存在定时器和触摸事件如何响应?
  1. 应用主线程启动,系统默认创建与之对应的RunLoop,并最终保持在kCFRunLoopDefaultMode,此时,启动一个定时器10秒后开始执行,RunLoop处于Sleep状态。
  2. 5秒的时候,用户点击了界面(source1-》source0)唤醒了RunLoop
  3. 通知observer即将触发Source0回调。
  4. 执行用户点击事件(source0),并执行被加入到Runloop Block链的Block
  5. 判断这过程中是否有source1事件发生。
  6. 如果有source1事件,去执行source1,执行完后判断是否符合退出Runloop的条件,符合退出Runloop,否则继续RunLoop
  7. 如果没有,进入sleep状态。
  8. 10秒钟的时候,Timer触发,通知observersRunLoop被唤醒了。
  9. 执行timer事件,执行被加入到Runloop Block链的Block
  10. 判断是否符合退出RunLoop的条件,符合退出runloop,不符合继续runloop循环。
c、实现原理
  • 在开发过程中几乎所有的操作都是通过Call out进行回调的(无论是Observer的状态通知还是TimerSource的处理)
  • 系统在回调时通常使用如下几个函数进行回调(换句话说你的代码其实最终都是通过下面几个函数来负责调用的,即使你自己监听Observer也会先调用下面的函数然后间接通知你,所以在调用堆栈中经常看到这些函数)
  • 例如在控制器的touchBegin中打入断点查看堆栈(由于UIEventSource0,所以可以看到一个Source0Call out函数CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION调用)
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();

实际的代码块如下:

/// 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);

6、Runloop与线程之间的关系

a、pthread_t和NSThread的关系
  • 过去苹果有份文档标明了NSThread只是 pthread_t的封装,但那份文档已经失效了,现在它们也有可能都是直接包装自最底层的 mach thread
  • 苹果并没有提供这两个对象相互转换的接口,但不管怎么样,可以肯定的是 pthread_tNSThread 是一一对应的。
  • 比如,你可以通过 pthread_main_np()[NSThread mainThread]来获取主线程;也可以通过 pthread_self()[NSThread currentThread]来获取当前线程。
  • CFRunLoop 是基于pthread来管理的。
b、Runloop与线程之间的关系
  • CFRunLoopRef源码可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里,key是对应的线程,Value为该线程对应的Runloop
  • 开一个子线程创建runloop,不是通过alloc init方法创建,而是直接通过调用currentRunLoop方法来创建,它本身是一个懒加载的。子线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。
  • RunLoop的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。所以你只能在一个线程的内部获取自己和主线程的RunLoop
  • [NSRunLoop currentRunLoop];方法调用时,会先看一下字典里有没有存子线程相对应的RunLoop,如果有则直接返回RunLoop,如果没有则会创建一个,并将与之对应的子线程存入字典中。

7、答疑

1. 问:RunLoop在休息的时候是一个什么状态?

答:我们都知道了RunLoop是一个死循环,有执行任务,退出和休息三种状态,前两个不必说,休息是一种什么状态呢?解释这个问题前,我们先要知道一个 概念:mach_port

mach是一个内核,提供cpu调度,进程间通信等一些基础服务,在 Mach 中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为”对象”。和其他架构不同, Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。”消息”是 Mach 中最基础的概念,一条消息中包含当前端口 local_port 和目标端口 remote_port,消息在两个端口 (port) 之间传递,这就是 MachIPC (进程间通信) 的核心。消息的发送和接收使用<mach/message.h>中的mach_msg()函数,而mach_msg()的本质是一个调用mach_msg_trap(),这相当于一个系统调用,会触发内核状态切换。让程序处于休眠状态。而这个状态就是:

APP依然在内存中,但不在主动申请cpu资源(此处不要较真,肯定不是完全不用cpu的),然后一直监听某一个端口(port),等待内核向该端口发送消息,监听到消息后,从睡眠状态中恢复(重新启动RunLoop的循环,处理完事件后继续休眠)。

2. 问:流程图中黄色的部分(执行被加入RunloopBlock)和唤醒事件中的『被外部显示唤醒』都是什么鬼?

答:先说『被外部显示唤醒』,其实,RunLoop提供了接口,允许RunLoop运行或休息的时候,我们是可以手动向RunLoop里添加Block让它执行的。

CFRunLoopPerformBlock添加的 Blocks
那么,这个添加的Block什么时候执行呢?就看流程图中黄色的部分就好了。需要说明的是,如果加入Block要马上执行,需要手动调用 CFRunLoopWakeUp。否则就要等到Runloop下次运行时再从链表里取出你刚刚加入的Block执行。

3. 问:source0,source1,timer这些事件源到底是什么关系,不太懂?
RunLoop从两种不同类型的Sources 接受事件(注意,这里sources是事件源的意思,不要理解成source0source1),Input Sources传递异步事件,通常是来自另一线程或来自不同应用程序的消息。Timer Sources传递在特定的时间或者重复的间隔内发生的同步事件。这两种类型的源都使用特定于应用程序的处理程序来处理事件到达时的事件。


二、Model

  • kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行 。
  • UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响。
  • kCFRunLoopCommonModes :这是一个占位用的Mode,不是一种真正的Mode,而是一种模式组合,一个操作 Common标记的字符串, 你可以用这个字符串来操作 Common ItemskCFRunLoopCommonModes不是实际存在的一种Mode,只是同步Source/Timer/Observer到多个Mode中的一种技术方案。
  • UIInitializationRunLoopMode:在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
  • GSEventReceiveRunLoopMode:接受系统事件的内部 Mode,通常用不到。

1、CFRunLoopModelRef

a、Mode
  • 一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer
  • 每次调用 RunLoop 时,只能指定其中一个Mode(每次运行CFRunLoopRun()函数时必须指定ModeCFRunLoopRun()就是runloop的入口函数),这个Mode被称作 CurrentMode
  • 如果需要切换Mode,只能退出当前的循环,再重新指定一个 Mode 进入。
b、mode item
  • 上面的Source/Timer/Observer被统称为 mode item,一个 mode item 可以被同时加入多个 mode
  • 但一个 mode item 被重复加入同一个 mode 时是不会有效果的。
  • 如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。
  • timersource0source1是常见的几个RunLoop事件源,还有一个GCD也可以算是一个(就是runloop内部循环时,会去询问GCD有没有事件需要我来帮你处理,如果有,我就帮你处理一下,没有就算了)。
c、commonModes
  • 一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop“commonModes”中)。
  • 每当RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有“Common” 标记的所有Mode里。
  • 主线程的RunLoop 里有两个预置的 ModekCFRunLoopDefaultModeUITrackingRunLoopMode。这两个 Mode 都已经被标记为”Common”属性。kCFRunLoopDefaultMode 是 App 平时所处的状态,UITrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。
d、管理 Mode 和 mode item 的接口

CFRunLoop对外暴露的管理 Mode 接口只有下面2个:

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);

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

CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);

CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);

你只能通过 mode name 来操作内部的 mode,当你传入一个新的mode nameRunLoop 内部没有对应 mode 时,RunLoop会自动帮你创建对应的 CFRunLoopModeRef。对于一个 RunLoop 来说,其内部的 mode 只能增加不能删除。

e、Mode切换

viewDidLoad时是处于 UIInitializationRunLoopMode,之后程序大部分时间处于 kCFRunLoopDefaultMode,当滑动 scrollView 时会将切换到UITrackingRunLoopMode

@interface ViewController () <UITextViewDelegate>

@property (weak , nonatomic) IBOutlet UITextView *textFeild;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.textFeild.delegate = self;
    NSLog(@"viewDidLoad: %@" , [NSRunLoop currentRunLoop].currentMode);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"touch: %@" , [NSRunLoop currentRunLoop].currentMode);
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    NSLog(@"scrollViewDidScroll: %@" , [NSRunLoop currentRunLoop].currentMode);
}

@end

运行结果:

RunLoopTest[3317:206736] viewDidLoad: UIInitializationRunLoopMode
RunLoopTest[3317:206736] touch: kCFRunLoopDefaultMode
RunLoopTest[3317:206736] scrollViewDidScroll: kCFRunLoopDefaultMode
RunLoopTest[3317:206736] scrollViewDidScroll: UITrackingRunLoopMode
RunLoopTest[3317:206736] scrollViewDidScroll: UITrackingRunLoopMode

2、CFRunLoopSourceRef

Source1
  • 基于mach_Port也就是可以监听系统端口,mach_port大家就理解成进程间相互发送消息的一种机制就好
  • 来自系统内核或者其他进程或线程的事件
  • 可以主动唤醒休眠中的RunLoop(由操作系统内核进行管理,例如CFMessagePort消息)
  • iOS里进程间通信开发过程中我们一般不主动使用
typedef struct {
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
    mach_port_t (*getPort)(void *info);
    void *  (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#else
    void *  (*getPort)(void *info);
    void    (*perform)(void *info);
}
Source0
  • 非基于Port的 处理事件,就是说你这个消息不是其他进程或者内核直接发送给你的,基本就是应用层事件,包括UIEventCFSocket
  • 只包含了一个回调(函数指针),并不能主动触发事件,需要手动触发
  • 使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source标记为待处理signal状态,然后手动调用CFRunLoopWakeUp(runloop)来唤醒RunLoop,让其处理这个事件。
typedef struct {
    void    (*schedule)(void *info, CFRunLoopRef rl, CFStringRef mode);
    void    (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode);
    void    (*perform)(void *info);
} CFRunLoopSourceContext;
触摸流程
  • SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,用 mach port转发给需要的App进程
  • 随后苹果注册的那个 Source1就会触发回调,包装成UIEvent进行处理或分发,其中包括识别 UIGesture、处理屏幕旋转、发送给 UIWindow等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
  • 我们触摸屏幕,先摸到硬件(屏幕),屏幕表面的事件会先包装成EventEvent先告诉source1(mach_port)source1唤醒RunLoop, 然后将事件Event分发给source0,然后由source0来处理。如果没有事件,也没有timer,则runloop就会睡眠,如果有,则runloop就会被唤醒,然后跑一圈。

3、CFRunLoopTimerRef

CFRunLoopTimerRef是基于时间的触发器,它和 NSTimertoll-free bridged(能够在Core FoundationFoundation之间互换使用) 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

struct __CFRunLoopTimer {
    uint16_t _bits;               //标记fire状态
    CFRunLoopRef _runLoop;        //添加该timer的runloop
    CFMutableSetRef _rlModes;     //存放所有 包含该timer的 mode的 modeName,意味着一个timer可能会在多个mode中存在
    CFTimeInterval _interval;     //理想时间间隔
    CFTimeInterval _tolerance;    //时间偏差
}

4、CFRunLoopObserverRef

  • 这个严格来说并不是需要处理的事件,而是类似 notification 或者 delegate 一样的东西,runloop 会向 observers 汇报状态,被封装为 CFRunLoopObserver
  • CFRunLoopObserverRef是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。
  • 相当于消息循环中的一个监听器,随时通知外部当前RunLoop的运行状态。
struct __CFRunLoopObserver {
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFIndex _rlCount;
    CFOptionFlags _activities;
    CFIndex _order;
    CFRunLoopObserverCallBack _callout;
    CFRunLoopObserverContext _context;
}

可以观测的时间点有以下几个:

typedef CF_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
};
  • 可以使用 CFRunLoopObserverCreateWithHandler()来创建 observer,创建时设置要监听的状态变化和回调
  • 再用 CFRunLoopAddObserver()来给 RunLoop 添加 observer
  • 当该 RunLoop 状态发生在监听类型内的变化时,observer 就会执行回调
/* 创建监听者
 * 第一个参数 CFAllocatorRef allocator:分配存储空间 CFAllocatorGetDefault()默认分配
 * 第二个参数 CFOptionFlags activities:要监听的状态 kCFRunLoopAllActivities 监听所有状态
 * 第三个参数 Boolean repeats:YES:持续监听 NO:不持续
 * 第四个参数 CFIndex order:优先级,一般填0即可
 * 第五个参数 :回调 两个参数observer:监听者 activity:监听的事件
 */
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    switch (activity) {
        case kCFRunLoopEntry:
            NSLog(@"RunLoop进入");
            break;
        case kCFRunLoopBeforeTimers:
            NSLog(@"RunLoop要处理Timers了");
            break;
        case kCFRunLoopBeforeSources:
            NSLog(@"RunLoop要处理Sources了");
            break;
        case kCFRunLoopBeforeWaiting:
            NSLog(@"RunLoop要休息了");
            break;
        case kCFRunLoopAfterWaiting:
            NSLog(@"RunLoop醒来了");
            break;
        case kCFRunLoopExit:
            NSLog(@"RunLoop退出了");
            break;
            
        default:
            break;
    }
});

/* 给RunLoop添加监听者
 * 第一个参数 CFRunLoopRef rl:要监听哪个RunLoop,这里监听的是主线程的RunLoop
 * 第二个参数 CFRunLoopObserverRef observer 监听者
 * 第三个参数 CFStringRef mode 要监听RunLoop在哪种运行模式下的状态
 */
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

/* CF(Core Foundation)的内存管理
 * 凡是带有Create、Copy、Retain等字眼的函数,创建出来的对象,都需要在最后做一次release
 * GCD本来在iOS6.0之前也是需要我们释放的,6.0之后GCD已经纳入到了ARC中,所以我们不需要管了
 */
CFRelease(observer);

三、RunLoop实际运用

1、NSTimer

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

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

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

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

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

本实例会为大家演示NSTimer和滑动事件的冲突与解决方案,NSTimer不精确的问题。

- (void)viewDidLoad
{
 [super viewDidLoad];
 self.view.backgroundColor = [UIColor blueColor];
 
 //创建计时器timer1,创建完成后,会自动指定NSDefaultRunLoopMode。
 self.timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer){
 NSLog(@"timer1...");
 }];
 
 //创建计时器timer2,创建完成后,没有指定任何mode,这样timer是运行不起来的。
 NSTimer *tempTimer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer){
 NSLog(@"timer2...");
 }];
 
 //为timer指定mode
 //指定NSRunLoopCommonModes时,即使滑动,timer2也能执行,指定NSDefaultRunLoopMode,滑动时timer2就不执行了
 //为什么?在上文讲解的第一部分的时候已经给出了答案
 [[NSRunLoop currentRunLoop] addTimer:tempTimer forMode:NSRunLoopCommonModes];
 self.timer2 = tempTimer;
 
 CGRect rect = [UIScreen mainScreen].bounds;
 UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectInset(rect, 0, 200)];
 [self.view addSubview:scrollView];
 
 UIView *contentView = [[UIView alloc] initWithFrame:CGRectInset(scrollView.bounds, -100, -100)];
 contentView.backgroundColor = [UIColor redColor];
 [scrollView addSubview:contentView];
 scrollView.contentSize = contentView.frame.size;
}

运行结果:

timer2指定NSDefaultRunLoopMode模式时:滑动scrollview,什么都不输出,停止滑动,循环输出timer1,timer2
timer2指定NSRunLoopCommonModes模式时:滑动scrollview,循环输出timer2,停止滑动,循环输出timer1,timer2

本实例会演示区分直接调用和「performSelector:withObject:afterDelay:」区别。

- (void)viewDidLoad
{
     [super viewDidLoad];
     self.thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(performTask) object:nil];
     [self.thread1 start];
}

- (void)performTask
{
     __weak typeof(self) weakSelf = self;
     // 使用下面的方式创建定时器虽然会自动加入到当前线程的RunLoop中,但是除了主线程外其他线程的RunLoop默认是不会运行的,
     //必须手动调用
     self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
     if ([NSThread currentThread].isCancelled)
     {
         [weakSelf.timer invalidate];
     }
     NSLog(@"timer...");
     }];
     
     NSLog(@"runloop before performSelector:%@",[NSRunLoop currentRunLoop]);
 
     // 区分直接调用和「performSelector:withObject:afterDelay:」区别,下面的直接调用无论是否运行RunLoop一样可以执行,但是后者则不行。
     //[self caculate];
     [self performSelector:@selector(caculate) withObject:nil afterDelay:2.0];
     
     // 取消当前RunLoop中注册测selector(注意:只是当前RunLoop,所以也只能在当前RunLoop中取消)
     // [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(caculate) object:nil];
     NSLog(@"runloop after performSelector:%@",[NSRunLoop currentRunLoop]);
     
     // 非主线程RunLoop必须手动调用
     [[NSRunLoop currentRunLoop] run];
     
     NSLog(@"注意:如果RunLoop不退出(运行中),这里的代码并不会执行,因为前面一直在循环,RunLoop本身就是一个循环.");
}

- (void)caculate
{
     for (int i = 0;i < 9999;++i)
     {
         NSLog(@"%i,%@",i,[NSThread currentThread]);
         if ([NSThread currentThread].isCancelled)
         {
             return;
         }
     }
}

运行结果大致就是先输出两个timer,然后就输出caculate方法里的内容,一直等到caculate执行完,才又执行timer,这说明什么?说明一旦错过此次looptimer只有到下一次loop才能执行。

TestRunLoopTimer[17479:2906022] timer...
TestRunLoopTimer[17479:2906022] timer...
TestRunLoopTimer[17479:2906022] 0,<NSThread: 0x1c026f7c0>{number = 7, name = (null)}
TestRunLoopTimer[17479:2906022] 1,<NSThread: 0x1c026f7c0>{number = 7, name = (null)}
TestRunLoopTimer[17479:2906022] 2,<NSThread: 0x1c026f7c0>{number = 7, name = (null)}
TestRunLoopTimer[17479:2906022] 3,<NSThread: 0x1c026f7c0>{number = 7, name = (null)}
TestRunLoopTimer[17479:2906022] 4,<NSThread: 0x1c026f7c0>{number = 7, name = (null)}
TestRunLoopTimer[17479:2906022] 5,<NSThread: 0x1c026f7c0>{number = 7, name = (null)}
TestRunLoopTimer[17479:2906022] 6,<NSThread: 0x1c026f7c0>{number = 7, name = (null)}
TestRunLoopTimer[17479:2906022] 7,<NSThread: 0x1c026f7c0>{number = 7, name = (null)}
TestRunLoopTimer[17479:2906022] 8,<NSThread: 0x1c026f7c0>{number = 7, name = (null)}
  1. NSTimer会对Target进行强引用直到任务结束或exit之后才会释放。如果上面的程序没有进行线程cancel而终止任务则即使关闭控制器也无法正确释放。

  2. 非主线程的RunLoop并不会自动运行(同时注意默认情况下非主线程的RunLoop并不会自动创建,直到第一次使用),RunLoop运行必须要在加入NSTimerSource0Sourc1Observer输入后运行否则会直接退出。例如上面代码如果run放到NSTimer创建之前则既不会执行定时任务也不会执行循环运算。

  3. performSelector:withObject:afterDelay:执行的本质还是通过创建一个NSTimer然后加入到当前线程RunLoop(通而过前后两次打印RunLoop信息可以看到此方法执行之后RunLooptimer会增加1个。类似的还有performSelector:onThread:withObject:afterDelay:,只是它会在另一个线程的RunLoop中创建一个Timer),所以此方法事实上在任务执行完之前会对触发对象形成引用,任务执行完进行释放(例如上面会对ViewController形成引用,注意:performSelector: withObject:等方法则等同于直接调用,原理与此不同)。

  4. 同时上面的代码也充分说明了RunLoop是一个循环事实,run方法之后的代码不会立即执行,直到RunLoop退出。

  5. 上面程序的运行过程中如果突然dismiss,则程序的实际执行过程要分为两种情况考虑:如果循环任务caculate还没有开始则会在timer1中停止timer1运行(停止了线程中第一个任务),然后等待caculate执行并break(停止线程中第二个任务)后线程任务执行结束释放对控制器的引用;如果循环任务caculate执行过程中dismisscaculate任务执行结束,等待timer1下个周期运行(因为当前线程的RunLoop并没有退出,timer1引用计数器并不为0)时检测到线程取消状态则执行invalidate方法(第二个任务也结束了),此时线程释放对于控制器的引用。

2、GCD

在RunLoop的源代码中可以看到用到了GCD的相关内容,但是RunLoop本身和GCD并没有直接的关系,GCD 的线程管理是通过系统来直接管理的。

GCD Timer

GCD Timer 是通过 dispatch portRunLoop 发送消息,来使 RunLoop 执行相应的 block,如果所在线程没有RunLoop,那么 GCD 会临时创建一个线程去执行 block,执行完之后再销毁掉,因此 GCDTimer 是不依赖 RunLoop 的。

至于这两个 Timer 的准确性问题,如果不在 RunLoop 的线程里面执行,那么只能使用 GCD Timer,由于 GCD Timer 是基于MKTimer(mach kernel timer),已经很底层了,因此是很准确的。

如果在 RunLoop 的线程里面执行,由于 GCD TimerNSTimer 都是通过port发送消息的机制来触发 RunLoop 的,因此准确性差别应该不是很大。如果线程 RunLoop 阻塞了,不管是 GCD Timer还是 NSTimer都会存在延迟问题。

dispatch_async

当调用了dispatch_async(dispatch_get_main_queue(), ^(void)block)时libDispatch会向主线程RunLoop发送消息来唤醒RunLoopRunLoop从消息中获取block,并且在CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE回调里执行这个block。不过这个操作仅限于主线程,其他线程dispatch操作是全部由libDispatch驱动的。

3、CADisplayLink

a、简介

含义:CADisplayLink是一个能让我们以和屏幕刷新率相同的频率将内容画到屏幕上的定时器。简单地说,它就是一个定时器,每隔几毫秒刷新一次屏幕。

底层实现:CADisplayLink同样是基于CFRunloopTimerRef实现,底层使用mk_timer

工作原理:我们在应用中创建一个新的 CADisplayLink对象,把它添加到一个runloop中,并给它提供一个 targetselector 在屏幕刷新的时候调用。一旦 CADisplayLink 以特定的模式注册到runloop之后,每当屏幕需要刷新的时候,runloop就会调用CADisplayLink绑定的target上的selector,这时target可以读到 CADisplayLink 的每次调用的时间戳,用来准备下一帧显示需要的数据。例如一个视频应用使用时间戳来计算下一帧要显示的视频数据。例如在UI做动画的过程中,需要通过时间戳来计算UI对象在动画的下一帧要更新的大小。

高优先级来保证动画的平滑:在添加进runloop的时候我们应该选用高一些的优先级,来保证动画的平滑。可以设想一下,我们在动画的过程中,runloop被添加进来了一个高优先级的任务,那么,下一次的调用就会被暂停转而先去执行高优先级的任务,然后在接着执行CADisplayLink的调用,从而造成动画过程的卡顿,使动画不流畅。

b、CADisplayLink的属性
  • duration属性: 提供了每帧之间的时间,也就是屏幕每次刷新之间的的时间。该属性在targetselector被首次调用以后才会被赋值。
  • selector的调用间隔时间计算方式是:时间 = duration × frameInterval。 我们可以使用这个时间来计算出下一帧要显示的UI的数值。但是duration只是个大概的时间,如果CPU忙于其它计算,就没法保证以相同的频率执行屏幕的绘制操作,这样会跳过几次调用回调方法的机会。
  • frameInterval属性: 是可读可写的NSInteger型值,标识间隔多少帧调用一次selector 方法,默认值是1,即每帧都调用一次。如果每帧都调用一次的话,对于iOS设备来说那刷新频率就是60HZ也就是每秒60次,如果将 frameInterval 设为2 那么就会两帧调用一次,也就是变成了每秒刷新30次。iOS设备的刷新频率事60HZ也就是每秒60次。那么每一次刷新的时间就是1/60秒 大概16.7毫秒。当我们的frameInterval值为1的时候我们需要保证的是 CADisplayLink调用的target的函数计算时间不应该大于 16.7否则就会出现严重的丢帧现象。
  • pause属性:控制CADisplayLink的运行。当我们想结束一个CADisplayLink的时候,应该调用-(void)invalidaterunloop中删除,并删除之前绑定的 targetselector
  • timestamp属性:只读的CFTimeInterval值,表示屏幕显示的上一帧的时间戳,这个属性通常被target用来计算下一帧中应该显示的内容。 打印timestamp值,其样式类似于:179699.631584
c、CADisplayLink 与 NSTimer 有什么不同?

1)、原理不同
CADisplayLink是一个能让我们以和屏幕刷新率同步的频率将特定的内容画到屏幕上的定时器类。
CADisplayLink以特定模式注册到runloop后, 每当屏幕显示内容刷新结束的时候,runloop就会向 CADisplayLink指定的target发送一次指定的selector消息, CADisplayLink类对应的selector就会被调用一次。
NSTimer以指定的模式注册到runloop后,每当设定的周期时间到达后,runloop会向指定的target发送一次指定的selector消息。

2)、周期设置方式不同
iOS设备的屏幕刷新频率(FPS)60Hz,因此CADisplayLinkselector 默认调用周期是每秒60次,这个周期可以通过frameInterval属性设置, CADisplayLinkselector每秒调用次数= 60/ frameInterval。比如当 frameInterval设为2,每秒调用就变成30次。因此, CADisplayLink 周期的设置方式略显不便。
NSTimerselector调用周期可以在初始化时直接设定,相对就灵活的多。

3)、精确度不同
iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。
NSTimer的精确度就显得低了点,比如NSTimer的触发时间到的时候,runloop如果在阻塞状态,触发时间就会推迟到下一个runloop周期。并且 NSTimer新增了tolerance属性,让用户可以设置可以容忍的触发的时间的延迟范围。

4)、使用场景
CADisplayLink使用场合相对专一,适合做UI的不停重绘来构建帧动画,比如自定义动画引擎或者视频播放的渲染,看起来相对更加流畅。
NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。

d、CADisplayLink 与 NSTimer 各自使用方式

❶ CADisplayLink的创建方法

self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

❷ CADisplayLink的停止方法:当把CADisplayLink对象addrunloop中后,selector就能被周期性调用,类似于重复的NSTimer被启动了;执行invalidate操作时,CADisplayLink对象就会从runloop中移除,selector调用也随即停止,类似于NSTimerinvalidate方法。

[self.displayLink invalidate];
self.displayLink = nil;

① NSTimer的创建方法

  • TimerInterval :执行之前等待的时间。比如设置成1.0,就代表1秒后执行方法
  • target:需要执行方法的对象。
  • selector:需要执行的方法
  • repeats:是否需要循环
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(action:) userInfo:nil repeats:NO];

② NSTimer的停止方法:调用创建方法后,target对象的计数器会加1,直到执行完毕,自动减1。如果是循环执行的话,就必须手动关闭,否则可以不执行释放方法。

[timer invalidate];
timer = nil;

③ NSTimer的特性
特性1:存在延迟:不管是一次性的还是周期性的timer的实际触发事件的时间,都会与所加入的RunLoopRunLoop Mode有关,如果此RunLoop正在执行一个连续性的运算,timer就会被延时出发。重复性的timer遇到这种情况,如果延迟超过了一个周期,则会在延时结束后立刻执行,并按照之前指定的周期继续执行。

特性2:必须加入Runloop
使用以下创建方式,会自动把timer加入MainRunloopNSDefaultRunLoopMode中。

NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(action:) userInfo:nil repeats:NO];

如果使用以下方式创建定时器,就必须手动加入Runloop

NSTimer *timer = [NSTimer timerWithTimeInterval:5 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

4、AutoreleasePool

其实从RunLoop源代码分析,AutoreleasePoolRunLoop并没有直接的关系,之所以将两个话题放到一起讨论最主要的原因是因为在iOS应用启动后会注册两个Observer管理和维护AutoreleasePool

在应用程序刚刚启动时打印currentRunLoop可以看到系统默认注册了很多个Observer,其中有两个Observercallout都是** _ wrapRunLoopWithAutoreleasePoolHandler**,这两个是和自动释放池相关的两个监听。

<CFRunLoopObserver 0x6080001246a0 [0x101f81df0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}
<CFRunLoopObserver 0x608000124420 [0x101f81df0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}

第一个Observer会监听RunLoop的进入,它会回调objc_autoreleasePoolPush()向当前的AutoreleasePoolPage增加一个哨兵对象标志创建自动释放池。每一次push都会加入一个边界对象,从边界对象往后加入对象a,b,c…。这个Observerorder-2147483647优先级最高,确保发生在所有回调操作之前。

第二个Observer会监听RunLoop的进入休眠和即将退出RunLoop两种状态。在准备进入睡眠之前,因为睡眠可能时间很长,所以为了不占用资源先调用 _objc_autoreleasePoolPop()释放旧的释放池,往前清理直到遇到哨兵对象,并调用 _objc_autoreleasePoolPush()创建新建一个新的,用来装载被唤醒后要处理的事件对象。在即将退出RunLoop时则会 _objc_autoreleasePoolPop()释放自动自动释放池内对象。这个Observerorder2147483647,优先级最低,确保发生在所有回调操作之后。

主线程的操作通常均在这个AutoreleasePool之内(main函数中),以尽可能减少内存维护操作。当然如果需要显式释放【例如循环】时可以自己创建AutoreleasePool,否则一般不需要自己创建。

其实在应用程序启动后系统还注册了其他Observer和多个Source1,例如即将进入休眠时执行注册回调_UIGestureRecognizerUpdateObserver用于手势处理、回调为_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPvObserver用于界面实时绘制更新,例如contextCFMachPort的Source1用于接收硬件事件响应进而分发到应用程序一直到UIEvent

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

自动释放池的创建、释放、销毁的时机:

kCFRunLoopEntry;// 进入runloop之前,创建一个自动释放池
kCFRunLoopBeforeWaiting;// 休眠之前,销毁自动释放池,创建一个新的自动释放池
kCFRunLoopExit;// 退出runloop之前,销毁自动释放池

5、事件响应

苹果注册了一个 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 事件都是在这个回调中完成的。

6、手势识别

当上面的_UIApplicationHandleEventQueue()识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的UIGestureRecognizer 标记为待处理。

苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。

当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

7、界面更新

如果打印App启动之后的主线程RunLoop可以发现另外一个callout_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的Observer,这个监听专门负责UI变化后的更新,比如修改了frame、调整了UI层级(UIView/CALayer)或者手动设置了setNeedsDisplay/setNeedsLayout之后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。这个Observer监听了主线程RunLoop的BeforeWaiting(即将进入休眠)和Exit(即将退出Loop) ,一旦进入这两种状态回调函数里则会遍历所有待处理的UIView/CAlayer` 并提交执行实际绘制更新。

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

_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];

通常情况下这种方式是完美的,因为除了系统的更新,还可以利用setNeedsDisplay等方法手动触发下一次RunLoop运行的更新。但是如果当前正在执行大量的逻辑运算可能UI的更新就会比较卡,因此facebook推出了AsyncDisplayKit来解决这个问题。AsyncDisplayKit其实是将UI排版和绘制运算尽可能放到后台,将UI的最终更新操作放到主线程(这一步也必须在主线程完成),同时提供一套类UIViewCALayer的相关属性,尽可能保证开发者的开发习惯。这个过程中AsyncDisplayKit在主线程RunLoop中增加了一个Observer监听即将进入休眠和退出RunLoop两种状态,收到回调时遍历队列中的待处理任务一一执行。

所以如果在一次运行循环中想用如下方法设置一个 view 的两条移动路径是行不通的,因为它会把视图的属性变化汇总起来,直接让 myView从起点移动到终点了:

CGRect frame = self.myView.frame;
// 先向下移动
frame.origin.y += 200;
[UIView animateWithDuration:1 animations:^{
self.myView.frame = frame;
[self.myView setNeedsDisplay];
}];

// 再向右移动
frame.origin.x += 200;
[UIView animateWithDuration:1 animations:^{
    self.myView.frame = frame;
    [self.myView setNeedsDisplay];
}];

8、AFNetworking的常驻线程

  • CFSocket 是最底层的接口,只负责 socket 通信。
  • CFNetwork 是基于 CFSocket 等接口的上层封装,AFHttpRequest工作于这一层。
  • NSURLConnection 是基于 CFNetwork 的更高层的封装,提供面向对象的接口,AFNetworking工作于这一层。
  • NSURLSession 底层仍然用到了 NSURLConnection 的部分功能, AFNetworking2Alamofire 工作于这一层。
AFNetworking实现常驻线程的方案

当调用了[connection start] 启动NSURLConnection以后就会不断调用delegate方法接收数据,这样一个连续的的动作正是基于RunLoop来运行。一旦NSURLConnection设置了delegate会立即创建一个线程com.apple.NSURLConnectionLoader,而start 这个函数的内部会获取 CurrentRunLoop,然后在其中的NSDefaultMode模式下添加4个Source0(即需要手动触发的Source)。其中CFHTTPCookieStorage用于处理cookieCFMultiplexerSource负责各种delegate回调并在回调中唤醒delegate内部的RunLoop(通常是主线程)来执行实际操作。

当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoadercom.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader这个线程内部会使用 RunLoop 来接收底层socket的事件,并通过之前添加的Source0 通知到上层的 Delegate

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

早期版本的AFNetworking 基于 NSURLConnection 构建时正是这样做的,为了能够在后台接收delegate回调,AFNetworking内部创建了一个空的线程并启动了RunLoopRunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking[runLoop run] 之前先创建了一个新的 NSMachPort添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port)并在外部线程通过这个port 发送消息到loop内,但此处添加 port只是为了让 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;
}

当需要使用这个后台线程执行任务时,AFNetworking通过调用 [NSObject performSelector:onThread:..]将这个任务放到后台线程的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];
}
其他实现常驻线程的方案

子线程默认是完成任务后结束。当要经常使用子线程,即需要创建一个在后台一直存在的程序,来做一些需要频繁处理的任务,比如检测网络状态等,那么每次开启子线程比较耗性能,此时可以开启子线程的 RunLoop,保持 RunLoop 运行,则使子线程保持不死。因为RunLoop启动前必须设置一个mode,而 mode 要存在则至少需要一个source / timer / port。所以可以有下面几种做法:

法一:[runLoop run] 之前为 RunLoopDefaultMode添加一个 NSMachPort对象。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 RunLoop 内,但此处添加 port只是为了让RunLoop不至于退出,并没有用于实际的发送消息。这也是AFNetworking 中使用的方式。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [self.thread start];
}

- (void)run
{
    NSLog(@"开始循环,当前线程为:%@", [NSThread currentThread]);
    @autoreleasepool{
    // 如果不加这句,会发现runloop创建出来就挂了,因为runloop如果没有CFRunLoopSourceRef事件源输入或者定时器,就会立马消亡。
    // 下面的方法给runloop添加一个NSport,就是添加一个事件源,也可以添加一个定时器,或者observer,让runloop不会挂掉
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    
    // 方法1 ,2,3实现的效果相同,让runloop无限期运行下去
    [[NSRunLoop currentRunLoop] run];
   }

    
    // 方法2
    // [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    
    // 方法3
    // [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
    
    NSLog(@"结束循环");
}

- (void)test
{
    NSLog(@"测试,当前线程为:%@", [NSThread currentThread]);
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

输出结果显示执行完了run方法,这个时候再点击屏幕,可以不断执行test方法,因为线程self.thread一直常驻后台,等待事件加入其中,然后执行。

2020-08-20 10:22:03.843767+0800 Demo[35647:3446248] 开始循环,当前线程为:<NSThread: 0x600000fd0700>{number = 6, name = (null)}
2020-08-20 10:22:06.756217+0800 Demo[35647:3446248] 测试,当前线程为:<NSThread: 0x600000fd0700>{number = 6, name = (null)}
2020-08-20 10:22:06.756217+0800 Demo[35647:3446248] 测试,当前线程为:<NSThread: 0x600000fd0700>{number = 6, name = (null)}

如果没有实现添加NSPort或者NSTimer,会发现执行完run方法,线程就会消亡,后续再执行touchbegan方法无效。

法二:通过添加source来实现,在注释中解释得非常清楚了,代码如下:

@implementation ResidentThreadBySourceViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    [self threadForDispatch];
}


// 两个静态全局变量
// 自定义的子线程
static NSThread *thread = nil;
// 标记是否要继续事件循环
static BOOL runAlways = YES;

// 创建thread
- (NSThread *)threadForDispatch {
    if (thread == nil) {
        // 线程安全的方式创建thread
        @synchronized(self) {
            if (thread == nil) {
                // 线程的创建
                thread = [[NSThread alloc] initWithTarget:self selector:@selector(runRequest) object:nil];
                // 设置线程名称
                [thread setName:@"com.xiejiapei.thread"];
                // 启动该线程
                [thread start];
            }
        }
    }
    return thread;
}

- (void)runRequest
{
    NSLog(@"开始循环,当前线程为:%@", [NSThread currentThread]);
    
    // 创建一个Source
    CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};// 上下文参数
    CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
    
    // 通过CFRunLoopGetCurrent创建RunLoop,子线程默认没有RunLoop
    // 同时向RunLoop的DefaultMode下面添加Source
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    
    // 如果可以运行
    while (runAlways) {
        // 通过@autoreleasepool保证每次循环完后都释放内存
        @autoreleasepool {
            // 令当前RunLoop运行在DefaultMode下面
            // 需要注意添加资源的Mode和运行的Mode必须相同
            // 1.0e10 指循环运行到指定时间后退出,这里是指数表达式相当大,可以理解为遥远的未来
            // true表示资源被处理后立刻返回
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
        }
    }
    
    NSLog(@"结束循环");
    
    // 某一时机 静态变量 runAlways = NO时 可以保证跳出RunLoop,线程退出
    // 移除并释放source,防止内存泄露
    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    CFRelease(source);
}

- (void)test
{
    NSLog(@"测试,当前线程为:%@", [NSThread currentThread]);
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:NO];
}

@end

输出结果为:

2020-08-20 10:38:46.296481+0800 Demo[35846:3459222] 开始循环,当前线程为:<NSThread: 0x600000f96b40>{number = 7, name = com.xiejiapei.thread}
2020-08-20 10:38:51.322172+0800 Demo[35846:3459222] 测试,当前线程为:<NSThread: 0x600000f96b40>{number = 7, name = com.xiejiapei.thread}
2020-08-20 10:22:06.756217+0800 Demo[35647:3446248] 测试,当前线程为:<NSThread: 0x600000fd0700>{number = 6, name = (null)}

法三:当然我们也可以添加一个超长启动时间的timer来既保持 RunLoop 不退出也不占用资源。

@implementation ResidentThreadByTimerViewController
{
    int count;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    count = 0;
    [self run];
}

- (void)run
{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        NSLog(@"开启线程…….");
        
        NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(doTimerTask:) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
        
        //若runloop一直存在,后面的代码就不执行了
        //最后一个参数,是否处理完事件返回,结束runLoop
        SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 100, YES);
        
        /*
         kCFRunLoopRunFinished = 1, //Run Loop结束,没有Timer或者其他Input Source
         kCFRunLoopRunStopped = 2, //Run Loop被停止,使用CFRunLoopStop停止Run Loop
         kCFRunLoopRunTimedOut = 3, //Run Loop超时
         kCFRunLoopRunHandledSource = 4 //Run Loop处理完事件,注意Timer事件的触发是不会让Run Loop退出返回的,即使CFRunLoopRunInMode的第三个参数是YES也不行
         */
        switch (result)
        {
            case kCFRunLoopRunFinished:
                NSLog(@"kCFRunLoopRunFinished");
                
                break;
            case kCFRunLoopRunStopped:
                NSLog(@"kCFRunLoopRunStopped");
                
            case kCFRunLoopRunTimedOut:
                NSLog(@"kCFRunLoopRunTimedOut");
                
            case kCFRunLoopRunHandledSource:
                NSLog(@"kCFRunLoopRunHandledSource");
            default:
                break;
        }
        
        NSLog(@"结束线程……");
    });
    
}

- (void)doTimerTask:(NSTimer *)timer
{
    count++;
    if (count == 2)
    {
        //停止timer,runloop没有source,没有timer,没有observer,退出runloop,线程随之往下执行,完成后也退出
        [timer invalidate];
    }
    NSLog(@"do timer task count:%d",count);
}

@end

输出结果为:

2020-08-20 10:48:57.320004+0800 Demo[35949:3465587] 开启线程…….
2020-08-20 10:48:58.323966+0800 Demo[35949:3465587] do timer task count:1
2020-08-20 10:48:59.323202+0800 Demo[35949:3465587] do timer task count:2
2020-08-20 10:48:59.323430+0800 Demo[35949:3465587] kCFRunLoopRunFinished
2020-08-20 10:48:59.323573+0800 Demo[35949:3465587] 结束线程……

9、滚动Scrollview导致定时器失效

失效原因

在界面上有一个UIScrollview控件,如果此时还有一个定时器在执行一个事件,你会发现当你滚动Scrollview的时候,定时器会失效。因为当你滚动Scrollview的时候,RunLoop会切换到UITrackingRunLoopMode 模式,而定时器运行在defaultMode下面,系统一次只能处理一种模式的RunLoop,所以导致defaultMode下的定时器失效。

- (void)viewDidLoad {
    [super viewDidLoad];
    [self timer1];
    [self timer2];
}

//下面两种添加定时器的方法效果相同,都是在主线程中添加定时器
- (void)timer1 {
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopDefaultModes];
}

- (void)timer2 {
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
}
解决方案

timer注册到NSRunLoopCommonModes,它包含了defaultModetrackingMode两种模式。commonModeItemsRunLoop 自动更新到所有具有”Common”属性的 Mode里去。

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

或者使用GCD创建定时器,GCD创建的定时器不会受RunLoop的影响。

// 获得队列
dispatch_queue_t queue = dispatch_get_main_queue();

// 创建一个定时器(dispatch_source_t本质还是个OC对象)
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);

// 设置定时器的各种属性(几时开始任务,每隔多长时间执行一次)
// GCD的时间参数,一般是纳秒(1秒 == 10的9次方纳秒)
// 比当前时间晚1秒开始执行
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));

//每隔一秒执行一次
uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
dispatch_source_set_timer(self.timer, start, interval, 0);

// 设置回调
dispatch_source_set_event_handler(self.timer, ^{
    NSLog(@"------------%@", [NSThread currentThread]);

});

// 启动定时器
dispatch_resume(self.timer);

10、图片下载后延迟显示

由于图片渲染到屏幕需要消耗较多资源,为了提高用户体验,当用户滚动Tableview的时候,只在后台下载图片,但是不显示图片,当用户停下来的时候才显示图片。

// 一般在滑动UITableview时,cell中的图片显示的方法我们会这样写
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"imgName"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];

// 为什么要这样写?其实答案很简单,换个写法大家就明白了
[self.imgView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"123"] afterDelay:0 inModes:@[NSDefaultRunLoopMode]];

用户点击屏幕,在主线程中,三秒之后显示图片,如果此时用户又开始滚动textview,那么就算过了三秒,图片也不会显示出来,当用户停止了滚动,才会显示图片。

这是因为限定了方法setImage只能在NSDefaultRunLoopMode 模式下使用,而滚动textview的时候,程序运行在tracking模式下面,所以方法setImage不会执行,这样无论多大图片在滑动tableview的过程中都不会显示,保证了滑动tableview的流畅,直到滑动结束,mode变回NSDefaultRunLoopMode,再执行显示图片的方法。

11、观察事件状态,优化性能

假设我们想实现cell的高度缓存计算,因计算cell的预缓存高度的任务需要在最无感知的时刻进行,所以应该同时满足:
RunLoop 处于空闲状态的 Mode
❷ 当这一次 RunLoop 迭代处理完成了所有事件,马上要休眠时

CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFStringRef runLoopMode = kCFRunLoopDefaultMode;
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler
(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {
    // 在 TODO 位置,就可以开始任务的收集和分发了,当然,不能忘记适时的移除这个 observer
    // TODO here
});
CFRunLoopAddObserver(runLoop, observer, runLoopMode);

12、线程间通信

上面我们提到source1是基于端口的源,监听程序相应的端口,source0是基于事件的源,监听自定义的事件,接下来,我们就用这两个源,举一个线程间通信的例子。方便大家更好的理解source0source1

基于端口的线程间通信

source1 是基于 mach_ports的,用于通过内核和其他线程互相发送消息。iOS / OSX都是基于 Mach内核,Mach 的对象间的通信是通过消息在两个端口(port)之间传递来完成。很多时候我们的 app 都是处于什么事都不干的状态,在空闲前指定用于唤醒的mach port 端口,然后在空闲时被 mach_msg()函数阻塞着并监听唤醒端口, mach_msg()又会调用 mach_msg_trap()函数从用户态切换到内核态,这样系统内核就将这个线程挂起,一直停留在 mac_msg_trap 状态。直到另一个线程向内核发送这个端口的msg后,trap状态被唤醒,RunLoop继续开始干活。

@interface ThreadCommunicationByPortViewController ()<NSMachPortDelegate>

@end

@implementation ThreadCommunicationByPortViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    [self source1];
}

- (void)source1
{
    //声明两个端口
    NSPort *mainPort = [NSMachPort port];
    NSPort *threadPort = [NSMachPort port];
    //设置线程的端口的代理回调为自己
    threadPort.delegate = self;
    
    //给主线程runloop加一个端口
    [[NSRunLoop currentRunLoop] addPort:mainPort forMode:NSDefaultRunLoopMode];
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //给子线程添加一个Port,并让子线程中的runloop跑起来
        [[NSRunLoop currentRunLoop] addPort:threadPort forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        
    });
    
    //2秒后,从主线程向子线程发送一条消息
    NSString *str = @"hello friend";
    NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        NSMutableArray *array = [NSMutableArray arrayWithArray:@[mainPort,data]];
        
        //过2秒向threadPort发送一条消息,
        //第一个参数:发送时间。msgid 消息标识。components,发送消息附带参数。reserved:为头部预留的字节数
        [threadPort sendBeforeDate:[NSDate date] msgid:1000 components:array from:mainPort reserved:0];
        
    });
}

//这个NSMachPort收到消息的回调,注意这个参数,如果用文档里的NSPortMessage会发现无法取值,可以先给一个id
- (void)handlePortMessage:(id)message
{
    NSLog(@"收到消息了,线程为:%@",[NSThread currentThread]);
    NSArray *array = [message valueForKeyPath:@"components"];
    NSData *data =  array[1];
    NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"收到的消息是:%@",str);
}

@end

输出结果为:

2020-08-20 11:34:00.432512+0800 Demo[36138:3488761] 收到消息了,线程为:<NSThread: 0x60000099a940>{number = 7, name = (null)}
2020-08-20 11:34:00.432724+0800 Demo[36138:3488761] 收到的消息是:hello friend
非基于端口的线程间通信

source0app 内部的消息机制,使用时需要调用CFRunLoopSourceSignal()来把这个source标记为待处理,然后调用 CFRunLoopWakeUp() 来唤醒RunLoop,让其处理这个事件。

@implementation RootViewController
{
    CFRunLoopRef runLoopRef;
    CFRunLoopSourceRef source;
    CFRunLoopSourceContext source_context;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    [self source0];
}

- (void)source0
{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        NSLog(@"开始 thread.......");
        
        runLoopRef = CFRunLoopGetCurrent();
        
        //初始化_source_context。
        bzero(&source_context, sizeof(source_context));
        
        //这里创建了一个基于事件的源,绑定了一个函数
        source_context.perform = fire;
        
        //参数
        source_context.info = "hello friend";
        
        //创建一个source
        source = CFRunLoopSourceCreate(NULL, 0, &source_context);
        
        //将source添加到当前RunLoop中去
        CFRunLoopAddSource(runLoopRef, source, kCFRunLoopDefaultMode);
        
        //开启runloop 第三个参数设置为YES,执行完一次事件后返回
        CFRunLoopRunInMode(kCFRunLoopDefaultMode, 9999999, YES);
        
        NSLog(@"结束 thread.......");
    });
    
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        if (CFRunLoopIsWaiting(runLoopRef))
        {
            NSLog(@"RunLoop 正在等待事件输入");
            
            //添加输入事件
            CFRunLoopSourceSignal(source);
            
            //唤醒线程,线程唤醒后发现由事件需要处理,于是立即处理事件
            CFRunLoopWakeUp(runLoopRef);
        }
        else
        {
            NSLog(@"RunLoop 正在处理事件");
            
            //添加输入事件,当前正在处理一个事件,当前事件处理完成后,立即处理当前新输入的事件
            CFRunLoopSourceSignal(source);
        }
    });
    
}

static void fire(void* info)
{
    
    NSLog(@"我现在正在处理后台任务");
    
    NSLog(@"信息是:%s",info);
}

@end

运行结果:

2020-08-20 11:45:14.572849+0800 Demo[36241:3496165] 开始 thread.......
2020-08-20 11:45:16.573055+0800 Demo[36241:3495959] RunLoop 正在等待事件输入
2020-08-20 11:45:16.573313+0800 Demo[36241:3496165] 我现在正在处理后台任务
2020-08-20 11:45:16.573492+0800 Demo[36241:3496165] 信息是:hello friend
2020-08-20 11:45:16.573627+0800 Demo[36241:3496165] 结束 thread.......

13、AsyncDisplayKit

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

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,监听了kCFRunLoopBeforeWaitingkCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。


四、RunLoop实现原理

续文见下篇 IOS基础:RunLoop(下)


Demo

Demo在我的Github上,欢迎下载。
RunLoopDemo

参考文献

RunLoop 详解
RunLoop 原理+使用场景

相关文章

网友评论

      本文标题:IOS基础:RunLoop(上)

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