学习MJ的视频课程,整理总结知识点–Runloop

RunLoop的运行逻辑

RunLoop 循环等待处理sources、timers,处理完睡觉

RunLoop-1

RunLoop-2

RunLoop-3

RunLoop-4

MJ流程图介绍(熟悉)
切换模式、线程销毁会推出Loop;循环2-10的步骤

实践RunLoop的调用流程

1
2
3
4
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 函数调用栈 LLDB中使用backtrace命令: bt
NSLog(@"111111");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
* frame #0: 0x00000001040f3571 Interview01-runloop流程`__29-[ViewController viewDidLoad]_block_invoke_2(.block_descriptor=0x00000001040f50c0) at ViewController.m:32:9
frame #1: 0x000000010440ddf0 libdispatch.dylib`_dispatch_call_block_and_release + 12
frame #2: 0x000000010440ed64 libdispatch.dylib`_dispatch_client_callout + 8
frame #3: 0x000000010441ce1c libdispatch.dylib`_dispatch_main_queue_callback_4CF + 1500
frame #4: 0x00007fff23afb699 CoreFoundation`__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
frame #5: 0x00007fff23af62f9 CoreFoundation`__CFRunLoopRun + 2329
frame #6: 0x00007fff23af56b6 CoreFoundation`CFRunLoopRunSpecific + 438
frame #7: 0x00007fff3815cbb0 GraphicsServices`GSEventRunModal + 65
frame #8: 0x00007fff47162a67 UIKitCore`UIApplicationMain + 1621
frame #9: 0x00000001040f3680 Interview01-runloop流程`main(argc=1, argv=0x00007ffeebb0bc68) at main.m:14:16
frame #10: 0x00007fff5123bcf5 libdyld.dylib`start + 1
frame #11: 0x00007fff5123bcf5 libdyld.dylib`start + 1

打印函数调用栈 bt

RunLoop执行流程源码分析,从CFRunLoopRunSpecific开始,在CFRunLoop.c中搜索CFRunLoopRunSpecific,找打对应的函数

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
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
__CFRunLoopLock(rl);
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
Boolean did = false;
if (currentMode) __CFRunLoopModeUnlock(currentMode);
__CFRunLoopUnlock(rl);
return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
}
volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
CFRunLoopModeRef previousMode = rl->_currentMode;
rl->_currentMode = currentMode;
int32_t result = kCFRunLoopRunFinished;

if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

__CFRunLoopModeUnlock(currentMode);
__CFRunLoopPopPerRunData(rl, previousPerRun);
rl->_currentMode = previousMode;
__CFRunLoopUnlock(rl);
return result;
}

调用细节
CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
_CFRUNLOOP_IS_CALLING_OUT_TO

GCD的很多流程是不依赖RunLoop的,除了dispatch_async(dispatch_get_main_queue(), ^{});

休眠细节
RunLoop休眠的实现原理
线程休眠,操作系统内核层面API才能实现,实现:mach_msg函数
用户态、内核态(操作系统的知识稍微补一下)
等待消息
没有消息就让线程休眠
有消息就唤醒线程

状态
活动名状态

模式
两种,执行时互相隔离,互不影响

应用

控制线程的生命周期(线程保活)

线程是默认执行完里面的任务后就会被自动销毁,有些场景需要经常在子线程做事情,但是一做完就销毁了,再次使用还要创建,频繁的创建销毁时比较浪费的。AFN避免线程频繁创建销毁,使用RunLoop来使线程做完事情后以休眠态阻塞线程,避免被销毁,达到保活的效果

保活基础代码

如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出

1
2
3
// 往RunLoop里面添加Source\Timer\Observer
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];

touchesBegan时让子线程做事情,完整代码如下

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
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
MJThread *thread = [[MJThread alloc] initWithTarget:self selector:@selector(run) object:nil];

[thread start];
self.thread = thread;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 让线程做事情,RunLoop检测到performSelector:onThread:withObject会醒过来干活
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:YES];
// 在子线程调用stop(waitUntilDone设置为YES,代表子线程的代码执行完毕后,这个方法才会往下走)
}

