Runloop, AutoreleasePool
Runloop
一个线程一次只能执行一个任务,执行完成后线程就会退出。RunLoop 机制能让线程随时处理事件但并不退出。这里说的随时是指:程序需要运行时就保持程序的持续运行,不需要的时候就进入休眠状态。
RunLoop 的结构
和 RunLoop 相关的主要涉及五个类:
- CFRunLoopRef:RunLoop对象
- CFRunLoopModeRef:运行模式
- CFRunLoopSourceRef:输入源/事件源
- CFRunLoopTimerRef:定时源
- CFRunLoopObserverRef:观察者
一个RunLoop 对象中可以包含多个 Mode,每个 Mode 又包含多个个 Source、Timer、Observer。
RunLoop 中的 Mode
关于Mode首先要知道一个RunLoop 对象中可能包含多个Mode,且每次调用 RunLoop 的主函数时,只能指定其中一个 Mode(CurrentMode)。切换 Mode,需要重新指定一个 Mode 。主要是为了分隔开不同的 Source、Timer、Observer,让它们之间互不影响。
总共是有五种Mode:
kCFRunLoopDefaultMode:默认模式,主线程是在这个运行模式下运行UITrackingRunLoopMode:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响UIInitializationRunLoopMode:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式,实际是kCFRunLoopDefaultMode和UITrackingRunLoopMode的结合。
Runloop运行流程
基本概念:
- source0, 触摸事件,performSelector
- source1, 系统事件,基于 Port,
- timers, 定时器
- observers,监听器,用于监听runloop 状态,执行回调
Runloop 具体来说主要执行逻辑是这样的:
- 通知 Obersvers,RunLoop 已经启动
- 通知 Obersvers,即将处理 Timers
- 通知 Obersvers,即将处理 Sources
- 处理 Blocks (此处 blocks 为 CFRunLoopPerformBlock 调用的 block)
- 处理 Sources0(可能会再次处理 blocks)
- 如果存在 Source1 ,则跳转到第 8 步直接处理。
- 通知 Obersvers,开始休眠(开始等待消息唤醒)。
- 如果唤醒,通知 Obersvers,结束休眠,根据唤醒的消息类型可能会处理
- GCDTimers
- Timer
- Sources
- 处理GCD Async To Main Queue
- 处理 Blocks
- 根据前面执行结果判断接下来如何操作,可能会:
- 回到 2
- 到下一步11,结束 runloop
- 通知 Obersvers,RunLoop结束。
常驻线程
借助RunLoop可以实现线程后台常驻的功能:
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(runOne) object:nil];
[self.thread start];
}
- (void) runOne{
NSLog(@"----任务1-----");
// 下面两句代码可以实现线程保活
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
// 测试是否开启了RunLoop,如果开启RunLoop,则来不了这里,因为RunLoop开启了循环。
NSLog(@"未开启RunLoop");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// 利用performSelector,在self.thread的线程中调用run2方法执行任务
[self performSelector:@selector(runTwo) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void) runTwo{
NSLog(@"----任务2------");
}
AutoreleasePool
应用程序一旦启动,主线程 RunLoop 里注册了两个 Observer。
一个 Observer 监听即将进入Loop事件,回调内会调用 objc_autoreleasePoolPush() 创建自动释放池,并保证创建释放池发生在其他所有回调之前。
_另外一个 Observer 监视了两个事件(RunLoop即将进入休眠和即将退出 RunLoop 事件) ,前者会调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;后者会调用 _objc_autoreleasePoolPop() 来释放自动释放池,并保证释放自动释放池事件发生在其它回调之后。
实现原理
- 一个线程的自动释放池是一个指针堆栈
- 每一个指针或者指向被释放的对象,或者是自动释放池的POOL_BOUNDARY,POOL_BOUNDARY 是自动释放池的边界。
- 一个池子的 token 是指向池子 POOL_BOUNDARY 的指针。当池子被出栈的时候,每一个高于标准的对象都会被释放掉。
- 堆栈被分成一个页面的双向链表。页面按照需要添加或者删除。
- 本地线程存放着指向当前页的指针,在这里存放着新创建的自动释放的对象。
自动释放池是由 AutoreleasePoolPage 以双向链表的方式实现的,当对象调用 autorelease 方法时,会将对象加入 AutoreleasePoolPage 的栈中,调用 AutoreleasePoolPage::pop 方法会向栈中的对象发送 release 消息.
使用场景
通常情况下,我们是不需要手动添加 autoreleasepool 的,使用线程自动维护的 autoreleasepool 就好了。仅在下列三种情况下需要我们手动添加 autoreleasepool:
- 如果你编写的程序不是基于 UI 框架的,比如说命令行工具;
- 如果你编写的循环中创建了大量的临时对象;
- 如果你创建了一个辅助线程。
例子:
int lagerNum = 1024 * 1024 * 2 ;
for(int i = 0 ; i < lagerNum; i++)
{
@autoreleasepool{
NSString *str = [NSString stringWithFormat:@"Hello"];
str = [str uppercaseString];
str = [NSString stringWithFormat:@"%@ - %@ %d",str, @"World!", i];
// NSLog(@"%@", str);
}
}
在我的钢琴app:klaver中也加入了常驻线程来处理琴谱解析的问题,由于对于这个app琴谱解析是常用的功能,为了避免每次切换下一曲时重新创建线程的开销而使用常驻线程。
关于 performSelector
先记录一道题目:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"1");
[self performSelector:@selector(_sel1) ];
[self performSelector:@selector(_sel2) withObject: nil afterDelay:0];
[self performSelectorOnMainThread: @selector(_sel3) withObject: nil waitUntilDone: false];
NSLog(@"2");
});
-(void) _sel1 {
NSLog(@"call _sel1 %@", [NSThread currentThread]);
}
-(void) _sel2 {
NSLog(@"call _sel2 %@", [NSThread currentThread]);
}
-(void) _sel3 {
sleep(6);
NSLog(@"call _sel3 %@", [NSThread currentThread]);
}
运行结果如下:
2021-07-28 22:48:05.556735+0800 ForLife[5958:2029108] --------start
2021-07-28 22:48:05.557100+0800 ForLife[5958:2029108] call _sel1 <NSThread: 0x280a69b00>{number = 4, name = (null)}
2021-07-28 22:48:05.557384+0800 ForLife[5958:2029108] --------end
2021-07-28 22:48:11.576002+0800 ForLife[5958:2029084] call _sel3 <NSThread: 0x280a2c6c0>{number = 1, name = main}
问题:
- 为什么没打印 call _sel2,因为当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
- performSelector 内部机制,只是一个单纯的消息发送,就是对 objc_msgSend的封装,和时间没有一点关系,所以不需要添加到子线程的Runloop中也能执行
- performSelectorOnMainThread,该方法主要用来用主线程来修改页面UI的状态,当调用方主线程的时候,waitUntilDone 参数无效
既已览卷至此,何不品评一二: