原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的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
。它一直处于处理消息和休息等待的循环中,直到退出。没有消息需要处理时,休眠以避免资源占用(用户态-->内核态),有消息需要处理时,立刻被唤醒(内核态-->用户态)。

2、作用
(1)让程序一直活着
(2)处理APP活着时遇到的各种事件
(3)节省CPU时间(有事件处理事件,无事件休息)
3、结构
a、RunLoop 就是个对象
在 iOS 中,RunLoop
就是个对象,在 CoreFoundation
框架为 CFRunLoopRef
对象,它提供了纯 C 函数的 API
,并且这些 API
是线程安全的;而在Foundation
框架中用 NSRunLoop
对象来表示,它是基于CFRunLoopRef
的封装,提供的是面向对象的 API
,但这些 API
不是线程安全的。

// 获得当前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
...
};


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
函数内部就启动了一个默认添加了有kCFRunLoopDefaultMode
和 UITrackingRunLoopMode
两个预置Mode
的RunLoop
,保持程序的持续运行,而且这个默认启动的 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、循环流程



a、App是如何响应触摸事件的?
- APP进程的
mach port
接收来自SpringBoard
的触摸事件,主线程的runloop
被唤醒,触发source1
回调。 -
source1
回调又触发了一个source0
回调,将接收到的事件封装成UIEvent
对象,此时APP将正式开始对于触摸事件的响应。 -
source0
回调将触摸事件添加到UIApplication
的事件队列,当触摸事件出队后UIApplication
为触摸事件寻找最佳响应者。 - 寻找到最佳响应者之后,接下来的事情便是事件在响应链中传递和响应。
b、同时存在定时器和触摸事件如何响应?
- 应用主线程启动,系统默认创建与之对应的
RunLoop
,并最终保持在kCFRunLoopDefaultMode
,此时,启动一个定时器10秒后开始执行,RunLoop
处于Sleep
状态。 - 5秒的时候,用户点击了界面
(source1-》source0)
唤醒了RunLoop
。 - 通知
observer
即将触发Source0
回调。 - 执行用户点击事件
(source0)
,并执行被加入到Runloop Block
链的Block
。 - 判断这过程中是否有
source1
事件发生。 - 如果有
source1
事件,去执行source1
,执行完后判断是否符合退出Runloop
的条件,符合退出Runloop
,否则继续RunLoop
。 - 如果没有,进入
sleep
状态。 - 10秒钟的时候,
Timer
触发,通知observers
,RunLoop
被唤醒了。 - 执行
timer
事件,执行被加入到Runloop Block
链的Block
。 - 判断是否符合退出
RunLoop
的条件,符合退出runloop
,不符合继续runloop
循环。
c、实现原理
- 在开发过程中几乎所有的操作都是通过
Call out
进行回调的(无论是Observer
的状态通知还是Timer
、Source
的处理) - 系统在回调时通常使用如下几个函数进行回调(换句话说你的代码其实最终都是通过下面几个函数来负责调用的,即使你自己监听
Observer
也会先调用下面的函数然后间接通知你,所以在调用堆栈中经常看到这些函数) - 例如在控制器的
touchBegin
中打入断点查看堆栈(由于UIEvent
是Source0
,所以可以看到一个Source0
的Call 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_t
和NSThread
是一一对应的。 - 比如,你可以通过
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
) 之间传递,这就是 Mach
的 IPC
(进程间通信) 的核心。消息的发送和接收使用<mach/message.h>
中的mach_msg()
函数,而mach_msg()
的本质是一个调用mach_msg_trap()
,这相当于一个系统调用,会触发内核状态切换。让程序处于休眠状态。而这个状态就是:
APP依然在内存中,但不在主动申请cpu
资源(此处不要较真,肯定不是完全不用cpu
的),然后一直监听某一个端口(port
),等待内核向该端口发送消息,监听到消息后,从睡眠状态中恢复(重新启动RunLoop
的循环,处理完事件后继续休眠)。
2. 问:流程图中黄色的部分(执行被加入Runloop
的Block
)和唤醒事件中的『被外部显示唤醒』都是什么鬼?
答:先说『被外部显示唤醒』,其实,RunLoop
提供了接口,允许RunLoop
运行或休息的时候,我们是可以手动向RunLoop
里添加Block
让它执行的。
CFRunLoopPerformBlock
添加的 Blocks
。
那么,这个添加的Block
什么时候执行呢?就看流程图中黄色的部分就好了。需要说明的是,如果加入Block
要马上执行,需要手动调用 CFRunLoopWakeUp
。否则就要等到Runloop
下次运行时再从链表里取出你刚刚加入的Block
执行。
3. 问:source0,source1,timer这些事件源到底是什么关系,不太懂?
RunLoop
从两种不同类型的Sources
接受事件(注意,这里sources
是事件源的意思,不要理解成source0
或source1
),Input Sources
传递异步事件,通常是来自另一线程或来自不同应用程序的消息。Timer Sources
传递在特定的时间或者重复的间隔内发生的同步事件。这两种类型的源都使用特定于应用程序的处理程序来处理事件到达时的事件。
二、Model
-
kCFRunLoopDefaultMode:App的默认
Mode
,通常主线程是在这个Mode
下运行 。 -
UITrackingRunLoopMode:界面跟踪
Mode
,用于ScrollView
追踪触摸滑动,保证界面滑动时不受其他Mode
影响。 -
kCFRunLoopCommonModes :这是一个占位用的
Mode
,不是一种真正的Mode
,而是一种模式组合,一个操作Common
标记的字符串, 你可以用这个字符串来操作Common Items
。kCFRunLoopCommonModes
不是实际存在的一种Mode
,只是同步Source
/Timer
/Observer
到多个Mode
中的一种技术方案。 -
UIInitializationRunLoopMode:在刚启动 App 时第进入的第一个
Mode
,启动完成后就不再使用。 -
GSEventReceiveRunLoopMode:接受系统事件的内部
Mode
,通常用不到。
1、CFRunLoopModelRef
a、Mode
- 一个
RunLoop
包含若干个Mode
,每个Mode
又包含若干个Source/Timer/Observer
。 - 每次调用
RunLoop
时,只能指定其中一个Mode
(每次运行CFRunLoopRun()
函数时必须指定Mode
,CFRunLoopRun()
就是runloop
的入口函数),这个Mode
被称作CurrentMode
。 - 如果需要切换
Mode
,只能退出当前的循环,再重新指定一个Mode
进入。
b、mode item
- 上面的
Source/Timer/Observer
被统称为mode item
,一个mode item
可以被同时加入多个mode
。 - 但一个
mode item
被重复加入同一个mode
时是不会有效果的。 - 如果一个
mode
中一个item
都没有,则RunLoop
会直接退出,不进入循环。 -
timer
,source0
,source1
是常见的几个RunLoop
事件源,还有一个GCD
也可以算是一个(就是runloop
内部循环时,会去询问GCD
有没有事件需要我来帮你处理,如果有,我就帮你处理一下,没有就算了)。
c、commonModes
- 一个
Mode
可以将自己标记为”Common”
属性(通过将其ModeName
添加到RunLoop
的“commonModes”
中)。 - 每当
RunLoop
的内容发生变化时,RunLoop
都会自动将_commonModeItems
里的Source/Observer/Timer
同步到具有“Common”
标记的所有Mode
里。 - 主线程的
RunLoop
里有两个预置的Mode
:kCFRunLoopDefaultMode
和UITrackingRunLoopMode
。这两个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 name
但 RunLoop
内部没有对应 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
的 处理事件,就是说你这个消息不是其他进程或者内核直接发送给你的,基本就是应用层事件,包括UIEvent
、CFSocket
- 只包含了一个回调(函数指针),并不能主动触发事件,需要手动触发
- 使用时,你需要先调用
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
事件都是在这个回调中完成的。 - 我们触摸屏幕,先摸到硬件(屏幕),屏幕表面的事件会先包装成
Event
,Event
先告诉source1(mach_port)
,source1
唤醒RunLoop
, 然后将事件Event
分发给source0
,然后由source0
来处理。如果没有事件,也没有timer
,则runloop
就会睡眠,如果有,则runloop
就会被唤醒,然后跑一圈。
3、CFRunLoopTimerRef
CFRunLoopTimerRef
是基于时间的触发器,它和 NSTimer
是toll-free bridged
(能够在Core Foundation
和Foundation
之间互换使用) 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 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
为了节省资源,并不会在非常准确的时间点回调这个Timer
。Timer
有个属性叫做 Tolerance
(宽容度),标示了当时间点到后,容许有多少最大误差。由于NSTimer
的这种机制,因此NSTimer
的执行必须依赖于RunLoop
,如果没有 RunLoop
,NSTimer
是不会执行的。
如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10
时我忙着玩手机错过了那个点的公交,那我只能等 10:20
这一趟了。
CADisplayLink
是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer
并不一样,其内部实际是操作了一个 Source
)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer
相似),造成界面卡顿的感觉。在快速滑动TableView
时,即使一帧的卡顿也会让用户有所察觉。Facebook
开源的 AsyncDisplayLink
就是为了解决界面卡顿的问题,其内部也用到了 RunLoop
。
当调用 NSObject
的 performSelecter: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
,这说明什么?说明一旦错过此次loop
,timer
只有到下一次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)}
-
NSTimer
会对Target
进行强引用直到任务结束或exit
之后才会释放。如果上面的程序没有进行线程cancel
而终止任务则即使关闭控制器也无法正确释放。 -
非主线程的
RunLoop
并不会自动运行(同时注意默认情况下非主线程的RunLoop
并不会自动创建,直到第一次使用),RunLoop
运行必须要在加入NSTimer
或Source0
、Sourc1
、Observer
输入后运行否则会直接退出。例如上面代码如果run
放到NSTimer
创建之前则既不会执行定时任务也不会执行循环运算。 -
performSelector:withObject:afterDelay:
执行的本质还是通过创建一个NSTimer
然后加入到当前线程RunLoop
(通而过前后两次打印RunLoop
信息可以看到此方法执行之后RunLoop
的timer
会增加1个。类似的还有performSelector:onThread:withObject:afterDelay:
,只是它会在另一个线程的RunLoop
中创建一个Timer
),所以此方法事实上在任务执行完之前会对触发对象形成引用,任务执行完进行释放(例如上面会对ViewController
形成引用,注意:performSelector: withObject:
等方法则等同于直接调用,原理与此不同)。 -
同时上面的代码也充分说明了
RunLoop
是一个循环事实,run
方法之后的代码不会立即执行,直到RunLoop
退出。 -
上面程序的运行过程中如果突然
dismiss
,则程序的实际执行过程要分为两种情况考虑:如果循环任务caculate
还没有开始则会在timer1
中停止timer1
运行(停止了线程中第一个任务),然后等待caculate
执行并break
(停止线程中第二个任务)后线程任务执行结束释放对控制器的引用;如果循环任务caculate
执行过程中dismiss
则caculate
任务执行结束,等待timer1
下个周期运行(因为当前线程的RunLoop
并没有退出,timer1
引用计数器并不为0)时检测到线程取消状态则执行invalidate
方法(第二个任务也结束了),此时线程释放对于控制器的引用。
2、GCD
在RunLoop的源代码中可以看到用到了GCD的相关内容,但是RunLoop本身和GCD并没有直接的关系,GCD
的线程管理是通过系统来直接管理的。
GCD Timer
GCD Timer
是通过 dispatch port
给 RunLoop
发送消息,来使 RunLoop
执行相应的 block
,如果所在线程没有RunLoop
,那么 GCD
会临时创建一个线程去执行 block
,执行完之后再销毁掉,因此 GCD
的 Timer
是不依赖 RunLoop
的。
至于这两个 Timer
的准确性问题,如果不在 RunLoop
的线程里面执行,那么只能使用 GCD Timer
,由于 GCD Timer
是基于MKTimer(mach kernel timer)
,已经很底层了,因此是很准确的。
如果在 RunLoop
的线程里面执行,由于 GCD Timer
和 NSTimer
都是通过port
发送消息的机制来触发 RunLoop
的,因此准确性差别应该不是很大。如果线程 RunLoop
阻塞了,不管是 GCD Timer
还是 NSTimer
都会存在延迟问题。
dispatch_async
当调用了dispatch_async
(dispatch_get_main_queue()
, ^(void)block
)时libDispatch
会向主线程RunLoop
发送消息来唤醒RunLoop
,RunLoop
从消息中获取block
,并且在CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
回调里执行这个block
。不过这个操作仅限于主线程,其他线程dispatch
操作是全部由libDispatch
驱动的。
3、CADisplayLink
a、简介
含义:CADisplayLink
是一个能让我们以和屏幕刷新率相同的频率将内容画到屏幕上的定时器。简单地说,它就是一个定时器,每隔几毫秒刷新一次屏幕。
底层实现:CADisplayLink
同样是基于CFRunloopTimerRef
实现,底层使用mk_timer
。
工作原理:我们在应用中创建一个新的 CADisplayLink
对象,把它添加到一个runloop
中,并给它提供一个 target
和 selector
在屏幕刷新的时候调用。一旦 CADisplayLink
以特定的模式注册到runloop
之后,每当屏幕需要刷新的时候,runloop
就会调用CADisplayLink
绑定的target
上的selector
,这时target
可以读到 CADisplayLink
的每次调用的时间戳,用来准备下一帧显示需要的数据。例如一个视频应用使用时间戳来计算下一帧要显示的视频数据。例如在UI做动画的过程中,需要通过时间戳来计算UI对象在动画的下一帧要更新的大小。
高优先级来保证动画的平滑:在添加进runloop
的时候我们应该选用高一些的优先级,来保证动画的平滑。可以设想一下,我们在动画的过程中,runloop
被添加进来了一个高优先级的任务,那么,下一次的调用就会被暂停转而先去执行高优先级的任务,然后在接着执行CADisplayLink
的调用,从而造成动画过程的卡顿,使动画不流畅。
b、CADisplayLink的属性
-
duration属性: 提供了每帧之间的时间,也就是屏幕每次刷新之间的的时间。该属性在
target
的selector
被首次调用以后才会被赋值。 -
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)invalidate
从runloop
中删除,并删除之前绑定的target
跟selector
。 -
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
,因此CADisplayLink
的selector
默认调用周期是每秒60次,这个周期可以通过frameInterval
属性设置, CADisplayLink
的selector
每秒调用次数= 60/ frameInterval
。比如当 frameInterval
设为2,每秒调用就变成30次。因此, CADisplayLink
周期的设置方式略显不便。
NSTimer
的selector
调用周期可以在初始化时直接设定,相对就灵活的多。
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
对象add
到runloop
中后,selector
就能被周期性调用,类似于重复的NSTimer
被启动了;执行invalidate
操作时,CADisplayLink
对象就会从runloop
中移除,selector
调用也随即停止,类似于NSTimer
的invalidate
方法。
[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
的实际触发事件的时间,都会与所加入的RunLoop
和RunLoop Mode
有关,如果此RunLoop
正在执行一个连续性的运算,timer
就会被延时出发。重复性的timer
遇到这种情况,如果延迟超过了一个周期,则会在延时结束后立刻执行,并按照之前指定的周期继续执行。
特性2:必须加入Runloop
使用以下创建方式,会自动把timer
加入MainRunloop
的NSDefaultRunLoopMode
中。
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
源代码分析,AutoreleasePool
与RunLoop
并没有直接的关系,之所以将两个话题放到一起讨论最主要的原因是因为在iOS应用启动后会注册两个Observer
管理和维护AutoreleasePool
。
在应用程序刚刚启动时打印currentRunLoop
可以看到系统默认注册了很多个Observer
,其中有两个Observer
的callout
都是** _ 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…
。这个Observer
的order
是-2147483647
优先级最高,确保发生在所有回调操作之前。
第二个Observer
会监听RunLoop
的进入休眠和即将退出RunLoop
两种状态。在准备进入睡眠之前,因为睡眠可能时间很长,所以为了不占用资源先调用 _objc_autoreleasePoolPop()
释放旧的释放池,往前清理直到遇到哨兵对象,并调用 _objc_autoreleasePoolPush()
创建新建一个新的,用来装载被唤醒后要处理的事件对象。在即将退出RunLoop
时则会 _objc_autoreleasePoolPop()
释放自动自动释放池内对象。这个Observer
的order
是2147483647
,优先级最低,确保发生在所有回调操作之后。
主线程的操作通常均在这个AutoreleasePool
之内(main
函数中),以尽可能减少内存维护操作。当然如果需要显式释放【例如循环】时可以自己创建AutoreleasePool
,否则一般不需要自己创建。
其实在应用程序启动后系统还注册了其他Observer
和多个Source1
,例如即将进入休眠时执行注册回调_UIGestureRecognizerUpdateObserver
用于手势处理、回调为_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv
的Observer
用于界面实时绘制更新,例如context
为CFMachPort的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的最终更新操作放到主线程(这一步也必须在主线程完成),同时提供一套类UIView
或CALayer
的相关属性,尽可能保证开发者的开发习惯。这个过程中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
的部分功能,AFNetworking2
和Alamofire
工作于这一层。
AFNetworking实现常驻线程的方案
当调用了[connection start]
启动NSURLConnection
以后就会不断调用delegate
方法接收数据,这样一个连续的的动作正是基于RunLoop
来运行。一旦NSURLConnection
设置了delegate
会立即创建一个线程com.apple.NSURLConnectionLoader
,而start
这个函数的内部会获取 CurrentRunLoop
,然后在其中的NSDefaultMode
模式下添加4个Source0
(即需要手动触发的Source
)。其中CFHTTPCookieStorage
用于处理cookie
,CFMultiplexerSource
负责各种delegate
回调并在回调中唤醒delegate
内部的RunLoop
(通常是主线程)来执行实际操作。
当开始网络传输时,我们可以看到 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
执行实际的回调。
早期版本的AFNetworking
基于 NSURLConnection
构建时正是这样做的,为了能够在后台接收delegate
回调,AFNetworking
内部创建了一个空的线程并启动了RunLoop
。RunLoop
启动前内部必须要有至少一个 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]
之前为 RunLoop
的DefaultMode
添加一个 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
,它包含了defaultMode
和trackingMode
两种模式。commonModeItems
被RunLoop
自动更新到所有具有”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
是基于事件的源,监听自定义的事件,接下来,我们就用这两个源,举一个线程间通信的例子。方便大家更好的理解source0
和source1
。
基于端口的线程间通信
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
非基于端口的线程间通信
source0
是app
内部的消息机制,使用时需要调用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
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
事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。
四、RunLoop实现原理
续文见下篇 IOS基础:RunLoop(下)
Demo
Demo在我的Github上,欢迎下载。
RunLoopDemo
网友评论