#pragma mark - Action

- (void)run {
NSLog(@"start --%s", __func__);

// 往RunLoop里面添加Source\Timer\Observer
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];

NSLog(@"end --%s", __func__);
}

// 子线程需要执行的任务
- (void)test {
NSLog(@"%s", __func__);
}
@end

优化

上面的代码,[MJThread alloc] initWithTarget:self thread会强引用target

1
2
3
4
5
6
7
8
9
MJThread *thread = [[MJThread alloc] initWithBlock:^{
NSLog(@"start --%s", __func__);

// 往RunLoop里面添加Source\Timer\Observer
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];

NSLog(@"end --%s", __func__);
}];

此时控制器可以正常销毁,但是线程还没销毁
因为线程里面的block任务还没执行完毕,NSLog(@"end --%s", __func__);还没执行

run的注释
run注释

run是个无限循环,想停止的话只能使用如下方法
解决如下:

1
2
3
4
5
6
7
8
9
10
11
12
MJThread *thread = [[MJThread alloc] initWithBlock:^{
NSLog(@"start --%s", __func__);

// 往RunLoop里面添加Source\Timer\Observer
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
[theRL addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
while (weakSelf.shouldKeepRunning) {
[theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
NSLog(@"end --%s", __func__);
}];
[thread start];

在需要销毁时把shouldKeepRunning修改为NO

1
2
3
4
5
6
7
8
- (IBAction)stop:(id)sender {
[self performSelector:@selector(stopRunLoop) onThread:self.thread withObject:nil waitUntilDone:NO];
}

- (void)stopRunLoop {
self.shouldKeepRunning = NO;
CFRunLoopStop(CFRunLoopGetCurrent());
}

封装

1
2
3
4
5
 // 创建上下文(要初始化一下结构体)
CFRunLoopSourceContext context = {0};

// 创建source
CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);

这就是封装后的

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
@implementation MJPermenantThread
#pragma mark - public methods
- (instancetype)init
{
if (self = [super init]) {
self.innerThread = [[MJThread alloc] initWithBlock:^{
NSLog(@"begin----");

// 创建上下文(要初始化一下结构体)
CFRunLoopSourceContext context = {0};

// 创建source
CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);

// 往Runloop中添加source
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);

// 销毁source
CFRelease(source);

// 启动
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);

// while (weakSelf && !weakSelf.isStopped) {
// // 第3个参数:returnAfterSourceHandled,设置为true,代表执行完source后就会退出当前loop
// CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
// }

NSLog(@"end----");
}];

[self.innerThread start];
}
return self;
}

//- (void)run
//{
// if (!self.innerThread) return;
//
// [self.innerThread start];
//}

- (void)executeTask:(MJPermenantThreadTask)task
{
if (!self.innerThread || !task) return;

[self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
}

- (void)stop
{
if (!self.innerThread) return;

[self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
}

- (void)dealloc
{
NSLog(@"%s", __func__);

[self stop];
}

#pragma mark - private methods
- (void)__stop
{
CFRunLoopStop(CFRunLoopGetCurrent());
self.innerThread = nil;
}

- (void)__executeTask:(MJPermenantThreadTask)task
{
task();
}

@end

解决NSTimer在滑动时停止工作的问题

RunLoop只能处于某一种模式,默认模式,拖拽时切换到另一种模式,所以NSTimer的默认模式消失
NSRunLoopCommonModes并不是一个真的模式,它只是一个标记,timer能在_commonModes数组中存放的模式下工作

RunLoop里面放的有模式,模式里面放的有timers,timer设置为commonmodes时会添加到runloop的_commonModeitems
timer工作:在runloop的执行流程中工作,timer能唤醒RunLoop

监控应用卡顿

性能优化

总结

Runloop循环处理当前模式下的source0、source1、timers、observes

参考和源码

源码:
CF源码

参考:
iOS Runloop与线程保活
深入理解RunLoop

评论