Runloop

Runloop大纲

1.Runloop

Run loops are part of the fundamental infrastructure associated with threads.A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events.The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.

Runloop是处理事件的循环,与线程紧密相关。启动后所在线程相当于一直处在do-while循环中,从而避免没事做被系统自动回收;此时要销毁这个线程必须停止这个 Runloop。

1.作用

  1. 保持程序的持续运行;
  2. 处理App中的各种事件(如触摸事件、定时器事件、Selector事件);
  3. 使线程有任务时执行任务,无任务时休眠,以节省CPU资源,提高程序性能;

2.与线程的关系

每个线程都有相关的 Runloop 对象,Cocoa与CF框架都提供了接口帮助管理线程的Runloop,无须我们显式的创建这些对象。

  • 主线程

在程序启动的过程中,系统会自动在主线程上设置并启动了一个 Runloop。具体入口是在 main 文件中的如下代码中:

1
2
3
4
5
6
7
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

函数UIApplicationMain()内部为主线程启动了一个runloop。应用能在无任何操作时休眠,监听到输入事件时立马响应,就是因为有这个runloop在一直监听和响应这些事件。

  • 其他线程

除主线程外,其他线程的runloop默认都是没有开启的,只在第一次获取时才会自动创建,并在线程结束时销毁。因此,这些线程在执行任务时是一条直线类型,从起点到终点,执行完任务后线程就会销毁掉。如果想让子线程保活并继续执行任务,则可自行配置并启动其runloop。

#示例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//自定义ASDFThread
@interface ASDFThread : NSThread

@end

@implementation ASDFThread

- (void)dealloc{
NSLog(@"++++THREAD IS DEALLOCED~");
}
@end


//AppDelegate 在自定义线程中执行任务
#import "ASDFThread.h"
@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
ASDFThread *th = [[ASDFThread alloc] initWithTarget:self selector:@selector(onHandleEvent) object:nil];
[th start];
return YES;
}

- (void)onHandleEvent{
NSLog(@"++++当前线程:%@",[NSThread currentThread]);
}

输出日志:

1
2
++++当前线程:<ASDFThread: 0x600003575e80>{number = 3, name = (null)}
++++THREAD IS DEALLOCED~

可以看到,自定义的 ASDFThread 线程对象在执行完任务之后,自动销毁了。

如果要线程执行完任务后,仍不销毁以便复用,则可以在当前线程中开启 Runloop:

#示例2:

1
2
3
4
5
6
7
8
- (void)onHandleEvent{
NSLog(@"++++当前线程:%@",[NSThread currentThread]);
//开启runloop
CFRunLoopSourceContext context = {0};
CFRunLoopSourceRef source = CFRunLoopSourceCreate(NULL, 0, &context);
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
CFRunLoopRun();
}

往子线程所在RunLoop中加入一个source并启动Runloop,线程执行完任务后不再自动销毁。

2.RunLoopMode

1
2
3
4
5
6
7
8
9
10
11
struct __CFRunLoopMode {
//Mode的名字
CFStringRef _name;
//事件源
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
//观察者
CFMutableArrayRef _observers;
//计时器
CFMutableArrayRef _timers;
};

RunLoopMode本质上是个结构体,其中包含了与之对应的事件类型观察者计时器等。

Runloop对象可以和若干个mode关联起来。但同一时间 Runloop 只能运行在一种模式下;

运行过程中,只有指定模式下的输入源才会被监听以及收到当前Runloop进度的通知。

需要切换Mode时,要先退出当前RunLoop,再选择一个Mode进入;切换Mode不会导致程序退出。

1.具体模式

1
2
3
NSDefaultRunLoopMode

KCFRunLoopDefaultMode

应用默认的运行模式;

1
UITrackingRunLoopMode

界面跟踪模式,用以在拖动界面时限制其他事件的进入,滑动时主线程的RunLoop会从Default模式切换到Tracking模式;

1
2
3
NSRunLoopCommonModes

KCFRunLoopCommonModes

这是个可配置的模式集合而非一个真正的模式,默认包括了DefaultEventTracking模式。将输入源与该模式关联,实质是将输入源与该组中的每个模式进行了关联。

2.场景:定时器失效

通过scheduled方式创建的Timer默认使用DefaultMode,而拖拽滚动 Scrollview 时 Runloop 处于TrackingMode。由于 Runloop 一次只能运行在一种 Mode 下,所以滚动过程中主线程Runloop无法处理注册在其DefaultMode下的定时器事件,因此定时器也就不会触发。

解决方案:

方案1: CommonMode

将定时器标记为common模式。

思路:定时器默认被加入到当前线程Runloop的DefaultMode中,被标记为common后会自动与common模式中的Tracking模式进行关联。而滑动时Runloop处在Tracking模式下,所以计时器事件能正常响应。

1
2
3
4
5
6
NSTimer *aTimer = [NSTimer scheduledTimerWithTimeInterval:1
target:self
selector:@selector(repeat:)
userInfo:nil
repeats:true];
[[NSRunLoop currentRunLoop] addTimer:aTimer forMode:NSRunLoopCommonModes];

方案2: 子线程

将定时器放入子线程中并开启此线程的RunLoop:

思路:滑动操作发生在主线程,定时器所在的线程是子线程,所以两者互不影响。

1
2
3
4
5
6
7
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[NSTimer scheduledTimerWithTimeInterval:1
target:self
selector:@selector(aSelector:)
userInfo:nil repeats:true];
[[NSRunLoop currentRunLoop] run];
});

需要注意的是,子线程中使用定时器,一定要主动开启子线程的RunLoop,否则定时器不会触发。

方案3: GCD timer

使用GCD提供的定时器API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#import "GCDTimerTest.h"

@interface GCDTimerTest ()
@property (nonatomic, strong) dispatch_queue_t seriaqueue;
@property (nonatomic, strong) dispatch_source_t timer;
@end

@implementation GCDTimerTest

- (void)gcd_timer{
_seriaqueue = dispatch_queue_create("xx", DISPATCH_QUEUE_SERIAL);
__weak typeof(self) wself = self;
dispatch_async(_seriaqueue, ^{
__strong typeof(wself) sself = wself;
NSLog(@"+++runloop before timer:%@",[NSRunLoop currentRunLoop]);
sself.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, sself.seriaqueue);
dispatch_source_set_timer(sself.timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(sself.timer, ^{
NSLog(@"++++执行计时任务");
//dispatch_source_cancel(sself.timer);
});
dispatch_resume(sself.timer);
NSLog(@"+++runloop after timer:%@",[NSRunLoop currentRunLoop]);
});
}
@end

GCD定时器不是RunLoop的源,不受Mode切换的影响,可参考#3.2定时源-特殊定时器章节的解析。

3.事件来源

Runloop中有了事件源才会有事做,才不会退出循环。事件源分两大类:输入源定时源

image

1.输入源

输入源事件分为source0source1两种:

  • source0:需要手动唤醒runloop,如触摸、滑动事件、performSelector:onThread:;
  • source1:具备唤醒runloop的能力,如基于Port的线程间通信、系统事件捕捉(mach_msg);

1.自定义的输入源

Source0类型,它并不能主动触发事件。使用时要先调用CFRunLoopSourceSignal(),将其标记为待处理。然后看当前Runloop是否在休眠中,如果是则手动调用CFRunLoopWakeUp()来唤醒RunLoop,让其处理这个事件。

#示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
static void sourcePerformor(void* info)
{
NSLog(@"处理自定义输入源事件");
}

- (void)customInputsource
{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"开启线程.......");
_mRunLoopRef = CFRunLoopGetCurrent();

//创建CFRunLoopSourceContext对象
CFRunLoopSourceContext mContext;
bzero(&mContext, sizeof(mContext));

//给context对象绑定一个函数
mContext.perform = sourcePerformor;
mContext.info = "information";

//创建CFRunLoopSourceRef对象
_mSourceRef = CFRunLoopSourceCreate(NULL, 0, &mContext);

//将source添加到当前RunLoop中
CFRunLoopAddSource(_mRunLoopRef, _mSourceRef, kCFRunLoopDefaultMode);

//开启Runloop
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10000, YES);
NSLog(@"线程结束.......");
});

//2秒后执行
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{

if (CFRunLoopIsWaiting(_mRunLoopRef)) {
NSLog(@"RunLoop正在等待事件输入+++");
//添加输入事件
CFRunLoopSourceSignal(_mSourceRef);
//唤醒线程,线程唤醒后发现由事件需要处理,于是立即处理事件
CFRunLoopWakeUp(_mRunLoopRef); // 在主线程中唤醒其他子线程的runloop。
}else {
NSLog(@"RunLoop正在处理事件+++");
//添加输入事件,当前正在处理一个事件,当前事件处理完成后,立即处理当前新输入的事件
CFRunLoopSourceSignal(_mSourceRef);
}
});
}

执行后,输出日志:

1
2
3
4
00:32:21.497879+0800  开启线程.......
00:32:24.663180+0800 RunLoop正在等待事件输入+++
00:32:24.663498+0800 处理自定义输入源事件
00:32:24.663881+0800 线程结束.......

2.Perform Selector

Source0类型,是 Cocoa 提供的一种自定义的源,允许你在任何线程上执行一个selector。下面列举了NSObject分类中定义的三种performSelector方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@interface NSObject (NSThreadPerformAdditions)

- (void)performSelectorOnMainThread:(SEL)aSelector
withObject:(id)arg
waitUntilDone:(BOOL)wait
modes:(NSArray<NSString *> *)array;

- (void)performSelectorOnMainThread:(SEL)aSelector
withObject:(id)arg
waitUntilDone:(BOOL)wait;
// equivalent to the first method with kCFRunLoopCommonModes

- (void)performSelector:(SEL)aSelector
onThread:(NSThread *)thr
withObject:(id)arg
waitUntilDone:(BOOL)wait
modes:(NSArray<NSString *> *)array;

- (void)performSelector:(SEL)aSelector
onThread:(NSThread *)thr
withObject:(id)arg
waitUntilDone:(BOOL)wait;
// equivalent to the first method with kCFRunLoopCommonModes

- (void)performSelectorInBackground:(SEL)aSelector
withObject:(id)arg;

@end

参数说明:

  • OnMainThread 是在主线程上执行任务;
  • onThread 是在你指定的线程中执行任务,可以是主线程也可以是子线程;
  • InBackground 则是在由系统自动分配的一个子线程中执行任务;
  • withObject 是 selector 方法的参数;
  • waitUntilDone 表示 performSelector 所处的线程是否等 selector 中的任务执行完再继续执行下一行;

#示例:

  • 自定义的子线程工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@interface ASDFThreadHelper : NSObject

-(NSThread *)getThread;
- (void)finish;
-(instancetype)initWithName:(NSString*)name;
@end

@interface ASDFThreadHelper()
@property (nonatomic, strong) NSThread *thread;
@end

@implementation ASDFThreadHelper

-(instancetype)initWithName:(NSString*)name{
if (self = [super init]) {
_thread = [[NSThread alloc] initWithTarget:self selector:@selector(onThreadInit:) object:nil];
_thread.name = name;
[_thread start];
}
return self;
}

-(NSThread *)getThread{
return _thread;
}

- (void)onThreadInit:(id)obj{
//因为是子线程,所以需要启动其runloop,不然子线程启动后就立刻退出,performselector时会崩溃
CFRunLoopRun();
}

- (void)start{
[_thread start];
}

- (void)finish{
CFRunLoopStop(CFRunLoopGetCurrent());
}
@end
  • 调用performSelector
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@interface AppDelegate()
@property (nonatomic, strong) ASDFThreadHelper *threadHelper;
@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSLog(@"+++before:%ld",(long)CFGetRetainCount((__bridge CFTypeRef)(self)));
_threadHelper = [[ASDFThreadHelper alloc] initWithName:@"asdf"];

[self performSelector:@selector(onSelector:)
onThread:[_threadHelper getThread]
withObject:nil waitUntilDone:YES];

NSLog(@"+++after:%ld",(long)CFGetRetainCount((__bridge CFTypeRef)(self)));

return YES;
}

-(void)onSelector:(id)obj{
NSLog(@"+++excute:%ld,thread:%@,",
(long)CFGetRetainCount((__bridge CFTypeRef)(self)),[NSThread currentThread]);
[_threadHelper finish];
}
  • 输出日志
1
2
3
+++before:1
+++excute:2,thread:<NSThread: 0x600001850c40>{number = 3, name = asdf},
+++after:1

从日志来看:

  • selector 会在我们指定的子线程中执行任务;
  • 如果 waitUntilDone=YES,执行到 [self perform..onThread..] 时,主线程会等待 selector 执行完之后才继续执行下一行;
  • 给 selector 指定的线程一定要有效,不能是已经退出的,所以我们需要在工具类中将子线程中的 runloop 启动起来;

3.基于端口的输入源

Source1类型,能主动唤醒线程的RunLoop。

内核中进程间的通信通过在两个端口之间传递消息来实现,Source1 监听的正是这些端口。你也可以手动创建端口,以实现不同线程间的通信。

#示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
- (void)runloopPortTest
{
//创建端口
NSPort *PORT1 = [NSMachPort new];
NSPort *PORT2 = [NSMachPort port];

NSLog(@"\nPORT1:%@ \nPORT2:%@",PORT1, PORT2);

//设置端口的代理
PORT1.delegate = self;
PORT2.delegate = self;

//给主线程runloop加一个端口
[[NSRunLoop currentRunLoop] addPort:PORT1 forMode:NSDefaultRunLoopMode];

//给子线程添加端口并启动其runloop
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[[NSRunLoop currentRunLoop] addPort:PORT2 forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
});

//component参数数组中只能包含两种类型的数据:一种是NSPort的子类,一种是NSData的子类;
NSString *STR = @"III";
NSData *data = [STR dataUsingEncoding:NSUTF8StringEncoding];
NSMutableArray *array = [NSMutableArray arrayWithArray:@[PORT1,data]];

//2秒后向PORT2发送消息
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
[PORT2 sendBeforeDate:[NSDate date] msgid:101 components:array from:PORT1 reserved:0];
});
}

#pragma mark -NSPortDelegate
- (void)handlePortMessage:(NSMessagePort*)message{
//1. 消息id
NSUInteger msgId = [[message valueForKeyPath:@"msgid"] integerValue];
//2. 当前主线程的port
NSPort *localPort = [message valueForKeyPath:@"localPort"];
//3. 接收到消息的port(来自其他线程)
NSPort *remotePort = [message valueForKeyPath:@"remotePort"];

NSLog(@"\n执行端口代理回调:\n端口ID = %lu \nlocalPort:%@
\nremotePort:%@",(unsigned long)msgId, localPort, remotePort);

if (101 == msgId){
//向子线的port发送消息
[remotePort sendBeforeDate:[NSDate date] msgid:102 components:nil from:localPort reserved:0];
} else if (102 == msgId){
//....
}
}

输出日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
19:30:24.335
PORT1:<NSMachPort: 0x600000549d70>
PORT2:<NSMachPort: 0x600000340370>
19:30:26.337
执行端口代理回调:
端口ID = 101
localPort:<NSMachPort: 0x600000340370>
remotePort:<NSMachPort: 0x600000549d70>
19:30:26.337
执行端口代理回调:
端口ID = 102
localPort:<NSMachPort: 0x600000549d70>
remotePort:<NSMachPort: 0x600000340370>

示例中两个线程间互相发送了一条消息。

1
2
3
4
5
- (BOOL)sendBeforeDate:(NSDate *)limitDate
msgid:(NSUInteger)msgID
components:(NSMutableArray *)components
from:(NSPort *)receivePort
reserved:(NSUInteger)headerSpaceReserved;

上面方法中,参数components数组中只能传NSPort、NSData类型的数据,所以除了NSPort对象外,其他数据需要先转成NSData类型。

1
2
3
4
5
6
7
8
@protocol NSPortDelegate <NSObject>
@optional

- (void)handlePortMessage:(NSPortMessage *)message;
// This is the delegate method that subclasses should send
// to their delegates, unless the subclass has something
// more specific that it wants to try to send first
@end

消息回调之后,通过上面的代理接收,其中的 NSMessagePort 类型只能用 KVC 的方式取值。

2.定时源

定时器的作用是:在预设的时间点或重复的时间间隔,触发某个操作。

1.CFRunLoopTimer

这是Core Foundation框架中的使用的定时器,使用方式如下面的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void onScheduleTimer(){
NSLog(@"+++++");
}
- (void)runloop_timer{
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopTimerContext context = {0, NULL, NULL, NULL, NULL};

CFRunLoopTimerRef aTimerRef = CFRunLoopTimerCreate(
kCFAllocatorDefault,
1, //fireDate
1, //interval
0, //flags
0,
&onScheduleTimer, //CFRunLoopTimerCallBack 回调函数
&context);
CFRunLoopAddTimer(runLoop, aTimerRef, kCFRunLoopCommonModes);
}

2.NSTimer

这是Cocoa框架中经常使用都的的定时器,其创建方式有以下几种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti 
target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval
repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti
invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

- (instancetype)initWithFireDate:(NSDate *)date
interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

- (instancetype)initWithFireDate:(NSDate *)date
interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep;

这五种方式创建的NSTimer需通过”[[NSRunLoop currentRunLoop] addTimer:forMode:]”手动加入到 Runloop 中并指定模式才能正常开始。

1
2
3
4
5
6
7
8
9
10
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti
target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
repeats:(BOOL)repeats
block:(void (^)(NSTimer *timer))block;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti
invocation:(NSInvocation *)invocation
repeats:(BOOL)yesOrNo;

以上三种方式会自动创建一个NSTimer对象并以DefaultMode加入到 Runloop 中。

NSTimer从本质上来说,就是一个CFRunLoopTimerRef

#示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)viewDidLoad {
_seriaqueue = dispatch_queue_create("disafsdfa", DISPATCH_QUEUE_SERIAL);
[self nstimer];
}
- (void)nstimer{
dispatch_async(_seriaqueue, ^{
//开始定时器之前
NSLog(@"+++runloop before timer:%@",[NSRunLoop currentRunLoop]);
[NSTimer scheduledTimerWithTimeInterval:2 repeats:NO block:^(NSTimer * _Nonnull timer) {
NSLog(@"++++执行定时任务");
CFRunLoopStop(CFRunLoopGetCurrent());
}];
// 开始定时器之后
NSLog(@"+++runloop after timer:%@",[NSRunLoop currentRunLoop]);
CFRunLoopRun();
// 定时任务完成
NSLog(@"+++runloop exit:%@",[NSRunLoop currentRunLoop]);
});
}

