废话不多说, 来啊, 互相伤害啊!!
0. RunLoop资料
- 苹果官方文档:
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html - CFRunLoopRef是开源的
http://opensource.apple.com/source/CF/CF-1151.16/
1. RunLoop对象
iOS中有两套API来访问和使用RunLoop
- Foundation框架(OC) --> NSRunLoop
- Core Foundation框架(C) -->CFRunLoopRef
NSRunLoop和CFRunLoopRef都代表着RunLoop对象
NSRunLoop是基于CFRunLoopRef的一层OC包装, 所以要了解RunLoop内部结构, 需要多研究CFRunLoopRef层面的API(Core Foundation层面)
1. main函数中的RunLoop
- main函数中的RunLoop: UIApplicationMain函数内部就启动了一个RunLoop, 所以UIApplicationMain函数一直没有返回,保持了程序的持续运行, 这个默认启动的RunLoop是跟主线程相关联的
- 由于main函数里面启动了个RunLoop,所以程序并不会马上退出,保持持续运行状态
2. RunLoop与线程
- 每条线程都有唯一的一个与之对应的RunLoop对象(如果我也想开一个子线成,并且让线程不死,则子线程开一个RunLoop)
- 主线程的RunLoop已经自动创建好了,子线程的RunLoop需要主动创建
- RunLoop在第一次获取时创建,在线程结束时销毁
3. 获得RunLoop对象
- Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
- Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
如果在主线程中: 当前线程的RunLoop对象和主线程的RunLoop对象取得的是相同的, 代码演示:
- (void)viewDidLoad {
[super viewDidLoad];
// 主线程中打印的mainRunLoop, currentRunLoop内存地址是相同的
NSLog(@"%p---%p", [NSRunLoop mainRunLoop], [NSRunLoop currentRunLoop]);
// 创建一个子线程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[thread start];
}
- (void)run
{
// 开启的子线程默认是没有runloop的
// 手动创建runloop对象, [NSRunLoop currentRunLoop]创建, 懒加载的创建方法,第一次访问创建,以后就不会创建了
NSRunLoop *currentLoop = [NSRunLoop currentRunLoop];
NSLog(@"thred--%p", currentLoop); //打印的内存地址和主线程的不同
}
我们打开开源的CFRunLoop.c代码, 里面的有部分核心代码为下面:
(下面的函数传一个线程进来,返回一个CFRunLoopRef对象, 说明一个线程对应一个runloop)
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
上面的代码表示: 当调用[NSRunLoop currentRunLoop]/CFRunLoopGetCurrent()时:
它会先创建一个可变字典
CFMutableDictionaryRef
, 然后创建一个主线程的CFRunLoopRef
, 并将主线程pthread_main_thread_np()
作为key, 主线程的CFRunLoopRef对象
作为value, 放入字典中(也就意味着,在访问其他线程的时候, 系统会先把主线程的runloop创建好, 所以主线程不用我们创建runloop)然后将当前开辟的线程传进入, 看看当前线程的线程的runloop在不在字典中, 如果不存在, 则创建当前开辟线程的
CFRunLoopRef
, 然后用新线程作为key, 用新线程的CFRunLoopRef对象
作为value, 放入字典中一个key对应一个value, 所以每条线程都有唯一的一个与之对应的RunLoop对象
4. RunLoop相关的类
Core Foundation中关于RunLoop的5个类:
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
由上面的5个类的关系图, 可以看出: runloop
要想跑起来, 里面必须有Mode
, Mode
中必须有Source
或Timer
(RunLoop包含Mode,Mode包含其他的三个类,其他的这三个类分别用Set,Arrya,Arrya装着)
这里注意: 根据源码,runloop要跑起来先判断mode是否为空,如果为空退出,
然后判断source0是否为空,如果为空退出,然后判断source1是否为空,如果为空退出,然后判断是否有timer,如果没有就退出,并没有判断是否有observer,所以runloop如果要跑起来,必须有source或者timer的其中一个
4.1.1 CFRunLoopModeRef
- CFRunLoopModeRef代表RunLoop的运行模式
- 一个
RunLoop
包含若干个Mode
,每个Mode
又包含若干个Source
/Timer
/Observer
- 每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode(可以获取到
[NSRunLoop currentRunLoop].currentMode
) - 如果需要切换Mode,只能退出Loop再重新指定一个Mode进入(因为RunLoop是一个运行循环, 一直在跑圈, 换另一个模式,必须先退出, 然后按照另一个模式跑圈)
- 这样做主要是为了分隔开不同组的
Source
/Timer
/Observer
,让其互不影响(切换模式是为了,让它按照另一个模式的Source,Timer,Observer来跑圈, 互不影响)
RunLoop 启动必须要传入一个模式,RunLoop有多个模式, 但是每次只能运行一种模式
//创建runloop对象
NSRunLoop *currentLoop = [NSRunLoop currentRunLoop];
//启动(必须添加启动模式)
[currentLoop runMode:<#(nonnull NSString *)#> beforeDate:<#(nonnull NSDate *)#>];
4.1.2 CFRunLoopModeRef的5个模式:
系统默认注册了5个Mode:
- kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
- UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响 (当我们滑动ScrollView,TableView等继承于ScrollView的控件是, 系统会切换模式为: UITrackingRunLoopMode, 跟踪你的触摸事件, 当停止滚动的时候, 系统会切换模式为: kCFRunLoopDefaultMode)
这个有什么用呢? 答案是这样的:
当ScrollView滚动时候, runloop对象只会处理
UITrackingRunLoopMode
这个模式下的定时器,Source, 以前添加的事件是不会处理的(以前添加的按钮等的事件是在kCFRunLoopDefaultMode
模式下的),所以这个模式专门用来处理滚动的, 使滚动更加流畅,提高性能...举个栗子: 如果有TableView上有轮播图(NSTimer), 则在滚动TableView的时候,定时器是不好使的, 因为添加定时器默认是在kCFRunLoopDefaultMode
下的
- UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用(这个是苹果系统在使用,我们一般用不到)
那么可以得出: 打开APP的时候, RunLoop先进入
UIInitializationRunLoopMode
模式,然后切换到kCFRunLoopDefaultMode
, 如果有滚动,则切换到UITrackingRunLoopMode
模式)
- GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到 (GS 绘图渲染等)
- kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode (RunLoop要启动需要传递一个模式才可以启动, 这个模式不能用来启动)
其实RunLoop真正的模式为四个, 最后一个不算真正的模式, 而我们可以使用的基本上就是上面的两个模式, 最后一个kCFRunLoopCommonModes算是一个标签, 打上这个模式的标签就是通用模式, 都可以跑, 下面讲解定时器案例时候说明, 默认的定时器添加到
kCFRunLoopDefaultMode
模式下,然后我们打上kCFRunLoopCommonModes
通用标签, 就解决定时器在TableView滚动时停止的问题了
4.2 CFRunLoopTimerRef
- CFRunLoopTimerRef是基于时间的触发器(基本上说的就是NSTimer), 一个模式下可以有多个Timer(Arrar中存放)
4.2.1 CFRunLoopTimerRef -->NSTimer
代码演练:
/**
* 这个方法内部实现是: 创建timer,添加到RunLoop中的默认的Mode中,RunLoop启动这个mode,取出这个mode中timer来用
*/
[NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(run) userInfo:nil repeats:YES];
/**
* 上面的代码等同于下面的
*/
// 创建Timer
NSTimer *timer = [NSTimer timerWithTimeInterval:0.5 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 定时器只运行在 NSDefaultRunLoopMode 模式下, 一旦RunLoop进入其他模式,这个定时器就不会工作
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 如果拖动时, 我们将定时器添打上这个NSRunLoopCommonModes的标记
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
NSLog(@"-----------%@", [NSRunLoop currentRunLoop]);
/**
* 定时器会跑在标记为common modes的模式下(这个模式只是个标记)
* RunLoop会寻找带有common标签的模式,有这个标签的,都可以跑
* 打印当前的RunLoop信息输出为:(有common modes标签的有两个,UITrackingRunLoopMode和kCFRunLoopDefaultMode),所以定时器可以在这两个模式下跑, RunLoop只会运行一种模式
common modes = {type = mutable set, count = 2,
entries =>
0 : {contents = "UITrackingRunLoopMode"}
2 : {contents = "kCFRunLoopDefaultMode"}
}
*/
我们可以用这个来做什么呢:
比如: 有些事情,我拖拽的时候,你不能做, 我手松开才可以做, 那么我们可以除了监听ScrollView的滚动来判断外, 我们也可以将想做的事情放到默认的模式kCFRunLoopDefaultMode
中 ,滚动时,事情不做,停止滚动,才做事情
如果我们将定时器放到UITrackingRunLoopMode
模式下, 则只有在拖动的时候,定时器才可以工作, 代码如下:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
// 调用了scheduledTimer返回的NSTimer的定时器对象,已经被自动添加到当前的runLoop中(一个线程对应一个runloop,如果在子线程中添加定时器..添加到子线程的runloop中),默认为NSDefaultRunLoopMode模式
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 如果需要更改模式, 直接这样就可以
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
4.2.2 CFRunLoopTimerRef -->CADisplayLink
CADisplayLink如果NSTimer一样, 也是添加到模式中
CADisplayLink的方法:
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;
- (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;
- (void)invalidate;
5 CFRunLoopSourceRef
CFRunLoopSourceRef是事件源(输入源): 一些触摸事件,点击事件等都是在source
中, 由source
来触发的, 所以有一些事件过来,就是有一些source
过来了, 处理source
就是处理事件
- 以前的分法:(官方文档,source的分类)(理论)
- Port-Based Sources: 基于端口的,和其他线程进行交互的或者说是一些内核过来的消息
- Custom Input Sources:自定义输入源
- Cocoa Perform Selector Sources:
[self performSelector:@selector(xxx)];
- 现在的分法:(按照源码,函数的调用栈来看,source的分类)(实践)
- Source0:非基于Port的 处理事件
- Source1:基于Port的, 通过内核和其他线程通信,接收,分发系统事件
我们添加一个按钮,看一下函数的调用栈:
source1是用来接收事件的, 虽然调用栈里面没有source1, 我们触摸屏幕,先摸到硬件(屏幕),屏幕表面的事件会先包装成Event, Event先告诉source1, 然后source1将事件Event分发给source0,然后由source0来处理
所以点击屏幕,触摸到硬件也会唤醒runloop..
performSelector和按钮的点击事件等等事件都是由source来处理的,这些事件一旦发生了,这个事件就会来到runloop,runloop里面就会把source0,source1处理一下,如果没有事件,也没有timer,则runloop就会睡眠, 如果有,则runloop就会被唤醒,然后跑一圈
6 CFRunLoopObserverRef
CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变
可以监听的时间点有以下几个:
说明:
1UL <<0 , 2的0次方 1
2UL <<5, 2的5次方 32
代码演练
// 这个方法是KVO,并不是观察runloop的状态,下面的方法才是监听状态
// [NSRunLoop currentRunLoop] addObserver:<#(nonnull NSObject *)#> forKeyPath:<#(nonnull NSString *)#> options:<#(NSKeyValueObservingOptions)#> context:<#(nullable void *)#>
/**
* 创建runloop观察着
*
* @param allocator#> 默认的allocator 点进去选择默认的即可
* @param activities#> 监听的状态(即将进入runloop,即将处理timer,即将处理source,即将进入休眠,刚从休眠中唤醒,即将退出runloop,监听所有状态)
* @param repeats#> 是否重复监听 YES 重复监听 description#>
* @param order#> 传0即可, description#>
* @param observer runloop观察着
* @param activity 监听到的状态(枚举值)
*
* @return runloop观察着
*/
CFRunLoopObserverRef observe = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"监听到runloop状态发生改变---%zd", activity);
// 这个方法也挺有用的,比如我们想在处理timer之前做一些事情,处理事件之前做一些事情
});
/**
* 添加观察着,监听runloop的状态
*
* @param rl#> runloop对象 description#>
* @param observer#> runloop观察着 description#>
* @param mode#> runloop的模式 description#>
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observe, kCFRunLoopDefaultMode);
/**
* CF CoreFounction框架的东西不受ARC控制,需要自己手动释放
* 释放observe
*/
CFRelease(observe);
运行程序,打印结果如下: (对照上面的图查看runloop的状态)
2016-06-18 23:36:19.421 RunLoop[61612:760980] 监听到runloop状态发生改变---1
2016-06-18 23:36:19.422 RunLoop[61612:760980] 监听到runloop状态发生改变---2
2016-06-18 23:36:19.422 RunLoop[61612:760980] 监听到runloop状态发生改变---4
2016-06-18 23:36:19.423 RunLoop[61612:760980] 监听到runloop状态发生改变---2
2016-06-18 23:36:19.424 RunLoop[61612:760980] 监听到runloop状态发生改变---4
2016-06-18 23:36:19.424 RunLoop[61612:760980] 监听到runloop状态发生改变---2
2016-06-18 23:36:19.424 RunLoop[61612:760980] 监听到runloop状态发生改变---4
2016-06-18 23:36:19.424 RunLoop[61612:760980] 监听到runloop状态发生改变---32
2016-06-18 23:36:19.425 RunLoop[61612:760980] 监听到runloop状态发生改变---64
........
这时候那么问题来了, runloop的处理逻辑是什么呢?
看下图:
测试点击按钮事件:打印情况如下
2016-06-19 00:04:54.843 RunLoop[63122:782041] 监听到runloop状态发生改变---64 被唤醒
2016-06-19 00:04:54.843 RunLoop[63122:782041] 监听到runloop状态发生改变---2 即将处理timer
2016-06-19 00:04:54.844 RunLoop[63122:782041] 监听到runloop状态发生改变---4 即将处理source
2016-06-19 00:04:54.845 RunLoop[63122:782041] 监听到runloop状态发生改变---2
2016-06-19 00:04:54.846 RunLoop[63122:782041] 监听到runloop状态发生改变---4
2016-06-19 00:04:54.847 RunLoop[63122:782041] 监听到runloop状态发生改变---2
2016-06-19 00:04:54.847 RunLoop[63122:782041] 监听到runloop状态发生改变---4
2016-06-19 00:04:54.847 RunLoop[63122:782041] 监听到runloop状态发生改变---32
2016-06-19 00:04:54.921 RunLoop[63122:782041] 监听到runloop状态发生改变---64
2016-06-19 00:04:54.922 RunLoop[63122:782041] 监听到runloop状态发生改变---2 即将处理timer
2016-06-19 00:04:54.922 RunLoop[63122:782041] 监听到runloop状态发生改变---4 即将处理source
2016-06-19 00:04:54.923 RunLoop[63122:782041] ---ButtonClick--- 按钮点击
2016-06-19 00:04:54.923 RunLoop[63122:782041] 监听到runloop状态发生改变---2
2016-06-19 00:04:54.924 RunLoop[63122:782041] 监听到runloop状态发生改变---4
2016-06-19 00:04:54.924 RunLoop[63122:782041] 监听到runloop状态发生改变---32
....后面还有, 最后变为即将进入休眠(32)
这里说明一点: runloop启动,先判断有没有模式,如果模式为空,则退出runloop,如果有模式,然后看看模式中有没有timer,source,observe,如果都没有,则也退出runloop..
这个可以根据runloop的源代码查看, 查看run的调用顺序得出...
7 runloop应用
- NSTimer
- ImageView显示
- PerformSelector
- 常驻线程
- 自动释放池