简介
存在的必要性:应用程序需要一直运行,所以需要一个以事件驱动为基础的事件循环机制。
RunLoop 的架构主题就是一个死循环,一个事件循环,没有退出就一直运行。这个循环具体的实现后文有详细的描述。
int main() {
init();
do {
var message = get_next_message();
prcess_mesage(message);
} while (message != quit);
}
调用解耦:主调方把需要执行的逻辑放入消息队列中。执行方从消息队列中拿到并执行,与主调方实现解耦。
RunLoop 系统上的应用
- NSTime 完全基于 RunLoop 去实现
- UIEvent 完全也基于 RunLoop 去执行 和 分发
- Autorelease 也基于 RunLoop,在Objective-C 高级编程那本书中有提到
- NSObj 中的延迟执行方法也是基于
- NSObj 中 NSThreadPerformAddition
- CATransition CAAnimation 也都是基于,UI 刷新基本都是基于 RunLoop
dispatch_get_main_queue()
- NSURLConnection & AFNetworking: NSURLConnection 回调就是在 RunLoop 上跑执行的
- 以及我们调试用的调用堆栈,有很多 RunLoop 的身影
- 其实基本上 OC 中所有的方法都是 RunLoop 分发出去的,这个文中后面方法名又长又臭的那里有解释
- ·····
框架结构
两个对象:
- Foundation 层的
NSRunLoop
- CoreFoundation 层的
CFRunLoopRed
CFRunLoopRef 在 CoreFoundation 框架中,提供纯 C 的函数供调用,线程安全,并且开源的。
NSRunLoop 基于 CFRunLoopRef 的对象封装,体面面向对象的 API。不做具体的事情。
创建过程
不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()
用于获得 mainRunLoop
以及 currentRunLoop
下面代码这里来自:YY 的 RunLoop 博客 -- 深入理解RunLoop
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;
CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}
/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
// 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
/// 直接从 Dictionary 里获取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
/// 取不到时,创建一个
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}
OSSpinLockUnLock(&loopsLock);
return loop;
}
通过创建的过程,分析 RunLoop 与线程的关系
创建 RunLoop 的方法表明了 RunLoop 与线程一定是一一对应的关系,尤其体现在 loopsDic
这个字典,字典中 key 是 pthread_t
, value 是 CFRunLoopRef
。
在任何线程中,只能获取当前线程的 runLoop 和主线程的 runLoop。即上文中的 mainRunLoop
和 currentRunLoop
。
内部结构
runloop_img01.pngCFRunLoop
与线程是一一对应的,一个 Thread 里面可以有很多 RunLoop。
CFRunLoopMode
:CFRunLoop 对应一个或多个 Mode,CFRunTimeMode
Mode
之下有 Timer
Sources
Observer
可以理解成: Source timer 以及 Observer 才是外部真正关心的事情,Mode 是对他们做的区分,最外通过 CFRunLoopRef 进行封装。
CFRunLoopTimer
是个基于事件的触发器,有时间和回调的地址,时间到达的时候,会执行回调地址的函数。
具体的应用有:NSTime、延迟执行performSelectorAfterDelay
、displayLink。
CFRunLoopSource
Source 是 RunLoop 的数据输入源,是一个抽象的 Protocol,符合这个 Source 的对象,才可以在 RunLoop 上面去执行,具体的看下面的 CFRunLoopSourceContext
Source 有两个版本:Source0
Source1
Source0:属于 App 内部事件,App 自己去负责管理和触发,比如:UIEvent、CFSocket。
Source0 中只有回调的指函数针,不能主动出发事件。
Source1:由 RunLoop 和 内核管理,Mach port 驱动(进程直接通讯的方式),比如:CFMachPort、CFMessagePort
可以基于 这个 Source Protool 自己实现一个 Source,基本不会去实现,不过没想到什么应用场景
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits;
pthread_mutex_t _lock;
CFIndex _order;
CFMutableBagRef _runLoops;
union {
CFRunLoopSourceContext version0;
CFRunLoopSourceContext1 version1;
} _context;
};
// union 中的 CFRunLoopSourceContect version0:
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
Boolean (*equal)(const void *info1, const void *info2);
CFHashCode (*hash)(const void *info);
void (*schedule)(void *info, CFRunLoopRef rl, CFStringRef mode);
void (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode);
void (*perform)(void *info);
} CFRunLoopSourceContext;
union 中的很多都是函数指针,需要实现体自己去实现,比如 内存管理的 retain release copy equal hash。最重要的是最后的一个 perform 方法,真正去调用的方法。
CFRunLoopObserver
RunLoop 开放给外部的 观察者,相当于 Delegate,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化:
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
};
框架中的一些机制结合了 RunLoopObserver,比如 CAAnimation,会有一些动画的延迟机制
CFRunLoopMode 比较重要
RunLoop 一定有且仅有一个 Mode
如果要切换,这个 runLoop 会 quit,重新走一个循环
iOS 滑动的时候,是 Mode: UITrackingRunLoopMode
所有的Mode:
NSDefaultRunLoopMode 默认状态、空闲状态
UITrackingRunLoopMode 滑动 ScrollView 时,iOS 流程的关键
UIInitializationRunLoopMode 私有,App 启动时是这个 Mode,成功后切换成第一个 Mode
NSRunLoopCommonModes []是一个数组,第一个 Mode 和第二个结合,都可以执行
Timer 与 Mode
滑动的时候,Time 是不会跑的,因为 RunLoop 在 UITrackingRunLoopMode 上。
解决方法就是加入到 NSRunLoopCommonModes
中去:
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0
target:self
selector:@selector(timerTick:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
注意:GCD 的 Timer 是内核单独维护的,跟 RunLoop 平级,只是落地点是在 RunLoop 上。
RunLoop 执行过程简化记录
//进入循环之前,调用GCDTimer,设置过期时间,否则就真的变成死循环了
SetupThisRunLoopRunTimeoutTimer(); // by GCD timer
do {
//进入循环 告诉 Observer 告诉要进入 timers sources
__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
__CFRunLoopDoObservers(kCFRunLoopBeforeSources);
__CFRunLoopDoBlocks();
//遍历 Source 0 执行
__CFRunLoopDoSource0();
//需要跑的代码直执行:__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
CheckIfExistMessagesInMainDispatchQueue(); // GCD
//调用 Observers BeforeWaiting
__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
//挂起方法,进入 trap,卡在这里,等待唤醒,获得唤醒的端口号
var wakeUpPort = SleepAndWaitForWakingUpPorts();
// mach_msg_trap
// Received mach_msg, wake up
__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
// Handle msgs 如果是Timer 唤醒的,就跑 Timer
if (wakeUpPort == timerPort) {
__CFRunLoopDoTimers();
// 如果是 GCD 唤醒的
} else if (wakeUpPort == mainDispatchQueuePort) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
// 基于 port 唤醒的,比如网络来数据了,就去处理数据
} else {
__CFRunLoopDoSource1();
}
__CFRunLoopDoBlocks();
// 判断是不是停止了, timeout 了没有
} while (!stop && !timeout);
RunLoop 的底层
OS X / iOS 中最底层是 Drawin 层,这个层面中包括了 mach port,在 <mach/message.h>
中有 mach 的定义:
typedef struct {
mach_msg_header_t header;
mach_msg_body_t body;
} mach_msg_base_t;
typedef struct {
mach_msg_bits_t msgh_bits;
mach_msg_size_t msgh_size;
mach_port_t msgh_remote_port;
mach_port_t msgh_local_port;
mach_port_name_t msgh_voucher_port;
mach_msg_id_t msgh_id;
} mach_msg_header_t;
发送和接受消息的 API 如下,option 标记了消息传递的方向:
mach_msg_return_t mach_msg(
mach_msg_header_t *msg,
mach_msg_option_t option,
mach_msg_size_t send_size,
mach_msg_size_t rcv_size,
mach_port_name_t rcv_name,
mach_msg_timeout_t timeout,
mach_port_name_t notify);
RunLoop 的挂起和唤醒
RunLoop 的另一个核心就在于 mach_msg(),RunLoop 调用这个方法后去等待唤醒,并获得唤醒源进行判断。
调用 mach_msg 监听唤醒端口,被唤醒前,系统内核将这个线程挂起,停留在 mach_msg_trap 状态。
由另一个线程(或另一个进程中的某个线程)向内核发送这个端口的 msg 后,trap 状态被唤醒,RunLoop 继续执行。
RunLoop 中进入 trap 等待唤醒其实就是一个 mach_msg()。例如你在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap() 这个地方。
RunLoop Callouts 调用外部方法的途径
当 RunLoop 调用 modeItems 中的外部指针的时候,都是通过一个很长的函数调的。
所以几乎所有的函数都是由这些方法调起的:
1. __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
2. __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
3. __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
4. __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
5. __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
6. __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
- 回调 Observer
- Source 0 的时候调用 block
- GCD 调用主线程,分发到 mainRunLoop 中执行
- Time 唤醒 RunLoop 的话,回调 Timer
- 触发 Source0 (非基于 port 的) 回调
- 触发 Source1 (mach_port 的) 回调
命名成这样也是为了在调用栈里面可以自解释。
与 GCD 的关系
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
如果在 GCD 中派发到主线程,那么就会分发到 mainRunLoop 中执行。
dispatch_after 同理。
RunLoopObserver 与 Autorelease Pool
在Objective-C 高级编程那本书中有提到:
RunLoop 的每次循环过程中,NSAutoreleasePool 对象被生成或废弃
准确的说,RunLoop 的下两次 Sleep/Trap 过程之间,上一次的 NSAutoreleasePool 有了时机去执行 drain()。
Apple 在 mainRunLoop 中注册两个 Observer:
当进入 Loop 的时候,调用 _objc_autoreleasePoolPush() 创建 自动释放池,这个操作的优先级是最小的 Int,确保发生在其他所有的回调之前。
第二个 Observer 发生在 Trap 休眠发成之前,会释放旧池,并创建新池。Quit 的时候也会释放旧池,这个操作优先级是最大的 Int,确保发生在其他所有的回调之后。
事件响应 与 手势的应用
事件响应上,iOS 的实现是注册一个基于 mach_port 的 Source1 去接收系统事件,回调函数为__IOHIDEventSystemClientQueueCallback()
。
IOKit 生成 IOHIDEvent,仅能由 SpringBoard 接收(iOS 6.1加入的限制),然后发送给各个需要的 App 的 mach_port,_UIApplicationHandleEventQueue() 会包装成 UIEvent,包括了 gesture、屏幕旋转、点击等,发送给 UIWindow。这里罗列了目前支持的 service 以及 keyboard events。
事件响应过程图示:
Touch发生 ----> IOKit 感知
|
|
V
生成 IOHIDEvent
|
|
V
--------------- ---------------
| |
| SpringBoard |
| |
-----------------------------------
|
| 传递到需要的 App 的 mach_port --->
| 入口:RunLoop 的 Source 1:
| _IOHIDEventSystemClientQueueCallback()
V
---- ----
| | _UIApplicationHandleEventQueue()
| App | 接收并生成 UIEvent --> UIWindow
| |
-----------
像 UIButton 的点击,touchBegin 等也都是在这个回调中完成的。
手势的实现:在 RunLoop beforeWaiting (即将休眠的时候)注册了,_UIGestureRecognizerUpdateObserver() 获得所有待处理的 GestureRecognizer,并执行回调。
关于网络请求
CFSocket
CFNetWorking -> ASIHttpRequest
NSURLConnection -> AFNetworking
NSURLSession -> AFNetworking 2, Alamofire
NSURLConnection 工作的时候,创建一个 自己的线程 C 和 CFSocket 的线程 S。
底层的 S 通过 C 的 Source 1 通知到 C,C 再通过 currentRunLoop 的 Source 0 去 通知到最上层的 Delegate。
实践 1:AFN 中对于 RunLoop 的实践 -- 常驻线程
下面是 AFN 的两端初始化的代码,第二个方法是 AFN 创建线程的方法,其中调用第一个方法,其结果是 AFN 这个单例创建持有了一个常驻的线程 + RunLoop,给 RunLoop 添加一个 mach port,一直去监听,这个 port 不会发送东西,让这个 RunLoop 一直不被销毁。
这是 AFN 线程常驻的一个很好的方法。
+ (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;
}
实践 2:TableView 加载图片的优化
滑动的时候设置图片会影响帧数,通过下面的代码,避开 trackingMode 的 RunLoop,在 default 中执行:
UIImage *downloadedImage = ...;
[self.avatarImageView performSelector:@selector(setImage:)
withObject:downloadedImage
afterDelay:0
inModes:@[NSDefaultRunLoopMode]];
参考资料:
RunLoop文章:
- YY 的 RunLoop 博客 -- 深入理解RunLoop
- CSDN 上一篇对 RunLoop 源码的注释
- Sunny 孙源的分享资料
- 从源码看RunLoop
- RunLoop系列之要点提炼
- RunLoop系列之源码分析
- 官方
其他:
网友评论