一个应用开始运行以后放在那里,如果不对它进行任何操作,这个应用就像静止了一样,不会自发的有任何动作发生,但是如果我们点击界面上的一个按钮,这个时候就会有对应的按钮响应事件发生。给我们的感觉就像应用一直处于随时待命的状态,在没人操作的时候它一直在休息,在让它干活的时候,它就能立刻响应。这就是Runloop的功劳。
Runloop是什么
`1. 它是个结构体对象`
`2. 对象内部会一直运行一个循环,其实就是个do..while循环,用来保证线程的长久存活(线程是处理事件的)`
不是单纯的dowhile循环,内部会发生一个用户态到内核态切换,可以节约CPU资源,提供程序的性能:该做事就做事,该休息就休息
`3. 这个循环维护事件,主要处理APP中以下六类事件(几乎全部事件),非这些被runloop管理的事件 就是系统macho管理`
block事件
GCD事件
timer事件
observer事件 - 监听Runloop的变化
source0事件 - 最常用的事件,包括方法调用,触摸事件,UIEvent事件等
source1事件 - 很少,主要是依赖于port进行线程通信以及处理系统内核的mach_msg事件
微信卡顿监控就是利用observer事件通知来记录下最近一次main runloop活动时间,在另一个check线程中用定时器检测当前时间距离最后一次活动时间过久来判断在主线程中的处理逻辑耗时和卡主线程
Runloop作用
`1. 保证线程的存活,而不是线性的执行完任务就退出了`
线程一般都是一次执行完毕任务,就销毁了,而在线程中添加了runloop,并运行起来,实际上是添加了一个do..while循环
这样这个线程的程序就一直卡在do..while循环上,相当于线程的任务一直没有执行完,所有线程一直不会销毁
一旦我们添加了一个runloop,并run了,我们如果要销毁这个线程,必须停止runloop
`2. 处理事件`
`3. 实现异步`
可以优化UITableView加载本地图片的卡顿
SDWebImage是加载网络图片的,当加载本地图片时,如每一行Cell都有三张图片,很多行,那么刚进这个页面会滑不动,因为此时系统要绘制非常多的图片
需要用异步来解决,关于Runloop的异步实现是:
监听Runloop的空闲状态,在Runloop即将休眠时(空闲时)再去绘制图片
`4. 常驻线程`
`5. Runloop会在一次循环绘制屏幕上所有的点`
如何实现常驻线程参考文章: https://www.jianshu.com/p/2f038d247aa2
实现异步
用Runloop来实现异步:监听Runloop的空闲状态,在Runloop即将休眠时(空闲时)再去处理事情
#import "GCRunloopObserver.h"
@interface GCRunloopObserver()
@property (nonatomic, strong) NSMutableArray *taskArray;
@end
@implementation GCRunloopObserver
+ (instancetype)runloopObserver {
static dispatch_once_t once;
static GCRunloopObserver *observer;
dispatch_once(&once, ^{
observer = [[GCRunloopObserver alloc] init];
});
return observer;
}
- (instancetype)init
{
self = [super init];
if (self) {
//监听kCFRunLoopBeforeWaiting 即将休眠状态,当Runloop处于空闲的状态,就会触发这个回调,在这里做想做的任务
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopBeforeWaiting, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
if (self.taskArray.count == 0) {
return;
}
// 取出任务
void(^task)(void) = self.taskArray.firstObject;
// 执行任务
task();
// 第一个任务出队列
[self.taskArray removeObjectAtIndex:0];
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
CFRelease(observer);
}
return self;
}
- (void)addTask:(void(^)(void))task {
if (task) {
[self.taskArray addObject:task];
}
}
- (NSMutableArray *)taskArray {
if (_taskArray == nil) {
_taskArray = [NSMutableArray array];
}
return _taskArray;
}
@end
在外部调用Runloop的工具类的addTask方法,把耗时任务添加到block中,然后加入到Runloop的工具类的taskArray数组中,当runloop空闲时,就会挨个执行数组中的任务
__weak typeof(self) weakSelf = self;
[[GCRunloopObserver runloopObserver] addTask:^{
UIImageView *imgView = weakSelf.imgViewArray[i];
UIImage *img = [UIImage imageNamed:dataArray[i]];
imgView.image = img;
}];
如何使用Runloop
######1. 创建RunLoop
苹果不允许直接创建RunLoop,它只提供了四个自动获取的函数
[NSRunLoop currentRunLoop];//获取当前线程的RunLoop
[NSRunLoop mainRunLoop];//获取主线程的RunLoop
CFRunLoopGetMain();
CFRunLoopGetCurrent();
######2. 子线程默认不开启Runloop
主线程是唯一一个例外,当App启动以后主线程会自动开启一个RunLoop来保证主线程的存活并处理各种事件。任意一个子线程的RunLoop都会保证主线程的RunLoop的存在。
######3. RunLoop能正常运行的条件是什么?
RunLoop如果没有事件需要处理的话,默认情况下,是不能自己维持事件循环,会直接退出,所以需要添加port / Source来维持事件循环机制
- (void)subThreadTodo{
NSLog(@"%@----开始执行子线程任务",[NSThread currentThread]);
//获取当前子线程的RunLoop
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
//注释掉下面这行和不注释掉下面这行分别运行一次
[runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
NSLog(@"RunLoop:%@",runLoop);
//让RunLoop跑起来
[runLoop run];
NSLog(@"%@----执行子线程任务结束",[NSThread currentThread]);
}
内部实现原理
1.Runloop与线程
`1. Runloop与线程一一对应, 通过一个全局可变字典进行关联CFMutableDictionaryRef`
runloop创建 - 依赖于线程__CFRunLoopCreate(pthread_main_thread_np())
runloop存储 - 线程的指针和runloop一起关联到字典里面
runloop获取 - 通过线程的指针取出
`2. 子线程的runloop默认不开启`
`3. 线程退出后,runloop会自动退出`
2.currentMode代表Runloop当前所处的模式
`1. 一个Runloop对应多个mode`
一个RunLoop可以有多个Mode,每个Mode中又可以有多个sources,observers,timers
之所以有多个Mode的原因:
假设当RunLoop运行在Mode1上时,如果此时Mode2的某个observers事件回调了,我们是没办法接收到Mode2中回调来的事件的
之所以RunLoop有多个Mode,就是为了起到这样一个屏蔽效果
当我们运行在Mode1上时,只能接收处理Mode1上的sources, observers以及Timers,其他Mode的回调事件,是不会给与处理的
栗子🌰
- tableView里面如果有滚动的广告栏,当我们滑动tableView时,广告栏就不滚动了
- 怎样保证子线程数据回来更新UI的时候不打断用户的滑动操作
在用户进行滑动的过程中,当前的RunLoop运行在UITrackingRunLoopMode模式下,而我们一般对网络请求是放在子线程中
子线程返回给主线程的数据要抛给主线程用来更新UI,可以把这部分逻辑包装起来,提交到主线程的default模式下
这样的话,当用户滑动时,default模式下的任务不会执行,当用户手停止时,mode就切换到了default模式下,就会处理子线程的数据了,这样就不会打断用户的滑动操作了
`2. 但每一次运行只会依赖于一个mode,每个事件源都会绑定在Run Loop的某个特定模式mode上`
-
NSDefaultRunLoopMode - 默认,空闲状态
UITrackingRunLoopMode - ScrollView滑动时
GSEventReceiveRunLoopMode - 接受系统事件的内部 Mode,通常用不到
KCFRunLoopCommonModes - 特殊的组合modes
UIInitializationRunLoopMode - 启动时,启动完成后就不再使用。
`3. CFRunLoopModeRef分析`
- sources集合,是个联合体
sources0 - 只包含一个回调指针,不能主动触发Runloop,需要用执行信号来标记它为待处理source,唤醒runloop去处理,然后移除source
source1 - 包含一个回调指针和mach_port,一旦source1事件到来,若此时Runloop在休眠,会立刻被唤醒,但source0就不能主动唤醒Runloop
- timers集合
timer执行条件:timer加入的mode必须和当前runloop的mode相等(滑动屏幕时timer不响应的原因),一个timer想同时加入到两个mode里面,需要将timer加入CommonModes中
timer一定会加入到runloop的某一个mode中:底层是将timer加到Items里面
- observers集合
observer会监听runloop的状态
Runloop的状态
kCFRunLoopEntry 即将进入RunLoop
kCFRunLoopBeforeTimers 即将处理Timer
kCFRunLoopBeforeSources 即将处理Source事件源
kCFRunLoopBeforeWaiting 即将进入休眠
kCFRunLoopAfterWaiting 刚从休眠中唤醒
kCFRunLoopExit 即将退出RunLoop
kCFRunLoopAllActivities 监听全部的活动类型
Runloop是怎么处理事件的?
程序从点击图标到程序启动,到程序被杀死这个过程中,系统是怎样实现的?
当调用Main函数之后,会调用UIApplicationMain函数,在内部启动主线程的RunLoop,经过一系列的处理,最终主线程的RunLoop处于休眠状态,之后如果点击了屏幕,会转成Source1, 就会把主线程唤醒,进行后续处理,当我们把线程杀死的时候,会发生RunLoop的退出通知,退出之后,线程就被销毁掉了
Runloop事件处理机制
Runloop启动后,首先会发出一个通知.告知观察者即将启动,之后将要处理Timer / Source0事件,然后进入正式的source0处理,如果有source1要处理,会通过一条goto语句实现,进行代码逻辑跳转,去处理唤醒时收到的消息,如果没有source1处理,此时线程即将进入休眠,同时也会发送通知给Observer,然后发生从用户态到内核态的切换,然后线程正式进入休眠,等待唤醒
- 唤醒线程或者RunLoop的条件有三个
- 通过Source1进行相关事件的唤醒
- Timer事件的回调到了
- 外部手动唤醒
网友评论