最近项目完成年前的时间也比较轻松,对一些知识就行总结。今天总结的是Runloop.
对于Runloop,在平时项目中用的多吗?为毛,有时候一个项目都看不见一个Runloop的关键词,但面试还一直不断的问?那是因为我们其实使用了它,但它并不是以NSRunloop出现的,而是以其他的一些形式出现。
我们就以最明显的主线程来引出Runloop
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
这段代码绝对见过吧,但是点进去看过它具体是怎么实现的吗?
就和网上写的方法,我们打印一下看看结果。
NSLog(@"开始");
int num=UIApplicationMain(argc, argv, nil, appDelegateClassName);
NSLog(@"结束");
输出为:2021-01-29 11:11:00.388412+0800 RuntimeDemo[1174:38507] 开始
没打印出结束,这说明这个函数它就一直没结束。具体为什么没执行完的原因是下面这段代码。
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
看不懂do里面的代码没关系,到do-while总能看的懂吧,这是不是就可以回答了为什么一直没结束了。因为它是个死循环啊,就算地球爆炸它也会一直执行下去的啊。
好,这算是引出Runloop了,下面我们从易到难来看看Runloop的实现和底层原理。
什么是Runloop
runloop是通过内部维护的事件循环来对事件/消息进行管理的对象。
Runloop的使用
先来个简单的代码实现
self.num=0;
NSTimer *time=[NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timebtnclick) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:time forMode:NSDefaultRunLoopMode];
-(void)timebtnclick{
self.num++;
NSLog(@"---%ld",self.num);
}
这就是一个简单的runloop。上面的NSTime不用说,我们来看看添加time到runloop时的参数。
1.[[NSRunLoop currentRunLoop] :获取当前runloop,在这也没创建其他的线程和runloop,在上面提到了,app主线程本身就是一个死循环的runloop,所以获取的这个必然就是主任务。
2.time:这个就是所需的时间。至于为什么要添加time,先不着急,下面会一张图就明白了。
3.Mode:NSDefaultRunLoopMode NSRunLoopCommonModes UITrackingRunLoopMode
我们先来看上面三个model。
我们设置的是默认模式NSDefaultRunLoopMode,当滑动视图的时候,它进入的是UI模式的也就是UITrackingRunLoopMode,所以它肯定就不会有打印出来。 UI模式它的调起是有UI的点击事件触发的。
如果想要滑动的时候也运行runloop,我们是不是可以这样写
self.num=0;
NSTimer *time=[NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timebtnclick) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:time forMode:UITrackingRunLoopMode];
[[NSRunLoop currentRunLoop]addTimer:time forMode:NSDefaultRunLoopMode];
同时把time添加到了默认模式和UI模式。这个运行起来也没什么问题。但 NSRunLoopCommonModes可能更方便。
self.num=0;
NSTimer *time=[NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timebtnclick) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:time forMode:NSRunLoopCommonModes];
Runloop应用
还是和之前一样,我们先来看它的应用然后在看底层的原理
常驻线程
子线程执行完操作以后就会立即释放。即使强引用子线程也不能给子线程添加操作。下面我们来看段代码
-(IBAction)btnclick:(UIButton *)sender{
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//创建子线程并开启
NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(show) object:nil];
self.thread=thread;
[thread start];
}
-(void)show{
//添加一个端口
[[NSRunLoop currentRunLoop]addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
// //添加timer
// NSTimer *timer=[NSTimer scheduledTimerWithTimeInterval:2.0f target:self selector:@selector(test) userInfo:nil repeats:YES];
// [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//创建监听者
CFRunLoopObserverRef observer=CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop进入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop要处理Timers了");
break;
case kCFRunLoopBeforeSources:
NSLog(@"RunLoop要处理Sources了");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop要休息了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop醒来了");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop退出了");
break;
default:
break;
}
});
//添加监听者
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
//开启runloop
[[NSRunLoop currentRunLoop]run];
CFRelease(observer);
}
-(void)test{
NSLog(@"%@",[NSThread currentThread]);
}
点击屏幕开启子线程并开启Runloop,然后点击button,我们看看打印结果
2021-03-19 16:18:15.170791+0800 RuntimeDemo[37765:14285242] RunLoop进入
2021-03-19 16:18:15.171060+0800 RuntimeDemo[37765:14285242] RunLoop要处理Timers了
2021-03-19 16:18:15.171230+0800 RuntimeDemo[37765:14285242] RunLoop要处理Sources了
2021-03-19 16:18:15.171343+0800 RuntimeDemo[37765:14285242] RunLoop要休息了
2021-03-19 16:18:31.043323+0800 RuntimeDemo[37765:14285242] RunLoop醒来了
2021-03-19 16:18:31.043712+0800 RuntimeDemo[37765:14285242] RunLoop要处理Timers了
2021-03-19 16:18:31.043828+0800 RuntimeDemo[37765:14285242] RunLoop要处理Sources了
2021-03-19 16:18:31.044092+0800 RuntimeDemo[37765:14285242] <NSThread: 0x283b1d540>{number = 7, name = (null)}
2021-03-19 16:18:31.044213+0800 RuntimeDemo[37765:14285242] RunLoop退出了
2021-03-19 16:18:31.044474+0800 RuntimeDemo[37765:14285242] RunLoop进入
2021-03-19 16:18:31.044588+0800 RuntimeDemo[37765:14285242] RunLoop要处理Timers了
2021-03-19 16:18:31.044681+0800 RuntimeDemo[37765:14285242] RunLoop要处理Sources了
2021-03-19 16:18:31.044776+0800 RuntimeDemo[37765:14285242] RunLoop要休息了
发现runloop处于休眠状态。
图片下载
把setImage放到NSDefaultRunLoopMode去做,在滑动的时候并不会去调用复制图片的方法,而是等到欢动完毕切换到NSDefaultRunLoopMode下才去调用。
[self.img performSelector:@selector(setImage:) withObject:image afterDelay:0 inModes:[NSDefaultRunLoopMode]];
//上面传递过来的image
-(void)setImage:(UIImgae *)image{
}
滚动的ScrollView导致定时器失效
在界面上有一个滚动视图,如果执行一个定时器执行时间,在滚动过程中定时器会失效。这个时候,我们可以把timer注册到NSRunLoopCommonModes,[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
使用GCD创建定时器,GCD创建的定时器不会受到runloop的影响
监测卡顿
卡顿检测在这里先不说,我把它规划到性能优化中去了。
自动释放池
Runloop内部有一个自动释放池,当Runloop开启时,就会自动创建一个自动释放池,当Runloop休眠之前会释放掉自动释放池的东西,然后重新创建一个新的空的自动释放池,当runloop被唤醒执行时Timer source等新的时间会被放到新的自动释放池中,当runloop退出的时候也会被释放。
只有主线程的runloop会默认启动,意味着会自动创建自动释放池,子线程需要在线程调度方法中手动的添加自动释放池。
Runloop的底层原理
Runloop是什么?
Runloop可以理解为运行循环,是线程内一个运行时间处理和响应传入事件的一个循环。它的作用就是为了在有事件到达时唤醒线程一处理各种事件,事件处理完让进入休眠节省CPU资源。
runloop和线程的关系
线程的作用是用来执行一个或者多个任务的,在默认情况下,线程执行完之后就会销毁,那假如我不希望它销毁,该怎么办呢?这也就是我们在一开始提到的线程保活。
1.一条线程对应一个Runloop对象,每条线程都有唯一一个和他对应的Runloop对象
2.主线程的Runloop系统已经自动创建好了,子线程的Runloop需要主动创建
3.Runloop在第一获取时创建,在线程结束时销毁。
4.Runloop并不保证线程的安全。我们只能在当前线程内部操作当前项城的runloop对象,而不能再当前线程内部去操作其他线程的runloop对象。
上面说了runloop和线程的关系,下面我们来看看runloop的中的几个相关类。
这张图是不是经常见到,它很好的描述了runloop几个相关类的关系。
CFRunLoopRef:代表 RunLoop 的对象
CFRunLoopModeRef:代表 RunLoop 的运行模式
CFRunLoopSourceRef:就是 RunLoop 模型图中提到的输入源 / 事件源
CFRunLoopTimerRef:就是 RunLoop 模型图中提到的定时源
CFRunLoopObserverRef:观察者,能够监听 RunLoop 的状态改变
一个Runloop对象包含了若干个运行模式Model,而在运行模式下有包含了若干个输入源(CFRunLoopSourceRef)、定时源、观察者。
每次runloop启动时,只能指定其中一个运行模式,这个运行模式被称作当前运行模式。
如果需要切换运行模式,只能退出当前的loop,在重新指定一个运行模式。
知道了runloop的结构,我们来具体看看每一个具体是什么?又是怎么实现的。
Observer事件
是观察者,每个 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
};
Timer事件
延迟的performSelector,延迟的dispatch_after,基于时间的触发器,可以与NSTimer进行转换
Source事件
1.Source0事件:非基于port的处理事件,不能主动唤醒休眠中的runloop,需要手动触发。触摸屏幕时,屏幕表面的事件会先包装成Event,event先告诉source1,source1唤醒runloop,然后将事件event分发给source0,然后由source0来处理。
2.Source1事件:基于mach_port的,来自系统内核或其他进程或线程的事件,可以主动的唤醒休眠中的Runloop
block事件
费延迟的performSelector,非延迟的dispatch_after,block回调。
Main_Dispatch_Queue事件
GCD中main queue的block会被dispatch到main loop执行。
知道了上面这些知识点,我们再来看下Runloop的处理逻辑流程图。