日志(有删减,保留了主要内容):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//开始定时器之前
+++runloop before timer:<CFRunLoop 0x600002f88500>{
modes = <CFBasicHash 0x600001ddc7b0 [0x10388eb48]>{type = mutable set, count = 1,
entries =>
2 : <CFRunLoopMode 0x600002888f70>{name = kCFRunLoopDefaultMode
sources0 = (null),
sources1 = (null),
observers = (null),
timers = (null) // 注意这里此时为空
}
// 开始定时器之后
+++runloop after timer:<CFRunLoop 0x600002f88500>{
modes = <CFBasicHash 0x600001ddc7b0 [0x10388eb48]>{type = mutable set, count = 1,
entries =>
2 : <CFRunLoopMode 0x600002888f70>{name = kCFRunLoopDefaultMode,
sources0 = (null),
sources1 = (null),
observers = (null),
timers = <CFArray 0x600003784ae0>{ //此时数组中有了CFRunLoopTimer对象
type = mutable-small,
count = 1,
values = (
0 : <CFRunLoopTimer 0x60000268f000>{valid = Yes, firing = No, interval = 0, tolerance = 0,
next fire date = 594361769 (1.99719393 @ 11234440222906),
callout = (NSTimer) [_NSTimerBlockTarget fire:]
)}
++++执行定时任务

// 定时任务完成
+++runloop exit:<CFRunLoop 0x600002f88500>{
modes = <CFBasicHash 0x600001ddc7b0>{type = mutable set, count = 1,
entries =>
2 : <CFRunLoopMode 0x600002888f70>{name = kCFRunLoopDefaultMode,
sources0 = (null),
sources1 = (null),
observers = (null),
// 此时任务已完成,runloop退出,数组中values又为空了
timers = <CFArray 0x600003784ae0 [0x10388eb48]>{type = mutable-small, count = 0, values = ()}
}

只看日志中“timers =”部分,定时器开始后,其所在的runloop中多出了一个CFRunLoopTimer对象。就是说NSTimer的底层是由CFRunLoopTimer来实现的。

1.内存泄露

在页面中启动了NSTimer,离开界面前如果NSTimer没执行完,则界面无法释放,即使把这个NSTimer对象置为nil,或者使用weakSelf。这是因为NSTimer对象会强引用它的Target。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@interface DKTimerController ()
@property (nonatomic, strong) NSTimer *aTimer;
@end

- (void)viewDidLoad{
// 1.使用self 会泄露
//_aTimer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(handleTimer) userInfo:nil repeats:YES];

// 2.使用weakself 会泄露
__weak typeof(self) weakSelf = self;
_aTimer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(handleTimer) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_aTimer forMode:NSRunLoopCommonModes];
}

- (void)handleTimer{
NSLog(@"++++go timer");
}

-(void)dealloc{
[_aTimer invalidate]; // 不会释放,放在这里无效
NSLog(@"++++dealloced");
}
1.invalidate

方法1:在适当的时机将NSTimer置为invalidate

1
2
3
- (void)viewWillDisappear:(BOOL)animated{
[_aTimer invalidate]; //置为invalidate
}
2.block timer

方法2:使用 weakself+block timer

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)viewDidLoad{
// 1.使用self
__weak typeof(self) weakself = self;
_aTimer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
__strong typeof(weakself)strogself = weakself;
[strogself handleTimer];
}];
}

- (void)dealloc{
[_aTimer invalidate]; //会执行dealloc
NSLog(@"++++dealloced");
}
3.NSProxy

方法3:使用NSProxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface DKTimerProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end

@implementation DKTimerProxy

+ (instancetype)proxyWithTarget:(id)target{
DKTimerProxy *proxy = [DKTimerProxy alloc];
proxy.target = target;
return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation{
[invocation invokeWithTarget:self.target];
}
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#import "DKTimerProxy.h"

@interface DKTimerController ()
@property (nonatomic, strong) NSTimer *aTimer;
@end

@implementation DKTimerController

- (void)viewDidLoad{
DKTimerProxy *proxy = [DKTimerProxy proxyWithTarget:self];
_aTimer = [NSTimer timerWithTimeInterval:1 target:proxy selector:@selector(handleTimer) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_aTimer forMode:NSRunLoopCommonModes];
}

- (void)handleTimer{
NSLog(@"++++go timer");
}

-(void)dealloc{
[_aTimer invalidate];
NSLog(@"++++dealloced");
}

@end

VC强引用了timer,而timer将proxy作为Target从而强引用proxy;VC中创建局部变量proxy,它弱引用了VC;这样就打破了强引用循环。proxy中调用timer的selector时查无对应的实现,会走消息转发流程,将方法的调用转发给VC。

2.实时性

定时器可以产生基于时间的通知,但它并不是一种real-time的机制。

  1. 如果子线程的 Runloop 根本没有运行,那么定时器也不会触发;
  2. 定时器也和 Runloop 的 mode 相关。如果定时器所在的模式当前未被 Runloop 监视,那么定时器将不会触发;
  3. 重复类型的定时器,添加到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。比如t,t + 5,t + 10。。如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。

#示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#import "Timer-Runloop.h"

@implementation Timer_Runloop

- (void)scheduleTimer
{
//创建timer 间隔1秒
[NSTimer scheduledTimerWithTimeInterval:1
target:self
selector:@selector(onScheduleTimer)
userInfo:nil repeats:YES];
//在第3秒的时 模拟一个复杂运算
[self performSelector:@selector(onMassTasks) withObject:nil afterDelay:3];
}

- (void)onScheduleTimer
{
NSLog(@"++++1秒重复定时执行++++");
}

- (void)onMassTasks
{
NSLog(@"+++开始处理复杂运算+++");
for (int i = 0; i< 0xffffffff; i++){
}
NSLog(@"++++++复杂运算完成++++++");
}
@end

调用之后输入日志:

1
2
3
4
5
6
7
8
9
20:30:10.952360 ++++1秒重复定时执行++++
20:30:11.952263 ++++1秒重复定时执行++++
20:30:12.952284 ++++1秒重复定时执行++++
20:30:12.952628 +++开始处理复杂运算+++
20:30:24.104517 ++++++复杂运算完成++++++
20:30:24.104825 ++++1秒重复定时执行++++
20:30:24.952362 ++++1秒重复定时执行++++
20:30:25.951624 ++++1秒重复定时执行++++
20:30:26.951637 ++++1秒重复定时执行++++

从日志可以看到:当线程空闲的时候定时器的消息触发还是比较准确的,但是在30分12秒开始线程一直忙着做大量运算,直到30分24秒该运算才结束,这时候定时器回调才触发。这个线程繁忙的过程超过了一个周期,但是定时器并没有连着触发两次消息,而是只触发了一次。也就是说繁忙期间的几次回调都跳过了,繁忙过后立刻执行了一次回调,之后又正常1秒执行一次回调。

3.特殊的定时器

1.delay

NSTimer是最常见的定时器,除此之外NSObject的分类中还定义了一些特殊的定时器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@interface NSObject (NSDelayedPerforming)

//延时
- (void)performSelector:(SEL)aSelector
withObject:(id)anArgument
afterDelay:(NSTimeInterval)delay
inModes:(NSArray<NSRunLoopMode> *)modes;

- (void)performSelector:(SEL)aSelector
withObject:(id)anArgument
afterDelay:(NSTimeInterval)delay;

//取消延时
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget
selector:(SEL)aSelector
object:(nullable id)anArgument;

+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;

@end

#示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-(void)onSelector:(id)obj{
// 延时任务结束
NSLog(@"+++thread:%@,++runloop:%@",[NSThread currentThread],[NSRunLoop currentRunLoop]);
}
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 延时任务开始前
NSLog(@"+++runloop:%@",[NSRunLoop currentRunLoop]);
[self performSelector:@selector(onSelector:) withObject:nil afterDelay:1];
// 添加延时任务后
NSLog(@"++++check timers:%@",[NSRunLoop currentRunLoop]);
CFRunLoopRun();//启动子线程的runloop
});
return YES;
}

输出日志(有删减,保留了主要内容):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 延时任务开始前
+++runloop:<CFRunLoop 0x60000222c700>{
。。。
modes = <CFBasicHash 0x600001062dc0>{type = mutable set, count = 1,
entries =>
2 : <CFRunLoopMode 0x60000252d040>{name = kCFRunLoopDefaultMode,
port set = 0x6107,
queue = 0x600003026380,
source = 0x600003026b80 (not fired), timer port = 0x9d03,
sources0 = (null),
sources1 = (null),
observers = (null),
timers = (null),// 注意这一行,此时timers数组是空的!!!!
}

// 添加延时任务后
++++check timers:<CFRunLoop 0x60000222c700]>{
。。。
modes = <CFBasicHash 0x600001062dc0>{type = mutable set, count = 1,
entries =>
2 : <CFRunLoopMode 0x60000252d040>{name = kCFRunLoopDefaultMode,
port set = 0x6107,
queue = 0x600003026380,
source = 0x600003026b80 (not fired), timer port = 0x9d03,
sources0 = (null),
sources1 = (null),
observers = (null),
// 注意,这里timers数组中values是有值的,是一个CFRunLoopTimer对象!!!
timers = <CFArray 0x600003a2f000 [0x10eb3db48]>{
type = mutable-small, count = 1, values = (
0 : <CFRunLoopTimer 0x600002b2e340]>{valid = Yes,
firing = No, interval = 0, tolerance = 0, next fire date = 594358054
)}

// 延时任务结束
+++thread:<NSThread:>{number = 5, name = (null)},++runloop:<CFRunLoop 0x60000222c700
。。。
entries =>
2 : <CFRunLoopMode 0x60000252d040>{name = kCFRunLoopDefaultMode,
port set = 0x6107,
queue = 0x600003026380,
source = 0x600003026b80 (not fired), timer port = 0x9d03,
sources0 = (null),
sources1 = (null),
observers = (null),
// 注意,这里timers数组中又为空了
timers = <CFArray>{type = mutable-small, count = 0, values = ()},
}

日志显示,在调用perform..afterDelay后,子线程 runloop 的 CFRunLoopMode 的timers数组中有了一个CFRunLoopTimer对象!它就是系统为延时操作创建的一个定时器;延时操作完成之后,此定时器对象又自动从timers数组中移除。

同时,如果在调用-onSelector:时打断点,可得到如下的堆栈信息:

pic_perform_afterDelay

从堆栈上也可以看到,延时操作的本质是运行时在当前线程的 runloop 中添加了一个“定时源”~

需要注意的是:在子线程中使用这种延时执行方法时,如果不主动启动子线程的 runloop,那么 selector 是不会执行的。这也就是前面提到的“如果子线程的 Runloop 根本没有运行,那么定时器也不会触发”,实际的开发中一定要注意这一点。

另外:这两个方法都只是将selector的调用延迟某个时间长度,并不影响调用方法时所处的线程,即在A线程调用这两个performSelector,延迟后的selector还是在A线程中执行。

2.GCD定时器

首先要说明的是,GCD的定时器与上面三种不同,它不是runloop的源!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#import "GCDTimerTest.h"

@interface GCDTimerTest ()
@property (nonatomic, strong) dispatch_queue_t seriaqueue;
@property (nonatomic, strong) dispatch_source_t timer;
@end

@implementation GCDTimerTest

- (void)gcd_timer{
_seriaqueue = dispatch_queue_create("xx", DISPATCH_QUEUE_SERIAL);
__weak typeof(self) wself = self;
dispatch_async(_seriaqueue, ^{
__strong typeof(wself) sself = wself;
NSLog(@"+++runloop before timer:%@",[NSRunLoop currentRunLoop]);
sself.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, sself.seriaqueue);
dispatch_source_set_timer(sself.timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(sself.timer, ^{
NSLog(@"++++执行计时任务");
//dispatch_source_cancel(sself.timer);
});
dispatch_resume(sself.timer);
NSLog(@"+++runloop after timer:%@",[NSRunLoop currentRunLoop]);
});
}
@end

日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
+++runloop before timer:<CFRunLoop
common mode items = (null),
modes = <CFBasicHash 0x600001b71dd0>{
entries =>
2 : <CFRunLoopMode 0x600002e2a8a0>{
name = kCFRunLoopDefaultMode,
source = 0x600003b25900 (not fired),
timer port = 0x5f03,
sources0 = (null),
sources1 = (null),
observers = (null),
timers = (null),
}
// 开始GCD定时器之后
+++runloop after timer:<CFRunLoop 0x600002928200{
modes = <CFBasicHash 0x600001b71dd0
entries =>
2 : <CFRunLoopMode 0x600002e2a8a0>{
name = kCFRunLoopDefaultMode,
sources0 = (null),
sources1 = (null),
observers = (null),
timers = (null),
}
++++执行计时任务

日志中可以看到,开启GCD定时器之后,当前runloop的timers中也并未增加新的定时源~

所以,GCD定时器并不是由CFRunLoopTimer实现的,也不需要加入到runloopMode中,不受runloop模式切换的影响,甚至在切换到后台时,它依然能正常运行。

4.观察者

Runloop在处理输入事件的同时,在其运行的特定阶段还会触发通知。我们可以使用 CF 的方法,注册观察者来接收通知并在某个特定时期处理一些事情。

Runloop可以被观察的状态包括以下阶段:

1
2
3
4
5
6
7
8
9
10
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //runloop进入时;
kCFRunLoopBeforeTimers = (1UL << 1), //将要处理Timer时;
kCFRunLoopBeforeSources = (1UL << 2), //将要处理Source0时;
kCFRunLoopBeforeWaiting = (1UL << 5), //将要进入睡眠时;
kCFRunLoopAfterWaiting = (1UL << 6), //将要被唤醒时;
kCFRunLoopExit = (1UL << 7), //runloop即将退出时;
kCFRunLoopAllActivities = 0x0FFFFFFFU
};

#示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
- (void)runloopReturnTest
{
dispatch_async(dispatch_get_global_queue(0, 0), ^{

NSRunLoop *mRunloop = [NSRunLoop currentRunLoop];

// 创建一个观察者.
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(
kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES, 0,
^(CFRunLoopObserverRef observer, CFRunLoopActivity activity){
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"observer: kCFRunLoopEntry...");
break;

case kCFRunLoopBeforeTimers:
NSLog(@"observer: kCFRunLoopBeforeTimers...");
break;

case kCFRunLoopBeforeSources:
NSLog(@"observer: kCFRunLoopBeforeSources...");
break;

case kCFRunLoopBeforeWaiting:
NSLog(@"observer: kCFRunLoopBeforeWaiting...");
break;

case kCFRunLoopAfterWaiting:
NSLog(@"observer: kCFRunLoopAfterWaiting...");
break;

case kCFRunLoopExit:
NSLog(@"observer: kCFRunLoopExit...");
break;

default:
break;
}
});

if (observer){
//把观察者附加到runloop上
CFRunLoopRef cfLoop = [mRunloop getCFRunLoop];
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
}

NSLog(@"This thread starting.......");

//新增计时器源并加入runloop
NSTimer *timer = [NSTimer timerWithTimeInterval:5
target:self
selector:@selector(onHandleTask:)
userInfo:nil
repeats:NO];
[mRunloop addTimer:timer forMode:NSDefaultRunLoopMode];

//最后一个参数:是否处理完事件返回,结束runLoop
SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 100, YES);

switch (result) {
case kCFRunLoopRunFinished:
NSLog(@"kCFRunLoopRunFinished");
break;

case kCFRunLoopRunStopped:
NSLog(@"kCFRunLoopRunStopped");
break;

case kCFRunLoopRunTimedOut:
NSLog(@"kCFRunLoopRunTimedOut");
break;

case kCFRunLoopRunHandledSource:
NSLog(@"kCFRunLoopRunHandledSource");
break;

default:
break;
}
NSLog(@"This thread end.......");
});
}

- (void)onHandleTask:(NSTimer *)timer{
[timer invalidate];
NSLog(@"timer Fired...");
}

上面的示例中,先往当前子线程的 runloop 中添加了一个观察者,监听并打印各个阶段的状态;随后往 runloop 中添加了一个定时器。执行到CFRunLoopRunInMode时,程序卡在这一行。5秒后计时器触发,计时器invalidate后,Runloop 中没有了任何事件源,所以退出并返回了result值,程序继续向下运行,输出日志如下:

1
2
3
4
5
6
7
8
9
10
23:48:10.831  This thread starting.......
23:48:10.831 observer: kCFRunLoopEntry...
23:48:10.832 observer: kCFRunLoopBeforeTimers...
23:48:10.832 observer: kCFRunLoopBeforeSources...
23:48:10.833 observer: kCFRunLoopBeforeWaiting...
23:48:15.834 observer: kCFRunLoopAfterWaiting...
23:48:15.835 timer Fired...
23:48:15.835 observer: kCFRunLoopExit...
23:48:15.836 kCFRunLoopRunFinished
23:48:15.836 This thread end.......

5.事件序列

1、通知观察者 Runloop 已经启动;

2、通知观察者 Timers 即将触发;

3、通知观察者 将要处理 Source0;

4、触发 Source0 回调;

5、如果有 Source1 处于 ready 状态,直接进入步骤9处理该 Source1事件;

6、通知观察者 线程即将休眠;

7、线程休眠 等待以下情形的唤醒:

  • 某一事件到达基于端口的源(Source1);
  • 定时器时间到了;
  • Runloop 设置的时间已经超时;
  • Runloop 被手动唤醒;

8、通知观察者线程将被唤醒;

9、处理唤醒时收到的消息:

  • 如果消息是Timer类型,则触发该Timer的回调;
  • 如果消息是 dispatch 到 main_ queue 的block,执行block;
  • 如果消息是Source1类型,则处理Source1回调;

10、以下条件中满足时候退出循环,否则从(2)继续循环:

  • 事件处理完毕,且启动 RunLoop 时参数设置为一次性执行;
  • 启动 RunLoop 时设置的最大运行时间到期;
  • RunLoop 被外部调用强行停止;
  • 启动 RunLoop 的 mode items为空;

11、通知观察者 Runloop 结束。

上面逻辑对应的 CFRunLoop.c源码 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
/// 用DefaultMode启动
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(),
kCFRunLoopDefaultMode, 1.0e10, false);
}

/// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef modeName,
CFTimeInterval seconds, Boolean stopAfterHandle) {

return CFRunLoopRunSpecific(CFRunLoopGetCurrent(),
modeName, seconds, returnAfterSourceHandled);
}

/// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName,
seconds, stopAfterHandle) {

/// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 如果mode里没有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;

/// 1. 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);

/// 内部函数,进入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {

Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {

/// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 处理非延迟的主线程调用
__CFRunLoopDoBlocks(runloop, currentMode);

/// 4. RunLoop 触发 Source0 (非port) 回调。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);

/// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}

/// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}

/// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
/// • 一个基于 port 的Source 的事件。
/// • 一个 Timer 到时间了
/// • RunLoop 自身的超时时间到了
/// • 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}

/// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);

/// 收到消息,处理消息。
handle_msg:

/// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}

/// 9.2 如果有dispatch到main_queue的block,执行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}

/// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop,
currentMode, livePort);

sourceHandledThisLoop = __CFRunLoopDoSource1(runloop,
currentMode, source1, msg);

if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}

/// 执行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);


if (sourceHandledThisLoop && stopAfterHandle) {
/// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}

/// 如果没超时,mode里没空,loop也没被停止,那继续loop。
} while (retVal == 0);
}

/// 10. 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。RunLoop 的核心是 mach_msg() 函数(见上面代码的第7步),RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。

6.Runloop对象

Runloop 对象提供了添加输入源、定时器、观察者以及启动 Runloop 的接口。每个线程都有唯一的与之关联的 Runloop对象。Cocoa和 CF 都有相应的接口可以操作 RunLoop:

  • Cocoa中对应的是 NSRunLoop;
  • CF 中对应的是 CFRunLoopRef;

1.获取RunLoop

这是 CF 框架提供的,它提供了纯C函数的API,这些API都是线程安全的。

1
CFRunLoopRef aCFRunloopObjRef = CFRunLoopGetCurrent();

这是Cocoa框架提供的,它提供了面向对象的 API,是基于CFRunLoopRef的一层封装,但这些API是非线程安全的;苹果官方文档说,我们不能在当前线程中去call另外一个线程中 NSRunLoop 对象的方法,那样很可能会造成意想不到的后果。

1
NSRunLoop *aRunloopObj = [NSRunLoop currentRunLoop];

不过,两种类型的 Runloop 可以混合使用。鉴于 CFRunLoopRef 是线程安全的,所以,可以通过 NSRunLoop 类的实例方法获取对应的 CFRunLoopRef 对象,进而达到线程安全的目的:

1
2
NSRunLoop *aRunloopObj = [NSRunLoop currentRunLoop];
CFRunLoopRef aCFRunloopObjRef = [aRunloopObj getCFRunLoop];

2.启动RunLoop

1.NSRunLoop的启动

1
- (void)run; //无条件的启动

不建议使用,因为这个接口会导致Run Loop永久性的在NSDefaultRunLoopMode模式。即使用CFRunLoopStop()函数也无法停止Run Loop的运行,除非能移除这个runloop上的所有事件源,不然这个子线程就无法停止,只能永久运行下去。

1
- (void)runUntilDate:(NSDate *)limitDate;//设置超时时间

比上面的接口好点,有个超时时间,可以控制每次 Runloop 的运行时间,也是运行在NSDefaultRunLoopMode模式。这个方法运行 Runloop 一段时间会退出给你检查运行条件的机会,如果需要可以再次运行 Runloop。

注意:使用这种方式启动runloop时,CFRunLoopStop()函数也无法停止这个runloop。

#示例:

1
2
3
4
5
6
7
8
BOOL finished = NO;

while(!finished) {
[[NSRunLoop currentRunLoop] runUntilDate:
[NSDate dateWithTimeIntervalSinceNow: 1]];

NSLog(@"exit runloop ......");
}

这个finished是我们自定义的一个Bool值,用来控制是否还需要开启下一次 Runloop。

上面例子所做的事:while循环内部有个 RunLoop 每秒循环一次,Runloop 结束后会输出exit Runloop ……。while循环会根据 finished 值来判断是否再去运行 Runloop。

输出日志如下:

1
2
3
4
5
6
21:20:20.980211+0800 exit runloop ......
21:20:21.981915+0800 exit runloop ......
21:20:22.983668+0800 exit runloop ......
21:20:23.984748+0800 exit runloop ......
21:20:24.986541+0800 exit runloop ......
21:20:25.988267+0800 exit runloop ......
1
- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;//特定的模式

比上面的方法多了mode参数,不同的是,这种运行方式是可以被CFRunLoopStop()函数停止的。

2.CFRunLoopRef的启动

1
void CFRunLoopRun();

使用这种方式启动后,Runloop 会一直运行,直到显示地调用CFRunLoopStop()才会停止。另外,删除 RunLoop 的所有事件源后,也能停止这个 Runloop。

1
SInt32 CFRunLoopRunInMode(mode, second, returnAfterSourceHandled);
  • 参数mode:模式;
  • 参数second:runloop的循环时间;
  • 参数returnAfterSourceHandled:是否在处理事件后让Run Loop退出返回;

这种方式启动的 Runloop 也可以使用CFRunLoopStop()来主动停止。NSRunloop 中的第三种启动方式,实质上就是基于这种方式的封装,只不过指定了最后一个returnAfterSourceHandled参数为YES。

  • Runloop的返回值

启动 Runloop 后,代码停在这一行不返回。当有值返回时Runloop就结束了。这个返回值就是Runloop结束原因,枚举如下:

1
2
3
4
5
6
7
/* Reasons for CFRunLoopRunInMode() to Return */
typedef CF_ENUM(SInt32, CFRunLoopRunResult) {
kCFRunLoopRunFinished = 1, //Runloop结束,所有的Sources都已被移除,无事件源可监听;
kCFRunLoopRunStopped = 2, //Runloop被使用CFRunLoopStop函数停止;
kCFRunLoopRunTimedOut = 3, //超时;
kCFRunLoopRunHandledSource = 4 //Runloop已处理完事件;
};

回头看看NSRunloop的第三种启动方式:

1
- (Bool)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

返回值为一个Bool值,如果是”performSelector” 事件或者其他 Input Source 事件触发并处理完成后,Runloop会退出并返回YES,其他情况下返回NO。

3.退出RunLoop

上面也有提到,有三种方法可以让 Runloop 处理事件之前退出:

  • 指定超时时间:

这可以使 Runloop 退出前完成所有正常操作,包括发送消息给 Runloop 观察者,如果可以配置的话,推荐使用这种方法。

  • 使用CFRunLoopStop:

这可以显式的停止 Runloop,Runloop 会把所有剩余的通知发送出去再退出。

  • 移除 Runloop 的输入源和定时器:

尽管这种方式也可能导致 Runloop 退出,但这并不是可靠的退出 Runloop 的方法。一些系统例程会添加输入源到 Runloop 里面来处理所需事件。因为你的代码未必会考虑到这些输入源,这样可能导致你无法移除它们,从而导致退出 Runloop 失败。

7.应用场景

1.场景汇总

  1. 开启常驻线程时,需要在当前线程中启动 runloop,如AFN;
  2. 在子线程中使用定时器时,需要在子线程中启动 runloop,定时器才能正常启动;
  3. 创建定时器,在当前 runloop 的特定Mode下执行,如滑动时的定时器;
  4. 在特定Mode下执行任务,default模式下设置图片、加载缓存。
  5. 添加观察者,在 runloop 的特定时刻处理某些事情;
  6. 使用端口或自定义输入源来和其他线程通信;

2.其他应用

ibireme的这篇博客里提到了一些具体的应用场景:

  • AutoreleasePool(监听通知以执行自动释放池的push与pop)
  • 界面更新(beforeWaiting时更新界面)
  • 事件响应(port->source1->source0)
  • 手势识别
  • 定时器
  • GCD
  • 网络请求

3.卡顿检测

卡顿原理:

  1. 主线程runloop处于beforeSource或afterWaiting状态时表示正在执行任务;
  2. 设置一个卡顿时长阈值T,隔时长T检查一次当前runloop的状态(mode);
  3. 若多个检查周期中,一直处于以上两种状态,而没有休眠,则说明线程处于卡顿状态;
  4. 获取卡顿时堆栈的情况,记录并上报;

实现方案:

  1. 监听并记录主线程runloop的mode(状态);
  2. 开启一个常驻线程,处理检测、上报任务;
  3. 启动定时器,阈值T时间到后比对runloop的状态;
  4. 当阈值T时间内连续检测到状态为beforeSource或afterWaiting,即为卡顿,上报堆栈;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
class RunLoopMonitor {

private init() {}

static let shared: RunLoopMonitor = RunLoopMonitor.init()
var loopObserver: CFRunLoopObserver?
var loopActivity: CFRunLoopActivity?
var aSemaphore: DispatchSemaphore?
var stuckTimes = 0 //出现卡顿的次数

/*原理:
线程runloop处于beforeSource或afterWaiting状态时表示正在执行任务;
多个检查周期中,runloop一直处于beforeSource或afterWaiting状态,
而没有休眠,则说明runloop处于卡顿状态。
*/
func startMonitor() {
let uptr = Unmanaged.passRetained(self).toOpaque()
let vptr = UnsafeMutableRawPointer(uptr)
var context = CFRunLoopObserverContext.init(version: 0,
info: vptr,
retain: nil,
release: nil,
copyDescription: nil)
//创建、添加观察者
loopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
CFRunLoopActivity.allActivities.rawValue,
true,
0,
observerCallBack(),
&context)
CFRunLoopAddObserver(CFRunLoopGetMain(), loopObserver, .commonModes)

aSemaphore = DispatchSemaphore.init(value: 0)

DispatchQueue.global().async {
while true {
// 方案一:通过单次超时时间判断,如超过250毫秒即为卡顿;
// 方案二:连续多次短期检测中均超时即为卡顿;
let st = self.aSemaphore?.wait(timeout: DispatchTime.now() + .milliseconds(80)) //戴铭在GCDFetchFeed中认为连续三次超时80秒就是卡顿
//80毫秒超时后
if st == .timedOut {
//指针检测
guard self.loopObserver != nil else {
self.aSemaphore = nil
self.loopActivity = nil
self.stuckTimes = 0
return
}
//如果loop处在“工作”状态,就无法休眠,记录+1
if self.loopActivity == .afterWaiting ||
self.loopActivity == .beforeSources
{
//记录一次卡顿
self.stuckTimes += 1
//未超过3次,表示正常
if self.stuckTimes < 3 { continue }
//超过三次,即为出现卡顿
DispatchQueue.global().async {
//获取当前卡顿时的堆栈 上报
}
}
}
}
}
}

//观察者回调
private func observerCallBack() -> CFRunLoopObserverCallBack {
return { (observer, activity, context) in
let weakself = Unmanaged<RunLoopMonitor>.fromOpaque(context!).takeUnretainedValue()
//记录runloop的状态
weakself.loopActivity = activity
weakself.aSemaphore?.signal()
}
}

//结束监测
func end() {
guard let _ = loopObserver else { return }
CFRunLoopRemoveObserver(CFRunLoopGetMain(), loopObserver, .commonModes)
loopObserver = nil
}
}

相关参考:

#©官方文档

#©ibireme

#©微信卡顿检测

#©百度孙源-优化UITableViewCell高度计算的那些事


Runloop
https://davidlii.cn/2017/09/09/runloop.html
作者
Davidli
发布于
2017年9月9日
许可协议